Dependency injection
siderite’s DI is inspired by FastAPI’s Depends, expressed with plain Rust
traits. Items live in siderite_core::di.
Declaring a dependency
Section titled “Declaring a dependency”struct Db { tenant: String }
impl Dependency for Db { async fn resolve(ctx: &mut ResolveContext<'_>) -> Result<Self, ApiError> { let config = ctx.resolve::<Config>().await?; Ok(Db { tenant: config.tenant.clone() }) } fn describe(op: &mut Operation, r: &mut SchemaRegistry) { /* OpenAPI */ }}
async fn handler(db: Depends<Db>) -> String { db.tenant.clone() }Depends<T> derefs to T and holds an Arc<T>.
Semantics
Section titled “Semantics”- Request-scoped and cached. Within one request a type resolves once, no matter how many arguments or nested dependencies ask for it. Failures are not cached.
- Cycles (
A -> B -> A) produce an opaque 500 problem; the logged error isDependencyError::Cycle { chain }. OpenAPI generation is also cycle-safe. - Application scope:
App::provide(value)/provide_arc(arc)share a value. It wins overT::resolveforDepends<T>. For types that are not dependencies, use theProvided<T>extractor. - Overrides (tests):
App::override_dependency::<T, _, _>(|head| async { .. })andApp::override_value(value)replaceT::resolve, including whenTis requested by another dependency. The closure receives an ownedRequestHead(method, URI, headers); a borrowedPartscannot cross the returned future. - Global dependencies:
App::dependency::<T>()resolvesTbefore every routed request of the app and its mounts; its error short-circuits (for example 401). 404s and the generated docs endpoints are not covered. The value stays cached for handlers. Global dependencies are not yet reflected in OpenAPI. - Mounts: a child sees its parent’s registrations; the child’s own take precedence inside the child.
- Per-route dependencies are not a separate feature: add a
Depends<T>argument (an unused_auth: Depends<Auth>is enough).
TestClient::builder exposes the same override helpers. See
Testing.
Teardown
Section titled “Teardown”Drop cannot be async, so register cleanup with
ctx.on_teardown(async move { .. }) (output () or Result<(), E: Display>).
Teardowns run after the response is produced, in LIFO order, on a
spawned task. Failures and panics are logged and do not stop the remaining
teardowns.
Teardown does not wait for a streaming body to finish, and is skipped (with a warning) if no Tokio runtime is available.