stano-di 0.2.0

Lightweight dependency injection container with lazy singleton resolution, cycle detection, and async-safe validation
Documentation

stano-di

Lightweight dependency injection container with lazy singleton resolution, cycle detection, and environment variable loading. Pairs with stano-di-macros for automatic boilerplate generation.

Install

[dependencies]
stano-di = { path = "../stano-di" }

API

Container

  • Container — service locator registry (TypeId-keyed internal storage).
    • new() -> Self — create an empty container.
    • register<T: Send+Sync+'static>(&mut self, factory: fn(&Container) -> Arc<T>) — register a concrete type with its factory. Lazily resolves on first access via OnceLock.
    • register_trait<T: DynComponent + ?Sized>(&mut self, factory: fn(&Container) -> Arc<T>) where Arc<T>: Send+Sync — register a trait object (e.g., Arc<dyn MyTrait>).
    • register_instance<T: Send+Sync+'static>(&mut self, instance: Arc<T>) — register a pre-built instance (no factory needed).
    • register_component<T: Component>(&mut self) — register a type that implements the Component trait (generated by #[service]).
    • get<T: Send+Sync+Clone+'static>(&self) -> Arc<T> — retrieve a registered type; panics if not found.
    • try_get<T: Send+Sync+Clone+'static>(&self) -> Result<Arc<T>, ContainerError> — safe retrieval.
    • get_trait<T: DynComponent + ?Sized>(&self) -> Arc<T> where Arc<T>: Clone — retrieve a trait object; panics if not found.
    • try_get_trait<T: DynComponent + ?Sized>(&self) -> Result<Arc<T>, ContainerError> where Arc<T>: Clone — safe trait retrieval.
    • has<T: 'static>(&self) -> bool — check if a concrete type is registered.
    • has_trait<T: DynComponent + ?Sized>(&self) -> bool — check if a trait is registered.
    • validate(&self) -> Result<(), Vec<ContainerError>> — eagerly resolve all singletons and detect cyclic dependencies. Call after all registrations and before serving traffic.
    • dependency_graph(&self) -> DependencyGraph — returns a displayable graph of registered types and their dependencies.

Application Context

  • ApplicationContext — facade over Container that also carries an Arc<dyn Environment>.
    • Same registration and retrieval methods as Container.
    • environment(&self) -> &Arc<dyn Environment> — access the environment.
    • register_all(&mut self) — auto-register all #[service]-annotated structs discovered via the inventory crate.
    • container_mut(&mut self) -> &mut Container — direct access to the inner container if needed.

Environment

  • Environment trait — loads config values by key.

    • fn get(&self, key: &str) -> Option<String> — retrieve an environment variable or config value.
  • OsEnvironment — loads from actual OS environment variables (and .env files via dotenvy).

    • fn new() -> Self — creates and auto-loads .env if present.

Error Type

  • ContainerError:
    • NotRegistered(&'static str) — type was not registered.
    • DowncastFailed(&'static str) — failed to downcast a trait object.
    • FactoryPanic(&'static str) — factory function panicked during validation.
    • CyclicDependency(Vec<&'static str>) — cyclic dependency detected (e.g., A → B → A).

Traits (Generated by Macros)

  • Component — contract for types that can be auto-registered. Generated by #[service].

    • fn component_type_name() -> &'static str — name of this type.
    • fn dependency_ids() -> Vec<TypeId> — IDs of types this type depends on.
    • fn build(container: &Container) -> Arc<Self> — construct an instance.
    • fn register(container: &mut Container) — register the factory.
  • Injectable — trait for types that can be retrieved from container. Generated by #[component] on traits.

    • fn get_from(container: &Container) -> Arc<Self> — retrieve from container.
  • DynComponent — marker trait for trait objects. Generated by #[component].

Usage Example

use stano_di::{Container, ApplicationContext};
use std::sync::Arc;

// Define a trait (mark with #[component]).
#[component]
pub trait MyService: Send + Sync {
    fn do_work(&self) -> String;
}

// Implement it (mark with #[service(dyn MyService)]).
pub struct MyServiceImpl;
impl MyService for MyServiceImpl {
    fn do_work(&self) -> String { "work done".into() }
}

// Manual registration (without the #[service] macro, for this example).
// register_trait requires the trait to implement DynComponent (via #[component]).
let mut container = Container::new();
container.register_trait::<dyn MyService>(|_| Arc::new(MyServiceImpl));

// Or via ApplicationContext with environment:
use stano_di::OsEnvironment;
let env = Arc::new(OsEnvironment::new());
let mut ctx = ApplicationContext::new(env);
ctx.register_all(); // auto-register all #[service] structs

// Validate (detects cycles, ensures all factories work).
ctx.validate()?;

// Retrieve.
let svc: Arc<dyn MyService> = ctx.get_trait();
println!("{}", svc.do_work()); // "work done"

Notes

  • Lazy resolution — singletons are resolved on first access, not at registration time.
  • Cycle detectionvalidate() detects circular dependencies (A → B → C → A) before they cause infinite loops at runtime.
  • #[service] and #[component] generation — see stano-di-macros for the macro syntax. These generate Component trait impls and auto-register via inventory::submit!.
  • Thread-safe — all types must be Send + Sync for use in async contexts; factory functions run synchronously and must not block.
  • No feature flags — all APIs available.