#di #ioc #dependency-injection

steloc

Simple, compile-time DI framework for Rust

1 unstable release

new 0.2.0 Dec 10, 2024

#955 in Rust patterns

Download history 142/week @ 2024-12-09

142 downloads per month

MIT/Apache

49KB
675 lines

steloc

Teloc is simple, compile-time DI framework for Rust inspired by C# Dependency Injection Framework.

What is DI?

Link to Wikipedia

Dependency injection (DI) is a technique in which an object receives other objects that it depends on. These other objects are called dependencies. In the typical "using" relationship the receiving object is called a client and the passed (that is, "injected") object is called a service. The code that passes the service to the client can be many kinds of things and is called the injector. Instead of the client specifying which service it will use, the injector tells the client what service to use. The "injection" refers to the passing of a dependency (a service) into the object (a client) that would use it.

Highlights

  • Compile-time - steloc uses the powerful rust type system check for the existence of dependencies that have the proper lifetime at compile-time. This means you cannot compile your code if a required dependency has not been registered or if it's lifetime is shorter to what's requested. If your code compiles, that means it runs!
  • Zero-overhead - steloc uses only zero-overhead abstractions such as traits, generics, newtypes and unit types, and compile-time resolving of dependencies, so you don't worry about overhead at runtime.
  • Simple API - steloc provides you a simple API with only one struct and one attribute macro needed for working with library.
  • Integration with existing enviroment - steloc can be used with any existing frameworks like actix-web, warp, rocket. Now there is only support for actix-web as you can see in the example.

How to use

There are one type can be provider of services: ServiceProvider. It used as store for dependencies with Instance and Singleton lifetimes, and for declaring all dependencies using .add_*() methods. It can be forked to create a local scope with local instances.

There are four lifetimes for dependencies:

  1. Transient. Service will be created when resolves. Can depend on dependencies with anything lifetime.
  2. Singleton. Service will be created once at ServiceProvider when it resolved (lazy). Can depend on dependencies with anything lifetime. Cannot depend on services from forked ServiceProvider instances.
  3. Instance. Dependency was created outside of ServiceProvider and can be used by any other dependency.

How to work:

  1. Declare your structs.
  2. Create constructors and add #[inject] macro on its.
  3. Create a ServiceProvider object.
  4. Add your services and dependencies using ServiceProvider::add_* methods.
  5. Fork ServiceProvider if you need to create local scope.
  6. Get service from provider using .resolve() method.
  7. Work with service.

Example:

use steloc::*;

// Declare your structs
struct ConstService {
    number: i32,
}
// #[inject] macro is indicate that dependency can be constructed using this function
#[inject]
impl ConstService {
    pub fn new(number: i32) -> Self {
        ConstService { number }
    }
}

// Derive macro can be used when all fields implement `Dependency` trait, but 
// we recommend using the #[inject] macro it in production code instead.
#[derive(Dependency)]
struct Controller {
    number_service: ConstService,
}

fn main() {
    // Create `ServiceProvider` struct that store itself all dependencies
    let sp = ServiceProvider::new()
        // Add dependency with `Singleton` lifetime. More about lifetimes see above.
        .add_singleton::<ConstService>()
        // Add dependency with `Transient` lifetime. More about lifetimes see above.
        .add_transient::<Controller>();
    // Fork `ServiceProvider`. It creates a new `ServiceProvider` which will have
    // access to the dependencies from parent `ServiceProvider`.
    let scope = sp
        // .fork() method creates a local mutable scope with self parent immutable `ServiceProvider`.
        .fork()
        // Add an instance of `i32` that will be used when `ConstService` will be initialized.
        .add_instance(10);
    // Get dependency from `ServiceProvider`
    let controller: Controller = scope.resolve();
    assert_eq!(controller.number_service.number, 10);
}

For documentation see page on docs.rs.

For more examples see examples folder or tests folder.

Comparison with other DI frameworks

Feature steloc shaku waiter_di
Compile-time checks Yes Yes Yes
Can be used without dyn traits Yes Yes Yes
Many service providers in one app Yes Yes No
Different lifetimes Yes Yes No

How to read errors

Sometimes steloc can give strange large errors. But no panic! You can define your problem by read the manual of reading errors.

Pro tips

This section contains pro tips that you might want to use when working with the library.

Get type of instance of ServiceProvider

It will be useful when you want to store an instance of ServiceProvider in a struct or return from a function or pass as an argument.

What you need:

  1. Paste following code after ServiceProvider initialization: let () = service_provider;.
  2. Compiler will give you very big terrible type starting with steloc::ServiceProvider<...>.
  3. Copy that type into type alias, for example type ConcreteSP = /*compiler output*/;.
  4. Use ConcreteSP when you want write ServiceProvider instance type.
  5. If you change ServiceProvider initialization repeat steps 1-4.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Dependencies

~2–7.5MB
~149K SLoC