Skip to content

Desiders/froodi

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

308 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

froodi

Crates.io Docs.rs License Telegram

Froodi is a lightweight, ergonomic Inversion of Control (IoC) container for Rust that helps manage dependencies with clear scoping and lifecycle management in a simple manner.

📚 Documentation

Purpose

froodi is for applications where object wiring has become repetitive enough that you want a container, but you still want lifetimes to stay explicit.

It focuses on a small set of DI problems:

  • how to register factories
  • how to express object lifetime using scopes
  • how to reuse scoped dependencies safely
  • how to create transient values when you need a fresh one
  • how to clean up resolved dependencies with finalizers
  • how to plug the container into framework request handling

Key features

  • Scopes. Built-in scopes let dependencies live for the whole application, a request, or even shorter units.
  • Thread safety. Thread safety is enabled by default. You can disable it to use Rc-based internals instead of Arc and remove Send / Sync requirements.
  • Finalizers. Dependencies can register cleanup logic that runs when a scope is closed.
  • Sync and async support. The crate supports both sync and async factories and containers.
  • Modular registries. Registries can be split and extended instead of building one large registration block.
  • Auto-registration. froodi-auto can collect providers declared with macros.
  • Framework integrations. axum, dptree, and telers are supported out of the box.

Quickstart

  1. Install the crate.
[dependencies]
froodi = "1.0.0-beta.18"
  1. Define your types.

In this example:

  • Config lives for the whole application
  • GreetingService is created per request
  • WelcomeHandler depends on the greeter and is resolved as a transient value
use std::sync::Arc;

trait Greeter: Send + Sync {
    fn greet(&self, name: &str) -> String;
}

#[derive(Clone)]
struct Config {
    greeting: String,
}

struct GreetingService {
    greeting: String,
}

impl Greeter for GreetingService {
    fn greet(&self, name: &str) -> String {
        format!("{}, {name}!", self.greeting)
    }
}

struct WelcomeHandler {
    greeter: Arc<Box<dyn Greeter>>,
}

impl WelcomeHandler {
    fn handle(&self, name: &str) {
        println!("{}", self.greeter.greet(name));
    }
}
  1. Register factories in a registry.
use froodi::{
    Container,
    DefaultScope::{App, Request},
    Inject, boxed, instance, registry,
};

fn build_container(cfg: Config) -> Container {
    Container::new(registry! {
        provide(App, instance(cfg)),
        scope(Request) [
            provide(|Inject(cfg): Inject<Config>| {
                Ok(boxed!(GreetingService { greeting: cfg.greeting.clone() }; Greeter))
            }),
            provide(|Inject(greeter)| {
                Ok(WelcomeHandler { greeter })
            }),
        ],
    })
}
  1. Create a container and enter the next scope.

Container::new(...) starts at the first non-optional default scope, which is usually App. enter_build() moves to the next non-optional child scope, which is usually Request.

let app_container = build_container(Config {
    greeting: "Hello".to_owned(),
});

let request_container = app_container.clone().enter_build().unwrap();
  1. Resolve dependencies.

Use get::<T>() for scoped shared dependencies and get_transient::<T>() for fresh values.

let handler = request_container.get_transient::<WelcomeHandler>().unwrap();
handler.handle("froodi");

let config = request_container.get::<Config>().unwrap();
assert_eq!(config.greeting, "Hello");
  1. Close containers when done.
request_container.close();
app_container.close();
Full example
use froodi::{
    Container,
    DefaultScope::{App, Request},
    Inject, boxed, instance, registry,
};
use std::sync::Arc;

trait Greeter: Send + Sync {
    fn greet(&self, name: &str) -> String;
}

#[derive(Clone)]
struct Config {
    greeting: String,
}

struct GreetingService {
    greeting: String,
}

impl Greeter for GreetingService {
    fn greet(&self, name: &str) -> String {
        format!("{}, {name}!", self.greeting)
    }
}

struct WelcomeHandler {
    greeter: Arc<Box<dyn Greeter>>,
}

impl WelcomeHandler {
    fn handle(&self, name: &str) {
        println!("{}", self.greeter.greet(name));
    }
}

fn build_container(cfg: Config) -> Container {
    Container::new(registry! {
        provide(App, instance(cfg)),
        scope(Request) [
            provide(|Inject(cfg): Inject<Config>| {
                Ok(boxed!(GreetingService { greeting: cfg.greeting.clone() }; Greeter))
            }),
            provide(|Inject(greeter)| {
                Ok(WelcomeHandler { greeter })
            }),
        ],
    })
}

fn main() {
    let app_container = build_container(Config {
        greeting: "Hello".to_owned(),
    });

    let request_container = app_container.clone().enter_build().unwrap();

    let handler = request_container.get_transient::<WelcomeHandler>().unwrap();
    handler.handle("froodi");

    let config = request_container.get::<Config>().unwrap();
    assert_eq!(config.greeting, "Hello");

    request_container.close();
    app_container.close();
}
  1. (Optional) Add async support or framework integration.

Concepts

Dependency

A dependency is simply a value constructed by the container. Factories can depend on other values, and froodi resolves those dependencies recursively.

Scope

A scope describes how long a dependency lives.

The built-in default scope chain is:

Runtime -> App -> Session -> Request -> Action -> Step

Runtime and Session are optional by default. That means:

  • Container::new(...) usually starts from App
  • container.enter_build() usually goes from App to Request

If you want one of the optional scopes explicitly:

use froodi::{Container, DefaultScope::{Request, Runtime, Session}, registry};

let runtime_container = Container::new_with_start_scope(registry! {
    scope(Runtime) [
        provide(|| Ok(())),
    ],
    scope(Session) [
        provide(|| Ok(((), ()))),
    ],
    scope(Request) [
        provide(|| Ok(((), (), ()))),
    ],
}, Runtime);

let session_container = runtime_container.clone().enter().with_scope(Session).build().unwrap();

Container

The container holds resolved scoped dependencies and is used to access them.

  • get::<T>() returns a scoped shared dependency
  • get_transient::<T>() creates a fresh value
  • enter_build() creates the next child scope
  • close() runs finalizers for resolved dependencies in that scope

If a child container was created by skipping optional parent scopes, closing the child also closes those skipped parents. For example, a request container created from an app container also closes the skipped Session scope.

Registry

The registry defines how dependencies are constructed.

The main registration forms are:

  • provide(scope, factory)
  • scope(ScopeName) [ provide(factory), ... ]
  • extend(other_registry)
  • instance(value) for values created outside the container

Finalizer

A finalizer is cleanup logic attached to a registered dependency. It is executed when the owning scope is closed.

use froodi::{
    Container,
    DefaultScope::App,
    instance, registry,
};

#[derive(Clone)]
struct AppState;

let container = Container::new_with_start_scope(
    registry! {
        provide(App, instance(AppState), finalizer = |_dep| println!("AppState finalized")),
    },
    App,
);

let _state = container.get::<AppState>().unwrap();
container.close();

Trait objects

Use boxed! when the provided type should be exposed as a trait object.

use froodi::{Inject, boxed, registry};
use froodi::DefaultScope::Request;

trait Greeter {
    fn greet(&self) -> &'static str;
}

struct GreetingService;

impl Greeter for GreetingService {
    fn greet(&self) -> &'static str {
        "hello"
    }
}

struct Handler {
    greeter: std::sync::Arc<Box<dyn Greeter>>,
}

let registry = registry! {
    scope(Request) [
        provide(|| Ok(boxed!(GreetingService; Greeter))),
        provide(|Inject(greeter)| Ok(Handler { greeter })),
    ],
};

Features

Common feature combinations:

[dependencies]
froodi = { version = "1.0.0-beta.18", features = ["async", "axum"] } # choose the flags you need
froodi-auto = { version = "1", features = ["async"] }

Important feature flags:

  • thread_safe (enabled by default)
  • async
  • axum
  • http2-axum
  • dptree
  • telers

Disable default features if you want to turn off thread_safe.

Examples

Browse the full examples directory.

Community

Contributing

Contributions are welcome.

License

Apache License, Version 2.0

About

An ergonomic Rust IoC container

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Contributors