#di #ioc #dependency-injection #teloc

teloc

Simple, compile-time DI framework for Rust

2 unstable releases

new 0.2.0 Nov 25, 2021
0.1.0 Jan 5, 2021

#160 in Rust patterns

24 downloads per month

MIT/Apache

55KB
1K SLoC

teloc

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 - teloc uses powerful rust type system for lifetime and existing of dependencies checking in compile-time. That means that you cannot build your code if you do not register dependencies for all of registered dependencies or lifetime of dependency does not correspondence to requester. If your code is compile, that mean it run!
  • Zero-overhead - teloc 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 in runtime.
  • Simple API - teloc provides you a simple API with only one struct and one attribute macro needed for working with library.
  • Integration with existing enviroment - teloc 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 teloc::*;

// 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 teloc 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 teloc 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 teloc::ServiceProvider<...>.
  3. Copy that type into type alias, for example type ConcreteSP = /*copiler 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

~1–5MB
~114K SLoC