Add dependency injection example (#2466)

This commit is contained in:
David Pedersen 2023-12-30 18:11:33 +01:00 committed by GitHub
parent c34041510c
commit 3b43b257e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 182 additions and 0 deletions

View file

@ -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"] }

View file

@ -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::<InMemoryUserRepo>))
.route("/users", post(create_user_generic::<InMemoryUserRepo>))
.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<dyn UserRepo>,
}
#[derive(Clone)]
struct AppStateGeneric<T> {
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<AppStateDyn>,
Json(params): Json<UserParams>,
) -> Json<User> {
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<AppStateDyn>,
Path(id): Path<Uuid>,
) -> Result<Json<User>, StatusCode> {
match state.user_repo.get_user(id) {
Some(user) => Ok(Json(user)),
None => Err(StatusCode::NOT_FOUND),
}
}
async fn create_user_generic<T>(
State(state): State<AppStateGeneric<T>>,
Json(params): Json<UserParams>,
) -> Json<User>
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<T>(
State(state): State<AppStateGeneric<T>>,
Path(id): Path<Uuid>,
) -> Result<Json<User>, 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<User>;
fn save_user(&self, user: &User);
}
#[derive(Debug, Clone, Default)]
struct InMemoryUserRepo {
map: Arc<Mutex<HashMap<Uuid, User>>>,
}
impl UserRepo for InMemoryUserRepo {
fn get_user(&self, id: Uuid) -> Option<User> {
self.map.lock().unwrap().get(&id).cloned()
}
fn save_user(&self, user: &User) {
self.map.lock().unwrap().insert(user.id, user.clone());
}
}