shaku

repository·master·Indexed 20 days ago

https://github.com/azuremarker/shaku

A compile-time dependency injection library for Rust (version 0.6.3). It provides abstractions for managing service lifecycles via Components (singletons) and Providers (transients). Shaku includes dedicated integration crates for popular web frameworks, including shaku_rocket for Rocket, shaku_axum for Axum, and shaku_actix for Actix Web, allowing dependencies to be injected directly into request handlers using extractors like Inject and InjectProvided.

Tokens
10.9K
Snippets
30
Records
42
Agent score
67%

What's inside shaku

  1. Integrate Shaku with Actix Web

    master
    The shaku_actix crate provides integration between the shaku dependency injection framework and the Actix Web web framework. This allows you to use Shaku's dependency injection capabilities within your Actix Web application handlers and services.
  2. What are Components and Providers in Shaku

    master

    Shaku uses two primary abstractions for dependency injection:

    • Component: Represents a single instance of a service (a singleton). Every time you resolve a component from the module, you receive the same instance.
    • Provider: Acts as a factory for instances (transient). Every time you resolve a provider from the module, you receive a new instance.

    Use Component when you want shared state or a single lifecycle, and Provider when you need fresh instances for every request.

  3. Integrate Shaku with web frameworks

    master

    Shaku provides dedicated integration crates for popular Rust web frameworks to facilitate dependency injection within request handlers:

    • Rocket: Use the shaku_rocket crate.
    • Axum: Use the shaku_axum crate.
    • Actix: Use the shaku_actix crate.
  4. Implement dependency injection with Shaku

    master

    To use Shaku, you define interfaces using traits that inherit from shaku::Interface, implement them in structs using the #[derive(Component)] macro, and register them in a module using the module! macro.

    For components that require runtime parameters (like configuration strings or numbers), you can use the .with_component_parameters::<T>(...) method on the module builder during initialization. Dependencies within components are injected using the #[shaku(inject)] attribute on fields.

    use shaku::{module, Component, Interface, HasComponent};
    use std::sync::Arc;
    
    trait Logger: Interface {
        fn log(&self, content: &str);
    }
    
    trait DateLogger: Interface {
        fn log_date(&self);
    }
    
    #[derive(Component)]
    #[shaku(interface = Logger)]
    struct LoggerImpl;
    
    impl Logger for LoggerImpl {
        fn log(&self, content: &str) {
            println!("{}", content);
        }
    }
    
    #[derive(Component)]
    #[shaku(interface = DateLogger)]
    struct DateLoggerImpl {
        #[shaku(inject)]
        logger: Arc<dyn Logger>,
        today: String,
        year: usize,
    }
    
    impl DateLogger for DateLoggerImpl {
        fn log_date(&self) {
            self.logger.log(&format!("Today is {}, {}", self.today, self.year));
        }
    }
    
    module! {
        MyModule {
            components = [LoggerImpl, DateLoggerImpl],
            providers = []
        }
    }
    
    fn main() {
        let module = MyModule::builder()
            .with_component_parameters::<DateLoggerImpl>(DateLoggerImplParameters {
                today: "Jan 26".to_string(),
                year: 2020
            })
            .build();
    
        let date_logger: &dyn DateLogger = module.resolve_ref();
        date_logger.log_date();
    }
  5. What is a Provider and when to use one

    master

    In Shaku, a Provider is used to represent a temporary service. Unlike a Component (which represents a long-lived service), a Provider is intended for services that are created on demand and may be short-lived, such as a connection to a remote service or a pooled database connection.

    Key characteristics of Providers:

    • They implement a specific interface via the Interface associated type.
    • They can have other Providers as dependencies.
    • Because they can depend on other providers, any service that consumes a provider must itself be a Provider (e.g., a DB repository using a DB connection, or a service using that repository).
    • Each call to resolve a provider creates a new instance of the service.
    pub trait Provider<M: Module>: 'static {
        type Interface: ?Sized;
        fn provide(module: &M) -> Result<Box<Self::Interface>, Box<dyn Error>>;
    }
  6. What is a Module in Shaku?

    master

    A Module represents a logical group of services. It acts as a container for components and can also manage submodules. By implementing traits like HasComponent on a module, service dependencies are verified at compile time. At runtime, the module holds the actual component instances.

    Modules are typically instantiated using the module! macro, which handles the boilerplate of implementing the Module trait and the necessary HasComponent traits for the services it provides.

    use shaku::{module, Component, Interface};
    
    trait MyComponent: Interface {}
    
    #[derive(Component)]
    #[shaku(interface = MyComponent)]
    struct MyComponentImpl;
    impl MyComponent for MyComponentImpl {}
    
    // MyModule implements Module and HasComponent<dyn MyComponent>
    module! {
        MyModule {
            components = [MyComponentImpl],
            providers = []
        }
    }
  7. Inject shaku providers into Rocket handlers using InjectProvided

    master

    To use services managed by a shaku Module within a Rocket request handler, use the InjectProvided<M, I> request guard.

    Requirements

    1. Module Storage: The shaku Module must be stored in Rocket's managed state using .manage(). It is recommended to wrap the module in a Box (e.g., Box::new(module)) to allow for dynamic module implementations if needed.
    2. Generic Parameters:
      • M: The shaku Module type (must implement ModuleInterface and HasProvider<I>).
      • I: The interface/trait of the service you want to inject (e.g., dyn MyTrait).

    Behavior

    • If the module is found in Rocket's state and the provider is successfully resolved, the guard succeeds.
    • If the module is missing from the state or the provider cannot be provided, the request will fail with a 500 Internal Server Error and the error message from shaku.
    #[macro_use] extern crate rocket;
    
    use shaku::{module, Provider};
    use shaku_rocket::InjectProvided;
    
    trait HelloWorld {
        fn greet(&self) -> String;
    }
    
    #[derive(Provider)]
    #[shaku(interface = HelloWorld)]
    struct HelloWorldImpl;
    
    impl HelloWorld for HelloWorldImpl {
        fn greet(&self) -> String {
            "Hello, world!".to_owned()
        }
    }
    
    module! {
        HelloModule {
            components = [],
            providers = [HelloWorldImpl]
        }
    }
    
    #[get("/")]
    fn hello(hello_world: InjectProvided<HelloModule, dyn HelloWorld>) -> String {
        hello_world.greet()
    }
    
    #[rocket::launch]
    fn rocket() -> _ {
        let module = HelloModule::builder().build();
    
        rocket::build()
            .manage(Box::new(module))
            .mount("/", routes![hello])
    }
  8. Inject components into Rocket handlers using `Inject`

    master

    To use shaku components within Rocket request handlers, use the Inject request guard.

    Requirements

    1. Module Storage: The shaku module must be stored in Rocket's managed state as a Box<M>, where M is your module type.
    2. Type Signature: The Inject guard requires two generic parameters:
      • M: The module type (must implement ModuleInterface and HasComponent<I>).
      • I: The component interface (must implement Interface).

    Usage Pattern

    Define your component and module using shaku macros, then include Inject<Module, dyn Interface> in your Rocket handler arguments. Because Inject implements Deref, you can call the interface methods directly on the guard.

    #[macro_use] extern crate rocket;
    
    use shaku::{module, Component, Interface};
    use shaku_rocket::Inject;
    
    trait HelloWorld: Interface {
        fn greet(&self) -> String;
    }
    
    #[derive(Component)]
    #[shaku(interface = HelloWorld)]
    struct HelloWorldImpl;
    
    impl HelloWorld for HelloWorldImpl {
        fn greet(&self) -> String {
            "Hello, world!".to_owned()
        }
    }
    
    module! {
        HelloModule {
            components = [HelloWorldImpl],
            providers = []
        }
    }
    
    // The handler uses Inject as a request guard
    #[get("/")]
    fn hello(hello_world: Inject<HelloModule, dyn HelloWorld>) -> String {
        hello_world.greet()
    }
    
    #[rocket::launch]
    fn rocket() -> _ {
        let module = HelloModule::builder().build();
    
        rocket::build()
            .manage(Box::new(module)) // Module must be managed as Box<M>
            .mount("/", routes![hello])
    }
  9. Inject components into Actix handlers using `Inject<M, I>`

    master

    To use shaku components within Actix-web handlers, use the Inject<M, I> extractor.

    Requirements

    1. Module Storage: The shaku module (M) must be stored in Actix's app data, wrapped in an Arc.
    2. Generic Parameters:
      • M: The module type that implements ModuleInterface and HasComponent<I>.
      • I: The component interface type (e.g., dyn MyInterface).

    Usage Pattern

    Define your component and module as usual, then include Inject<YourModule, dyn YourInterface> as an argument in your Actix handler function. The Inject struct implements Deref, so you can call interface methods directly on the extractor instance.

    Example

    use actix_web::{App, HttpServer, web};
    use shaku::{module, Component, Interface};
    use shaku_actix::Inject;
    use std::sync::Arc;
    
    trait HelloWorld: Interface {
        fn greet(&self) -> String;
    }
    
    #[derive(Component)]
    #[shaku(interface = HelloWorld)]
    struct HelloWorldImpl;
    
    impl HelloWorld for HelloWorldImpl {
        fn greet(&self) -> String {
            "Hello, world!".to_owned()
        }
    }
    
    module! {
        HelloModule {
            components = [HelloWorldImpl],
            providers = []
        }
    }
    
    // The handler uses Inject to retrieve the component
    async fn hello(hello_world: Inject<HelloModule, dyn HelloWorld>) -> String {
        hello_world.greet()
    }
    
    #[actix_web::main]
    async fn main() -> std::io::Result<()> {
        // The module must be wrapped in Arc and added to app_data
        let module = Arc::new(HelloModule::builder().build());
    
        HttpServer::new(move || {
            App::new()
                .app_data(module.clone())
                .route("/", web::get().to(hello))
        })
        .bind("127.0.0.1:8080")?
        .run()
        .await
    }
    async fn hello(hello_world: Inject<HelloModule, dyn HelloWorld>) -> String {
        hello_world.greet()
    }
  10. Configure Shaku crate features

    master

    Shaku's behavior is controlled via crate features. By default, it is thread-safe and includes procedural macros. You can opt-out of these features in your Cargo.toml if you need to reduce constraints or binary size:

    • thread_safe: If disabled, components are not required to be Send + Sync.
    • derive: If disabled, the Component and Provider proc-macro derives and the module! macro will be unavailable.
    [dependencies]
    shaku = { version = "0.6.3", default-features = false, features = ["derive", "thread_safe"] }