Skip to content

Desiders/froodi

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

322 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

18 stars

Watchers

1 watching

Forks

Releases

No releases published

Contributors