Expand description
§dilib-rs
A dependency injection library for Rust.
§Usage
[dependencies]
dilib = "0.2.0"
§Example
§Basic Usage
use dilib::Container;
struct Printer;
impl Printer {
pub fn print(&self, s: &str) {
println!("{}", s);
}
}
struct EnglishGreeting;
impl EnglishGreeting {
pub fn greet(&self) -> String {
"Hello!".to_string()
}
}
struct SpanishGreeting;
impl SpanishGreeting {
pub fn greet(&self) -> String {
"Hola!".to_string()
}
}
let mut container = Container::new();
container.add_singleton(Printer).unwrap();
container.add_scoped(|| EnglishGreeting).unwrap();
container.add_scoped_with_name("es", || SpanishGreeting).unwrap();
let printer = container.get::<Printer>().unwrap();
let en = container.get::<EnglishGreeting>().unwrap();
let es = container.get_with_name::<SpanishGreeting>("es").unwrap();
printer.print(&en.greet());
printer.print(&es.greet());
§Table of Contents
§Container
The container is the main storage for the 2 types of provides:
Scoped
: creates a new instance each timeSingleton
: returns the same instance each time
All these providers can be named using the methods ended with with_name(...)
.
§Scoped provider
The scoped providers creates a new instance each time they are called.
use dilib::Container;
let mut container = Container::new();
container.add_scoped(|| String::from("Apple Pie")).unwrap();
let s = container.get::<String>().unwrap();
assert_eq!(s.as_ref(), "Apple Pie");
§Singleton provider
The singleton providers returns the same instance each time they are called.
use dilib::Container;
use std::sync::Mutex;
let mut container = Container::new();
container.add_singleton(Mutex::new(0)).unwrap();
{
let c1 = container.get::<Mutex<i32>>().unwrap();
*c1.lock().unwrap() = 3;
}
let c2 = container.get::<Mutex<i32>>().unwrap();
assert_eq!(*c2.lock().unwrap(), 3);
§Inject trait
The Inject
trait is a mechanism to create a type using the
providers of a container.
To add a type that implements Inject
to the container,
you use the add_deps
methods, this adds the type as a Scoped
provider.
use std::sync::{Mutex, atomic::AtomicUsize};
use dilib::{Container, Inject};
struct IdGenerator(AtomicUsize);
impl IdGenerator {
pub fn next(&self) -> usize {
1 + self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
}
}
#[derive(Clone, Debug)]
struct Fruit {
id: usize,
tag: String
}
impl Inject for Fruit {
fn inject(container: &Container) -> Self {
let generator = container.get::<IdGenerator>().unwrap();
let id = generator.next();
let tag = container.get_with_name::<String>("fruit").unwrap().cloned();
Fruit { id, tag }
}
}
let mut container = Container::new();
container.add_singleton(IdGenerator(AtomicUsize::new(0))).unwrap();
container.add_scoped_with_name("fruit", || String::from("b18ap31")).unwrap();
container.add_deps::<Fruit>().unwrap();
let f1 = container.get::<Fruit>().unwrap();
let f2 = container.get::<Fruit>().unwrap();
assert_eq!(f1.id, 1);
assert_eq!(f1.tag, "b18ap31");
assert_eq!(f2.id, 2);
assert_eq!(f2.tag, "b18ap31");
§Bind trait to implementation
To add a trait to a container you should bind the trait to its implementation using the macros:
add_scoped_trait!(container, name, trait => impl)
add_singleton_trait!(container, name, trait => impl)
add_scoped_trait!(container, name, trait @ Inject)
add_singleton_trait!(container, name, trait @ Inject)
The
name
is optional.
This adds the trait as a Box<dyn Trait>
.
And you can get the values back using:
get_scoped_trait!(container, name, trait)
get_singleton_trait!(container, name, trait)
get_resolved_trait(container, name, trait)
The
name
is also optional.
This returns the trait as a Box<dyn Trait>
.
use dilib::{
Container,
add_scoped_trait,
add_singleton_trait,
get_resolved_trait,
};
trait Discount {
fn get_discount(&self) -> f32;
}
trait Fruit {
fn name(&self) -> &str;
fn price(&self) -> f32;
}
struct TenPercentDiscount;
impl Discount for TenPercentDiscount {
fn get_discount(&self) -> f32 {
0.1
}
}
struct Apple;
struct Orange;
impl Fruit for Apple {
fn name(&self) -> &str {
"Apple"
}
fn price(&self) -> f32 {
2.0
}
}
impl Fruit for Orange {
fn name(&self) -> &str {
"Orange"
}
fn price(&self) -> f32 {
1.7
}
}
let mut container = Container::new();
add_singleton_trait!(container, Discount => TenPercentDiscount).unwrap();
add_scoped_trait!(container, "apple", Fruit => Apple).unwrap();
add_scoped_trait!(container, "orange", Fruit => Orange).unwrap();
// All types are returned as `Box<dyn Trait>`
let discount = get_resolved_trait!(container, Discount).unwrap();
let apple = get_resolved_trait!(container, Fruit, "apple").unwrap();
let orange = get_resolved_trait!(container, Fruit, "orange").unwrap();
assert_eq!(discount.get_discount(), 0.1);
assert_eq!(apple.name(), "Apple");
assert_eq!(apple.price(), 2.0);
assert_eq!(orange.name(), "Orange");
assert_eq!(orange.price(), 1.7);
§get, get_scoped and get_singleton
There are 3 ways to retrieve a value from the container:
get
get_scoped
get_singleton
And it’s named variants:
get_with_name
get_scoped_with_name
get_singleton_with_name
get_scoped
and get_singleton
are self-explanatory, they get
a value from a scoped
or singleton
provider.
But get
can get any scoped
and singleton
value,
the difference is that get
returns a Resolved<T>
and the others returns a T
(scoped) or Arc<T>
(singletons).
Resolved<T>
is just an enum for a Scoped(T)
and Singleton(Arc<T>)
where you can convert it back using into_scoped
or into_singleton
,
it also implements Deref
over T
.
§Derive Inject
This requires the
derive
feature.
Inject is implemented for all types that implement Default
and can be auto-implemented using #[derive]
. When using the derive
types Arc<T>
and Singleton<T>
will be injected as singleton,
and other types as scoped unless specified.
use dilib::{Singleton, Inject, Container};
use dilib_derive::*;
#[derive(Inject)]
struct Apple {
// Singleton is an alias for Arc<T>
#[inject(name="apple")]
tag: Singleton<String>,
#[inject(name="apple_price")]
price: f32
}
let mut container = Container::new();
container.add_singleton_with_name("apple", String::from("FRUIT_APPLE")).unwrap();
container.add_scoped_with_name("apple_price", || 2.0_f32).unwrap();
container.add_deps::<Apple>();
let apple = container.get::<Apple>().unwrap();
assert_eq!(apple.tag.as_ref(), "FRUIT_APPLE");
assert_eq!(apple.price, 2.0);
§Global Container
This requires the
global
feature.
dilib
also offers a global container so you don’t require
to declare your own, you can access the values of the container
using get_scoped!
, get_singleton!
or get_resolved!
,
you can also access the container directly using get_container()
.
use dilib::{global::init_container, resolve};
init_container(|container| {
container.add_scoped(|| String::from("Orange")).unwrap();
container.add_singleton_with_name("num", 123_i32).unwrap();
}).expect("unable to initialize the container");
let orange = resolve!(String).unwrap();
let num = resolve!(i32, "num").unwrap();
assert_eq!(orange.as_ref(), "Orange");
assert_eq!(*num, 123);
§Provide
This requires the
unstable_provide
feature.
§Why unstable_provide?
The feature unstable_provide
make possible to have dependency
injection more similar to other frameworks like C# EF Core
or Java Spring
.
To allow run code before main we use the the ctor crate, which have been tested in several OS so is stable for most of the use cases.
§provide macro
You can use the #[provide]
macro over any function or type that implements
**Inject
to register it to the global container.
use std::sync::RwLock;
use dilib::global::init_container;
use dilib::{resolve, Singleton, Inject, provide};
#[allow(dead_code)]
#[derive(Debug, Clone)]
struct User {
name: &'static str,
email: &'static str,
}
trait Repository<T> {
fn add(&self, item: T);
fn get_all(&self) -> Vec<T>;
}
#[derive(Default)]
#[provide(scope="singleton")]
struct Db(RwLock<Vec<User>>);
#[derive(Inject)]
#[provide(bind="Repository<User>")]
struct UserRepository(Singleton<Db>);
impl Repository<User> for UserRepository {
fn add(&self, item: User) {
self.0.0.write().unwrap().push(item);
}
fn get_all(&self) -> Vec<User> {
self.0.0.read().unwrap().clone()
}
}
// Initialize the container to register the providers
init_container(|_container| {
// Add additional providers
}).unwrap();
let user_repository = resolve!(trait Repository<User>).unwrap();
user_repository.add(User { name: "Marie", email: "marie@example.com" });
user_repository.add(User { name: "Natasha", email: "natasha@example.com" });
let users = user_repository.get_all();
let db = resolve!(Db).unwrap();
println!("Total users: {}", db.0.read().unwrap().len());
println!("{:#?}", users);
Re-exports§
pub use macros::*;
Modules§
- global
- A global instance of
Container
. - late_
init - A lazy evaluated cell.
- macros
- procedural macros of
dilib
.
Macros§
- add_
scoped_ trait - Helper macro to bind a
trait
to it’s implementation in aContainer
as scoped. - add_
singleton_ trait - Helper macro to bind a
trait
to it’s implementation in aContainer
as a singleton. - get_
resolved_ trait - Helper macro to get an implementation of a
trait
in aContainer
. - get_
scoped - Returns a scoped value from the global
Container
orNone
if is not in the container. - get_
scoped_ trait - Helper macro to get the implementation of a
trait
in aContainer
as scoped. - get_
singleton - Returns a singleton value from the global
Container
orNone
if is not in the container. - get_
singleton_ trait - Helper macro to get the implementation of a
trait
in aContainer
as a singleton. - resolve
- Returns a value from the
Container
orNone
if is not in the container.
Structs§
- Container
- Represents a store to register and retrieve objects.
- Injection
Key - Represents an unique key for identify a provider.
Enums§
- Provider
- Provides a
Container
value. - Provider
Kind - Represents the type of the provider.
- Resolved
- Represents a value from a
Container
. - Scoped
- Represents an
Scoped
provider which provide a new instance each time. - Shared
- Provides a singleton value.
Traits§
- Inject
- A trait for constructing a type getting the dependencies from a
Container
.
Type Aliases§
- Singleton
- A convenient singleton type.
Derive Macros§
- Inject
- Provides an implementation of the
Inject
trait for the given type.