State
So now that we have the application core, a way to talk to it, and a way for it to obtain the data, we can now tie everything together.
We declare a State
struct in which we keep references to concrete drivers, the concrete constructor for our service, and we move the concrete type alias here.
#![allow(unused)] fn main() { use hextacy::adapters::db::sql::seaorm::SeaormDriver; use hextacy::adapters::queue::redis::RedisMessageQueue; use hextacy::adapters::queue::redis::RedisPublisher; pub type AuthenticationService = Authentication< SeaormDriver, UserAdapter, SessionAdapter, RedisPublisher, >; #[derive(Debug, Clone, State)] pub struct AppState { #[env("DATABASE_URL")] #[load_async] pub repository: SeaormDriver, #[env( "RD_HOST", "RD_PORT" as u16, "RD_USER" as Option, "RD_PASSWORD" as Option, )] pub redis_q: RedisMessageQueue, } impl AuthenticationService { pub async fn init(state: &AppState) -> AuthenticationService { AuthenticationService::new( state.repository.clone(), UserAdapter, SessionAdapter, state .redis_q .publisher("my-channel") .await .expect("Could not create publisher"), ) } } }
Neat!
For each field annotated with env
, the State
derive macro will attempt to call the type's associated new
function, loading variables from std::env
beforehand and passing them to the call. Luckily, both of these structs have them so we get an AppState::load_repository_env
function and the same for redis_q
. The as
will attempt to parse the value of the env variable before passing it to new
.
In the impl block for the service we set it up by calling it's new
function, created from the component
macro. All the components being passed satisfy the service's bounds. Here it's worth mentioning that the adapters are zero-sized, meaning they do not actually allocate any memory and are here simply to satisfy the bound restriction of the service, a sort of behaviour struct. The repository is cloned, which clones only the underlying reference to the connection pool and a publisher is created.
Finally, the main function.
#[tokio::main] async fn main() { hextacy::env::load_from_file("path/to/.env").unwrap(); let state = AppState::configure().await.unwrap(); let (host, port) = ( env::get_or_default("HOST", "127.0.0.1"), env::get_or_default("PORT", "3000"), ); info!("Starting server on {addr}"); let router = router(&state).await; axum::Server::bind(&addr.parse().unwrap()) .serve(router.into_make_service()) .await .expect("couldn't start server"); } pub async fn router(state: &AppState) -> Router { use crate::controllers::http::auth::*; let auth_service = AuthenticationService::init(state).await; let router = Router::new() .route("/register", post(register)) .route("/login", post(login)); Router::new().nest("/auth", router).with_state(service) }
And we have a working app! We haven't talked about how the files are set up, because this largely depends on preference and is ultimately arbitrary.
Next up, we'll ensure our app works by writing some tests.