sadi 1.1.1-beta.1

Semi-Automatic Dependency Injector
Documentation

SaDi - Semi-automatic Dependency Injector

Crates.io Documentation License Build Status

A lightweight, type-safe dependency injection container for Rust applications. SaDi provides ergonomic service registration (including trait-object bindings), transient and singleton lifetimes, semi-automatic dependency resolution, and circular dependency detection.

โœจ Features

  • ๐Ÿ”’ Type-Safe: Leverages Rust's type system for compile-time safety
  • ๐Ÿ”„ Transient Services: Create new instances on each request
  • ๐Ÿ”— Singleton Services: Shared instances with reference counting via Arc / Rc
  • ๐Ÿ” Circular Detection: Prevents infinite loops in dependency graphs
  • โŒ Error Handling: Comprehensive error types with detailed messages
  • ๐Ÿ“Š Optional Logging: Tracing integration with feature gates
  • ๐Ÿš€ Zero-Cost Abstractions: Feature gates enable compile-time optimization
  • ๐Ÿงต Thread-Safe by Default: Uses Arc + RwLock for concurrent access
  • ๐Ÿ“ฆ Module System: Organize services into reusable modules
  • ๐Ÿ—๏ธ Enterprise Ready: Supports layered architecture, repository pattern, and use cases

๐Ÿ“ฆ Installation

Add this to your Cargo.toml:

[dependencies]
sadi = { path = "../sadi" }  # For local development

Or from crates.io (when published):

[dependencies]
sadi = "1.1.0"

๐Ÿš€ Quick Start

use sadi::{Injector, Provider, Shared, Module, Application};

// Define your services
struct DatabaseService {
    connection_string: String,
}

impl DatabaseService {
    fn new() -> Self {
        Self {
            connection_string: "postgresql://localhost:5432/myapp".to_string(),
        }
    }

    fn query(&self, sql: &str) -> String {
        format!("Executing '{}' on {}", sql, self.connection_string)
    }
}

struct UserService {
    db: Shared<DatabaseService>,
}

impl UserService {
    fn new(db: Shared<DatabaseService>) -> Self {
        Self { db }
    }

    fn create_user(&self, name: &str) -> String {
        self.db.query(&format!("INSERT INTO users (name) VALUES ('{}')", name))
    }
}

struct RootModule;

impl Module for RootModule {
    fn providers(&self, injector: &sadi::Injector) {
         // Register DatabaseService as singleton
        injector.provide::<DatabaseService>(Provider::root(|_| {
            Shared::new(DatabaseService::new())
        }));
        
        // Register UserService with DatabaseService dependency
        injector.provide::<UserService>(Provider::root(|inj| {
            let db = inj.resolve::<DatabaseService>();
            UserService::new(db).into()
        }));
    }
}

fn main() {
    // Create an application and register services
    let mut app = Application::new(RootModule);

    app.bootstrap();

    // Resolve and use services
    match app.injector().try_resolve::<UserService>() {
        Ok(user_service) => println!("{}", user_service.create_user("Alice")),
        Err(e) => eprintln!("Service resolution failed: {}", e),
    }

    // or just
    app.injector().resolve::<UserService>(); // This panics if not registered
}

๏ฟฝ Examples

SaDi includes three comprehensive examples showcasing different use cases and patterns:

1. Basic Example

Location: examples/basic/

A simple introduction to SaDi fundamentals:

  • Service registration with Injector and Provider
  • Transient and singleton lifetimes
  • Basic dependency resolution with try_resolve()
  • Error handling with Result types

Run:

cargo run --example basic

2. Complex Example (Advanced Patterns)

Location: examples/complex/

Demonstrates enterprise-grade architecture with:

  • Domain Layer: Clear entity definitions and repository interfaces
  • Application Layer: Use case pattern for business logic
  • Infrastructure Layer: SQLite persistence with concrete implementations
  • Dependency Injection: Multi-level service composition
  • Module System: Modular DI configuration with imported modules

Architecture:

core/
  โ”œโ”€โ”€ domain/       (User, Todo entities & repository traits)
  โ””โ”€โ”€ application/  (CreateUserUseCase, GetAllTodoUseCase, etc.)
infra/
  โ”œโ”€โ”€ di/           (Modules & dependency registration)
  โ””โ”€โ”€ persistence/  (SQLite repositories)

Run:

cd examples/complex
cargo run

Run Tests:

cd examples/complex
./test.sh

3. Axum REST API Example

Location: examples/axum/

Real-world REST API integration with Axum web framework:

  • HTTP handler functions with DI-resolved dependencies
  • Structured JSON responses with error handling
  • CRUD endpoints for Users and Todos
  • Service state management via Axum's State extractor
  • Dependency resolution per-request

Features:

  • POST /users - Create user
  • GET /users - List all users
  • GET /users/{id} - Get user by ID
  • DELETE /users/{id} - Delete user
  • POST /todos - Create todo
  • GET /todos - List all todos
  • PUT /todos/{id}/status - Update todo status
  • DELETE /todos/{id} - Delete todo

Run:

# Terminal 1: Start server
cd examples/axum
cargo run

# Terminal 2: Run comprehensive test suite
cd examples/axum
./test.sh

The test suite includes:

  • Server health checks
  • Sequential dependency extraction between requests
  • HTTP status code validation
  • JSON response parsing and assertion

๏ฟฝ๐Ÿ“– Usage Guide

Service Registration

Transient Services

Create new instances on each request:

use sadi::{Injector, Provider, Shared};
use uuid::Uuid;

struct LoggerService {
    session_id: String,
}

let injector = Injector::new();

// Transient: new instance each time (default behavior)
injector.provide::<LoggerService>(Provider::transient(|_| {
    Shared::new(LoggerService { 
        session_id: Uuid::new_v4().to_string() 
    })
}));

let logger1 = injector.resolve::<LoggerService>();
let logger2 = injector.resolve::<LoggerService>();
// logger1 and logger2 are different instances

Singleton Services

Create once and share across all dependents:

use sadi::{Injector, Provider, Shared};

struct ConfigService {
    app_name: String,
    debug: bool,
}

let injector = Injector::new();

// Singleton: same instance every time
injector.provide::<ConfigService>(Provider::root(|_| {
    Shared::new(ConfigService { 
        app_name: "MyApp".to_string(), 
        debug: true 
    })
}));

let config1 = injector.resolve::<ConfigService>();
let config2 = injector.resolve::<ConfigService>();
// config1 and config2 point to the same instance

Error Handling

SaDi provides both panicking and non-panicking variants:

use sadi::{Injector, Provider, Shared, Error};

let injector = Injector::new();
injector.provide::<String>(Provider::new(|_| Shared::new("Hello".to_string())));

// Non-panicking (try_resolve returns Result)
match injector.try_resolve::<String>() {
    Ok(s) => println!("Got: {}", s),
    Err(e) => println!("Error: {}", e),
}

// Trying to resolve an unregistered type
match injector.try_resolve::<u32>() {
    Ok(_) => unreachable!(),
    Err(e) => println!("Expected error: {}", e),
}

Dependency Injection

Services can depend on other services. Use module-based registration for clean organization:

use sadi::{Injector, Module, Provider, Shared};

struct DatabaseService { /* ... */ }
impl DatabaseService { fn new() -> Self { DatabaseService {} } }

struct CacheService { /* ... */ }
impl CacheService { fn new() -> Self { CacheService {} } }

struct UserRepository {
    db: Shared<DatabaseService>,
    cache: Shared<CacheService>,
}

impl UserRepository {
    fn new(db: Shared<DatabaseService>, cache: Shared<CacheService>) -> Self {
        Self { db, cache }
    }
}

// Define a module for persistence services
struct PersistenceModule;

impl Module for PersistenceModule {
    fn providers(&self, injector: &Injector) {
        injector.provide::<DatabaseService>(Provider::root(|_| {
            Shared::new(DatabaseService::new())
        }));
        
        injector.provide::<CacheService>(Provider::root(|_| {
            Shared::new(CacheService::new())
        }));
        
        injector.provide::<UserRepository>(Provider::root(|inj| {
            let db = inj.resolve::<DatabaseService>();
            let cache = inj.resolve::<CacheService>();
            UserRepository::new(db, cache).into()
        }));
    }
}

let injector = Injector::new();
let module = PersistenceModule;
module.providers(&injector);

let repo = injector.resolve::<UserRepository>();

๐Ÿ” Advanced Features

Circular Dependency Detection

SaDi automatically detects and prevents circular dependencies by tracking resolution paths:

use sadi::{Injector, Provider, Shared};

// Example: attempting to create circular dependencies will fail
struct ServiceA {
    b: Shared<ServiceB>,
}

struct ServiceB {
    a: Shared<ServiceA>,
}

let injector = Injector::new();

// These registrations will create a circular dependency
// Attempting to resolve either service will result in an error
// Error: "Circular dependency detected in resolution path"

Tracing Integration

Enable the tracing feature for automatic logging (the crate's default feature includes tracing):

[dependencies]
sadi = { path = "../sadi", features = ["tracing"] }
use sadi::{Application, Module, Provider, Shared};
use tracing::info;

struct MyModule;

impl Module for MyModule {
    fn providers(&self, injector: &sadi::Injector) {
        injector.provide::<DatabaseService>(Provider::root(|_| {
            info!("Registering DatabaseService");
            Shared::new(DatabaseService::new())
        }));
    }
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();

    let mut app = Application::new(MyModule);
    app.bootstrap();
    
    // Resolving services will be traced when tracing feature is enabled
    let _db = app.injector().try_resolve::<DatabaseService>();
}

๐Ÿงช Testing

Unit Tests

Run the crate test suite:

# Run all tests for the workspace
cargo test

# Run tests for the sadi crate only
cargo test -p sadi

# Run with tracing feature
cargo test --features tracing

# Run documentation tests
cargo test --doc -p sadi

๐Ÿ“ Project Structure

sadi/
โ”œโ”€โ”€ sadi/                 # SaDi library crate
โ”‚   โ”œโ”€โ”€ src/              # core implementation (container, macros, types)
โ”‚   โ””โ”€โ”€ README.md         # This file
โ”œโ”€โ”€ examples/
โ”‚   โ”œโ”€โ”€ basic/            # Basic usage example with simple DI
โ”‚   โ”œโ”€โ”€ complex/          # Advanced DI patterns with SQLite, repositories, use cases
โ”‚   โ”‚   โ”œโ”€โ”€ src/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ core/     # Domain (entities, use cases)
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ infra/    # Infrastructure (persistence, DI configuration)
โ”‚   โ”‚   โ””โ”€โ”€ test.sh       # Test script for complex example
โ”‚   โ””โ”€โ”€ axum/             # REST API with Axum web framework
โ”‚       โ”œโ”€โ”€ src/
โ”‚       โ”‚   โ””โ”€โ”€ main.rs   # HTTP handlers with DI integration
โ”‚       โ””โ”€โ”€ test.sh       # Comprehensive API test suite
โ””โ”€โ”€ README.md

๐Ÿ”ง Configuration

Feature Flags

SaDi exposes a small set of feature flags. See sadi/Cargo.toml for the authoritative list, but the crate currently defines:

  • thread-safe (enabled by default) โ€” switches internal shared pointer and synchronization primitives to Arc + RwLock/Mutex for thread-safe containers.
  • tracing (enabled by default) โ€” integrates with the tracing crate to emit logs during registration/resolution.

The workspace default enables both thread-safe and tracing. To opt out of thread-safe behavior (use Rc instead of Arc), disable the thread-safe feature.

Environment Variables

When using the tracing feature, you can control logging levels:

# Set log level
RUST_LOG=debug cargo run --example basic

# Enable only SaDi logs
RUST_LOG=sadi=info cargo run --example basic

๐Ÿค Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

Development Setup

  1. Clone the repository:
git clone https://github.com/binary-sea/sadi.git
cd sadi
  1. Run tests:
cargo test --all-features
  1. Check formatting:
cargo fmt --check
  1. Run clippy:
cargo clippy -- -D warnings

๐Ÿ“‹ Roadmap & TODO

๐Ÿงต Thread Safety

  • Arc-based Container: Thread-safe version of SaDi using Arc instead of Rc (implemented behind the thread-safe feature)
  • Send + Sync Services: Support for Send + Sync services in thread-safe mode (enforced by API bounds)
  • Concurrent Access: Concurrent reads/writes supported via RwLock/Mutex in thread-safe mode
  • Lock-free Operations: Minimize contention in high-concurrency scenarios

๐Ÿ”ง Advanced Features

  • Lazy Initialization: Singleton instances are created on first provide (implemented in Factory)
  • Service Metrics: Internal container metrics for observability (resolution counts, timing)

๐Ÿ“ฆ Ecosystem Integration

  • Async Factory Support: Enable async/await in factory functions for Tokio/async-std runtimes
  • Actix-web Integration: Extension trait and extractors for Actix-web framework
  • Axum Integration: Demonstrated with REST API example and state management
    • Create a plugin to automatically resolve dependency
  • Rocket Integration: Layer and extractor support for Rocket web framework

๏ฟฝ๏ธ Architectural Patterns

  • Repository Pattern: Demonstrated in complex example with SQLite repositories
  • Layered Architecture: Clean separation of domain, application, and infrastructure layers
  • Use Case Pattern: Business logic encapsulated in use cases with DI
  • Web Framework Integration: Explored with Axum web framework

๏ฟฝ๐Ÿ› ๏ธ Developer Experience

  • Derive Macros: Auto-generate factory functions from service structs (#[injectable])
  • Error Suggestions: Better error messages with fix suggestions

๐Ÿ“Š Observability

  • OpenTelemetry: Built-in telemetry and distributed tracing
  • Prometheus Metrics: Expose container metrics for monitoring

๐ŸŽฏ Performance

  • Memory Optimization: Reduced memory footprint for large containers

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿ™ Acknowledgments

  • Inspired by dependency injection patterns from other languages and frameworks
  • Built with โค๏ธ using Rust's powerful type system
  • Thanks to the Rust community for excellent crates and documentation

SaDi - A semi-automatic dependency injection container for Rust
Repository: binary-sea/sadi
Made with โค๏ธ by the Binary Sea Team