//! Run with //! //! ```not_rust //! cargo run -p example-dependency-injection //! ``` use std::{ collections::HashMap, sync::{Arc, Mutex}, }; use axum::{ extract::{Path, State}, http::StatusCode, routing::{get, post}, Json, Router, }; use serde::{Deserialize, Serialize}; use tokio::net::TcpListener; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use uuid::Uuid; #[tokio::main] async fn main() { tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| format!("{}=debug", env!("CARGO_CRATE_NAME")).into()), ) .with(tracing_subscriber::fmt::layer()) .init(); let user_repo = InMemoryUserRepo::default(); // We generally have two ways to inject dependencies: // // 1. Using trait objects (`dyn SomeTrait`) // - Pros // - Likely leads to simpler code due to fewer type parameters. // - Cons // - Less flexible because we can only use object safe traits // - Small amount of additional runtime overhead due to dynamic dispatch. // This is likely to be negligible. // 2. Using generics (`T where T: SomeTrait`) // - Pros // - More flexible since all traits can be used. // - No runtime overhead. // - Cons: // - Additional type parameters and trait bounds can lead to more complex code and // boilerplate. // // Using trait objects is recommended unless you really need generics. let using_dyn = Router::new() .route("/users/{id}", get(get_user_dyn)) .route("/users", post(create_user_dyn)) .with_state(AppStateDyn { user_repo: Arc::new(user_repo.clone()), }); let using_generic = Router::new() .route("/users/{id}", get(get_user_generic::)) .route("/users", post(create_user_generic::)) .with_state(AppStateGeneric { user_repo }); let app = Router::new() .nest("/dyn", using_dyn) .nest("/generic", using_generic); let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap(); tracing::debug!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await; } #[derive(Clone)] struct AppStateDyn { user_repo: Arc, } #[derive(Clone)] struct AppStateGeneric { user_repo: T, } #[derive(Debug, Serialize, Clone)] struct User { id: Uuid, name: String, } #[derive(Deserialize)] struct UserParams { name: String, } async fn create_user_dyn( State(state): State, Json(params): Json, ) -> Json { let user = User { id: Uuid::new_v4(), name: params.name, }; state.user_repo.save_user(&user); Json(user) } async fn get_user_dyn( State(state): State, Path(id): Path, ) -> Result, StatusCode> { match state.user_repo.get_user(id) { Some(user) => Ok(Json(user)), None => Err(StatusCode::NOT_FOUND), } } async fn create_user_generic( State(state): State>, Json(params): Json, ) -> Json where T: UserRepo, { let user = User { id: Uuid::new_v4(), name: params.name, }; state.user_repo.save_user(&user); Json(user) } async fn get_user_generic( State(state): State>, Path(id): Path, ) -> Result, StatusCode> where T: UserRepo, { match state.user_repo.get_user(id) { Some(user) => Ok(Json(user)), None => Err(StatusCode::NOT_FOUND), } } trait UserRepo: Send + Sync { fn get_user(&self, id: Uuid) -> Option; fn save_user(&self, user: &User); } #[derive(Debug, Clone, Default)] struct InMemoryUserRepo { map: Arc>>, } impl UserRepo for InMemoryUserRepo { fn get_user(&self, id: Uuid) -> Option { self.map.lock().unwrap().get(&id).cloned() } fn save_user(&self, user: &User) { self.map.lock().unwrap().insert(user.id, user.clone()); } }