diff --git a/examples/dependency-injection/Cargo.toml b/examples/dependency-injection/Cargo.toml new file mode 100644 index 00000000..1f2801b0 --- /dev/null +++ b/examples/dependency-injection/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "example-dependency-injection" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +axum = { path = "../../axum", features = ["tracing", "macros"] } +serde = { version = "1.0", features = ["derive"] } +tokio = { version = "1.0", features = ["full"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +uuid = { version = "1.0", features = ["serde", "v4"] } diff --git a/examples/dependency-injection/src/main.rs b/examples/dependency-injection/src/main.rs new file mode 100644 index 00000000..b8e1a451 --- /dev/null +++ b/examples/dependency-injection/src/main.rs @@ -0,0 +1,169 @@ +//! 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(|_| "example_dependency_injection=debug".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.unwrap(); +} + +#[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()); + } +}