[dependencies]
ferrunix = "0.3"
Compiler support: requires rustc 1.67.1+
Check out the User Guide.
Documentation
Due to how the various features affect the public API of the library, the documentation is provided for each major feature separately.
Feature Flags | Link to Documentation |
---|---|
none | link to docs |
multithread | link to docs |
tokio | link to docs |
Features
- Can register and inject any type (incl. generics, types must be
Send
if themultithread
feature is enabled, andSend + Sync
iftokio
is enabled). - Simple and elegant Rust API; making the derive macro purely optional.
- Different dependency lifetimes:
- Singleton: Only a single instance of the object is created.
- Transient: A new instance is created for every request.
- Dependency resolution happens at run time, making it possible to dynamically register types.
- Injection of concrete value types (
T
),Box<T>
,Rc<T>
, andArc<T>
. - Derive macro (
#[derive(Inject)]
) to simplify registration. - Automatic registration of types, thanks to
inventory
. - One global registry; with support for multiple sub-registries.
Cargo Feature Flags
Ferrunix has the following features to enable further functionality.
Features enabled by default are marked with *
.
multithread
: Enables support for accessing the registry from multiple threads. This adds a bound that all registered types must beSend
.derive
(*
): Enables support for the#[derive(Inject)]
macro.tokio
: Enables support forasync
constructors. Bumps the MSRV up to1.75.0
because some of the internal traits require RPITIT.tracing
: Enables support for tracing and annotates all public functions withtracing::instrument
.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.By contributing to this project (for example, through submitting a pull request) you agree with the individual contributor license agreement. Make sure to read and understand it.
First Steps
Follow the steps of creating your first application with ferrunix
.
Installation
Install ferrunix
by adding the following to your Cargo.toml
.
[dependencies]
ferrunix = "0.3"
or by running cargo add
:
cargo add ferrunix
Once installed, the next step is understanding the core concepts.
Core Concepts
Fundamentally, ferrunix
is a hash table, with the registered type as the
key, and the objects constructor (for transients) or value (for singletons) as
the value.
In Rust, this could be, very crudely, represented as a HashMap
:
enum Provider {
Transient(fn() -> Box<dyn Any>),
Singleton(OnceCell<dyn Any>),
}
type Registry = HashMap<TypeId, Provider>;
Of course, reality is a bit more complicated.
The core concepts necessary to understand for efficiently using ferrunix
are:
All of which will be tacked in the next few pages.
Registry
The Registry
is used for registering new objects, and retrieving previously
registered objects.
Each object has a specific lifetime set during registration, currently, the following two lifetimes are supported:
transient
: when retrieved, a new object is created.singleton
: when retrieved, a single instance is lazily created, and a reference is returned.
All functions of the registry are outlined in the Registry
reference guide
or described in detail in the documentation.
The next step is to more thoroughly understand lifetimes.
Lifetimes
Transient
Transients are constructed every time when requested. For transients, the following container types are supported:
T
: No container.Box<T>
: A boxed type, whereT
can be adyn T
.
Singleton
Singletons are lazily constructed on the first access, and only a single instance can be created over the programs lifetime. When requested, a reference to the object is returned. The following container types are supported:
Rc<T>
: Whenmultithread
andtokio
feature are disabled.Arc<T>
: When themultithread
ortokio
is enabled.
The library offers a Ref
type alias, which is aliasing the correct
container, based on the enabled features.
The next step is to understand registration.
Registration
To register a new object, we need to tell the Registry
which type we want
to use as a key, what dependencies it has, and how to construct it.
For the registration, we have four functions on the Registry
that are of
interest:
Registry::transient(...)
Registry::singleton(...)
Registry::with_deps::<_, (Ts, ...)>().transient(...)
Registry::with_deps::<_, (Ts, ...)>().singleton(...)
Without dependencies
The most straightforward object to register is a type without dependencies:
#![allow(unused)] extern crate ferrunix; use ferrunix::Registry; pub struct CurrentCurrency(&'static str); fn main() { // Create a new empty registry. let registry = Registry::empty(); // Register `CurrentCurrency` as a singleton. registry.singleton(|| CurrentCurrency("USD")); // Retrieve `CurrentCurrency` from the registry. let currency = registry.get_singleton::<CurrentCurrency>().unwrap(); // Assert that our retrieved object is actually what we expect. assert_eq!(currency.0, "USD"); }
Of course, this example is rather simple, but should highlight the pattern of registering a new object:
- The constructor is registered with
registry.singleton
orregistry.transient
. - A new object is retrieved using either
registry.get_singleton
orregistry.get_transient
. - The retrieved object is used.
The CurrentCurrency
object in the example above is registered with a
singleton
lifetime. As a result, it needs to be retrieved with
get_singleton
. Trying to retrieve it with get_transient
will return None
.
Retrieval lifetime must match registration lifetime!
With dependencies
Types that have dependencies need to state the dependencies they have, so that
the Registry
can fulfill them before passing them to the constructor of the
type to be registered.
#![allow(unused)] extern crate ferrunix; use ferrunix::{Transient, Registry}; pub struct CurrentCurrency(&'static str); pub struct Config { currency: CurrentCurrency, } fn main() { let registry = Registry::empty(); // Register a type without dependencies. registry.transient(|| CurrentCurrency("USD")); // Register our `Config` types with a dependency. registry .with_deps::<_, (Transient<CurrentCurrency>,)>() // Trailing comma required! .transient(|(currency,)| { Config { currency: currency.get(), } }); // Construct a new config. let config = registry.get_transient::<Config>().unwrap(); // Assert that our retrieved object is actually what we expect. assert_eq!(config.currency.0, "USD"); }
This follows a very similar pattern as our previous example; however, the registration of the type with dependencies is slightly different.
Let's examine this in a bit more detail:
#![allow(unused)] extern crate ferrunix; use ferrunix::{Transient, Registry}; pub struct CurrentCurrency(&'static str); pub struct Config { currency: CurrentCurrency, } fn main() { let registry = Registry::empty(); registry .with_deps::<_, (Transient<CurrentCurrency>,)>() // <-- (1) .transient(|(currency,)| { // <-- (2) Config { currency: currency.get(), // <-- (3) } }); }
At (1)
, the .with_deps
function has the following (simplified) call
signature: fn with_deps<Ret, Deps>
with Ret
being the type that's to be
registered, in our case Config
and Deps
a tuple type of our dependencies,
in our case (CurrentCurrency,)
.
To indicate, that this is a transient dependency, the Transient<T>
marker type
needs to be used. A similar marker also exists for singletons, it's called
Singleton<T>
.
At (2)
, the constructor for the transient object Config
is registered. It
takes a single argument, a tuple of dependencies, which we immediately
destructure into it's parts.
The trailing comma for the tuple at
(1)
and (2)
is
necessary to indicate a single element tuple (and not a literal that's in
parentheses). This is only necessary for types with one dependency.
At (3)
, we'll use the Transient<T>
's get()
function to consume it and
get the inner CurrentCurrency
to construct the Config
, which is returned
from the constructor function. The inner CurrentCurrency
is constructed with
the previously registered constructor.
With the registration done, the last part to is retrieval of constructed objects.
Retrieval
Each registered object needs to be retrieved with the same lifetime that it was registered with.
For retrieval, the following functions are available on the [Registry
]:
The generic type parameter T
is necessary to be specified, either on the
function call or the assignee. It's necessary to find which object to
construct.
This is the example from the previous section:
#![allow(unused)] extern crate ferrunix; use ferrunix::{Transient, Registry}; pub struct CurrentCurrency(&'static str); pub struct Config { currency: CurrentCurrency } fn main() { let registry = Registry::empty(); registry.transient(|| CurrentCurrency("USD")); registry .with_deps::<_, (Transient<CurrentCurrency>,)>() // Trailing comma required! .transient(|(currency,)| { Config { currency: currency.get(), } }); // Construct a new config and currency. let config = registry.get_transient::<Config>().unwrap(); // <-- (1) let currency: CurrentCurrency = registry.get_transient().unwrap(); // <-- (2) // Assert that our retrieved object is actually what we expect. assert_eq!(config.currency.0, "USD"); assert_eq!(currency.0, "USD"); }
At (1)
the Registry::get_transient::<T>()
function is used to construct a
new Config
. The generic type parameter T
is directly specified at the
function call.
However, at (2)
, the Rust compiler can infer the type T
from the annotation
on the left, and therefore, it's not necessary to explicitly specify it.
Registry
Creation
Registration
Registry::transient(...)
Registry::singleton(...)
Registry::with_deps::<_, (Ts, ...)>().transient(...)
Registry::with_deps::<_, (Ts, ...)>().singleton(...)
Retrieval
Validation
Derive Macro
The Inject
derive macro supports the two following attributes:
#[provides]
: Customizing the object registration.#[inject]
: Customizing how an injected member is created.
#[derive(Inject)]
#[provides(PROPERTY...)]
struct Transient {
#[inject(PROPERTY...)]
field: UserType,
}
provides
Properties
transient [= "<TYPE-SIGNATURE>"]
- The object is provided as a transient registered with
<TYPE-SIGNATURE>
as key. If the signature is omitted, the concrete type is used as a key.
- The object is provided as a transient registered with
singleton [= "<TYPE-SIGNATURE>"]
- The object is provided as a singleton registered with
<TYPE-SIGNATURE>
as key. If the signature is omitted, the concrete type is used as a key.
- The object is provided as a singleton registered with
ctor = <IDENTIFIER>
- The object isn't constructed using member-wise construction, but it's
constructed using a custom constructor (e.g.,
new
). The constructor will be passed the members in order of declaration as parameters.
- The object isn't constructed using member-wise construction, but it's
constructed using a custom constructor (e.g.,
no_registration
- The type isn't registered automatically and the generated
Self::register(&ferrunix::Registry)
function needs to be called manually to register the type.
- The type isn't registered automatically and the generated
inject
Properties
default
- Construct the field using the
Default
implementation.
- Construct the field using the
ctor = "<RUST-CODE>"
- Construct the field using the provided Rust code.
transient [= true]
- Construct the field as a transient by retrieving it from the
Registry
.
- Construct the field as a transient by retrieving it from the
singleton [= true]
- Construct the field as a singleton by retrieving it from the
Registry
.
- Construct the field as a singleton by retrieving it from the
Full Example
#![allow(unused)] extern crate ferrunix; use ferrunix::Inject; pub trait Logger {} #[derive(Inject)] #[provides(transient = "dyn Logger", no_registration)] // ^^^^^^^^^^^^^^ // The explicit type can be omitted, if it matches the concrete type. pub struct MyLogger {} impl Logger for MyLogger {} #[derive(Inject)] #[provides(singleton, no_registration)] pub struct MyConfig { #[inject(default)] // Use the `Default::default` impl for construction. counter: u32, #[inject(ctor = r#""log-prefix: ".to_owned()"#)] // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // The constructor must be valid Rust code. For strings, two sets of quotes // are required. prefix: String, #[inject(transient /* = true */)] // ^^^^^^^^^^^^ // The long form with `= true` is optional. logger: Box<dyn Logger>, } fn main() { let registry = ferrunix::Registry::empty(); MyLogger::register(®istry); MyConfig::register(®istry); }
Feature Flags
Features enabled by default are marked with *
.
multithread
: Enables support for accessing the registry from multiple threads. This adds a bound that all registered types must beSend
.derive
(*
): Enables support for the#[derive(Inject)]
macro.tokio
: Enables support forasync
constructors. Bumps the MSRV up to1.75.0
because some of the internal traits require RPITIT.tracing
: Enables support for tracing and annotates all public functions withtracing::instrument
.