Zero-cost compile-time dependency wiring for Rust. One trait, one derive, one
way to express a dependency. No container, no TypeId, no dynamic dispatch — the
derive expands to exactly the constructor you would hand-write.
pub trait Wire<Ctx> {
fn wire(ctx: &Ctx) -> Self;
}A service is a struct. Its dependencies are its fields. #[derive(Wire)]
generates the impl that wires each field from a context.
use dowel::Wire;
// The composition root owns one concrete context.
struct AppCtx { db: Db }
// A leaf is a cheap, clonable handle taught to the context by hand.
#[derive(Clone)]
struct Db { url: &'static str }
impl Wire<AppCtx> for Db {
fn wire(ctx: &AppCtx) -> Self { ctx.db.clone() }
}
// Services derive their wiring: every field is itself `Wire<Ctx>`.
#[derive(Wire)]
struct PlayerRepo { db: Db }
#[derive(Wire)]
struct PlayerService { repo: PlayerRepo }
let ctx = AppCtx { db: Db { url: "pg://" } };
let svc = PlayerService::wire(&ctx);#[wire(skip)]— construct the field withDefault::default(); adds no bound.#[wire(default = expr)]— construct it withexpr; adds no bound. For a leaf with noDefaultbut a known init, e.g.#[wire(default = Cache::with_capacity(128))].#[wire(with = path)]— construct it withpath(ctx); adds no bound. Keep the service generic overCtx, so the provider is generic too (fn make<C>(ctx: &C) -> Field); any bound it needs (e.g.Seed: Wire<C>) must come from the struct's own wired fields.
Every plain field type F gets a where F: Wire<Ctx> bound, so a forgotten leaf
impl is a compile error at the wiring site:
error[E0599]: the function or associated item `wire` exists for struct `PlayerRepo`,
but its trait bounds were not satisfied
= note: trait bound `Db: Wire<AppCtx>` was not satisfied
That is the intended repair signal — add the leaf impl, don't paper over it.
Writing one impl Wire<AppCtx> per leaf by hand gets repetitive. #[derive(Context)]
generates them — one impl Wire<AppCtx> for FieldType per field, cloning the field
out of the context:
use dowel::{Wire, Context};
#[derive(Clone)]
struct Db { url: &'static str }
#[derive(Clone, Copy)]
struct Clock;
#[derive(Context)]
struct AppCtx { db: Db, clock: Clock }
#[derive(Wire)]
struct PlayerRepo { db: Db, clock: Clock }
let ctx = AppCtx { db: Db { url: "pg://" }, clock: Clock };
let repo = PlayerRepo::wire(&ctx);#[context(skip)]omits a field (config primitives, or to dodge a duplicate type).- Two non-skipped fields of the same type are a compile error — they would produce
conflicting
Wireimpls; annotate one with#[context(skip)]and wire it by hand.
[T; N] is Wire<Ctx> whenever T is: it wires N independent instances from the
same context via a monomorphized core::array::from_fn — no container, no
allocation. The homogeneous companion to a tuple struct of distinct fields.
use dowel::Wire;
struct Ctx { seed: u32 }
struct Worker { id: u32 }
impl Wire<Ctx> for Worker {
fn wire(ctx: &Ctx) -> Self { Worker { id: ctx.seed } }
}
let pool: [Worker; 3] = Wire::wire(&Ctx { seed: 7 });The graph does not deduplicate (rule 5): each element is wired independently, so this is N distinct instances. For one shared instance N times, the sharing lives in the leaf.
examples/axum.rs shows a Wired<S> extractor that calls S::wire(&ctx) from
the axum State (the composition root), letting a handler declare exactly the
slice of the graph it needs:
async fn get_player(Wired(repo): Wired<PlayerRepo>, Path(id): Path<u64>) -> impl IntoResponse {
repo.find(id)
}- A dependency is a struct field of a concrete type — never
Arc<dyn Trait>. - Construction belongs to
#[derive(Wire)]; don't hand-write a re-wiringnew(). - Services stay generic over
Ctx; the final binary picks the concrete context. - Leaves are cheap, clonable handles (
Arc-backed orCopy). - Singletons live in the leaf, not the graph — the graph does not deduplicate.
Licensed under either of MIT or Apache-2.0 at your option.