A proc macro for designing loosely coupled Rust applications.
entrait is used to generate an implemented trait from the definition of regular functions.
The emergent pattern that results from its use enable the following things:
* Zero-cost loose coupling and inversion of control
* Dependency graph as a compile time concept
* Mock library integrations
* Clean, readable, boilerplate-free code
The resulting pattern is referred to as the entrait pattern (see also: philosophy).
The macro looks like this:
#[entrait(MyFunction)]
fn my_function<D>(deps: &D) {
}
which generates a new single-method trait named MyFunction, with the method signature derived from the original function.
Entrait is a pure append-only macro: It will never alter the syntax of your function.
The new language items it generates will appear below the function.
In the first example, my_function has a single parameter called deps which is generic over a type D, and represents dependencies injected into the function.
The dependency parameter is always the first parameter, which is analogous to the &self parameter of the generated trait method.
To add a dependency, we just introduce a trait bound, now expressable as impl Trait.
This is demonstrated by looking at one function calling another:
#[entrait(Foo)]
fn foo(deps: &impl Bar) {
println!("{}", deps.bar(42));
}
#[entrait(Bar)]
fn bar<D>(deps: &D, n: i32) -> String {
format!("You passed {n}")
}
Other frameworks might represent multiple dependencies by having one value for each one, but entrait represents all dependencies within the same value. When the dependency parameter is generic, its trait bounds specifiy what methods we expect to be callable inside the function.
Multiple bounds can be expressed using the &(impl A + B) syntax.
The single-value dependency design means that it is always the same reference that is passed around everywhere. But a reference to what, exactly? This is what we have managed to abstract away, which is the whole point.
When we want to compile a working application, we need an actual type to inject into the various entrait entrypoints. Two things will be important:
Entrait generates implemented traits, and the type to use for linking it all together is Impl<T>:
#[entrait(Foo)]
fn foo(deps: &impl Bar) -> i32 {
deps.bar()
}
#[entrait(Bar)]
fn bar(_deps: &impl std::any::Any) -> i32 {
42
}
let app = Impl::new(());
assert_eq!(42, app.foo());
🔬 Inspect the generated code 🔬
The linking happens in the generated impl block for Impl<T>, putting the entire impl under a where clause derived from the original dependency bounds:
impl<T: Sync> Foo for Impl<T> where Self: Bar {
fn foo(&self) -> i32 {
foo(self) // <---- calls your function
}
}
Impl is generic, so we can put whatever type we want into it.
Normally this would be some type that represents the global state/configuration of the running application.
But if dependencies can only be traits, and we always abstract away this type, how can this state ever be accessed?
So far we have only seen generic trait-based dependencies, but the dependency can also be a concrete type:
struct Config(i32);
#[entrait(UseTheConfig)]
fn use_the_config(config: &Config) -> i32 {
config.0
}
#[entrait(DoubleIt)]
fn double_it(deps: &impl UseTheConfig) -> i32 {
deps.use_the_config() * 2
}
assert_eq!(42, Impl::new(Config(21)).double_it());
The parameter of use_the_config is in the first position, so it represents the dependency.
We will notice two interesting things:
* Functions that depend on UseTheConfig, either directly or indirectly, now have only one valid dependency type: Impl<Config>1.
* Inside use_the_config, we have a &Config reference instead of &Impl<Config>. This means we cannot call other entraited functions, because they are not implemented for Config.
The last point means that a concrete dependency is the end of the line, a leaf in the dependency graph.
Typically, functions with a concrete dependency should be kept small and avoid extensive business logic. They ideally function as accessors, providing a loosely coupled abstraction layer over concrete application state.
To reduce the number of generated traits, entrait can be used as a mod attribute.
When used in this mode, the macro will look for non-private functions directly within the module scope, to be represented as methods on the resulting trait.
This mode works mostly identically to the standalone function mode.
#[entrait(pub MyModule)]
mod my_module {
pub fn foo(deps: &impl super::SomeTrait) {}
pub fn bar(deps: &impl super::OtherTrait) {}
}
This example generates a MyModule trait containing the methods foo and bar.
UnimockThe whole point of entrait is to provide inversion of control, so that alternative dependency implementations can be used when unit testing function bodies. While test code can contain manual trait implementations, the most ergonomic way to test is to use a mocking library, which provides more features with less code.
Entrait works best together with unimock, as these two crates have been designed from the start with each other in mind.
Unimock exports a single mock struct which can be passed as argument to every function that accept a generic deps parameter
(given that entrait is used with unimock support everywhere).
To enable mock configuration of entraited functions, supply the mock_api option, e.g. mock_api=TraitMock if the name of the trait is Trait.
This works the same way for entraited modules, only that those already have a module to export from.
Unimock support for entrait is enabled by passing the unimock option to entrait (#[entrait(Foo, unimock)]), or turning on the unimock feature, which makes all entraited functions mockable, even in upstream crates (as long as mock_api is provided.).
#[entrait(Foo, mock_api=FooMock)]
fn foo<D>(_: &D) -> i32 {
unimplemented!()
}
#[entrait(MyMod, mock_api=mock)]
mod my_mod {
pub fn bar<D>(_: &D) -> i32 {
unimplemented!()
}
}
fn my_func(deps: &(impl Foo + MyMod)) -> i32 {
deps.foo() + deps.bar()
}
let mocked_deps = Unimock::new((
FooMock.each_call(matching!()).returns(40),
my_mod::mock::bar.each_call(matching!()).returns(2),
));
assert_eq!(42, my_func(&mocked_deps));
Entrait with unimock supports un-mocking. This means that the test environment can be partially mocked!
#[entrait(SayHello)]
fn say_hello(deps: &impl FetchPlanetName, planet_id: u32) -> Result<String, ()> {
Ok(format!("Hello {}!", deps.fetch_planet_name(planet_id)?))
}
#[entrait(FetchPlanetName)]
fn fetch_planet_name(deps: &impl FetchPlanet, planet_id: u32) -> Result<String, ()> {
let planet = deps.fetch_planet(planet_id)?;
Ok(planet.name)
}
pub struct Planet {
name: String
}
#[entrait(FetchPlanet, mock_api=FetchPlanetMock)]
fn fetch_planet(deps: &(), planet_id: u32) -> Result<Planet, ()> {
unimplemented!("This doc test has no access to a database :(")
}
let hello_string = say_hello(
&Unimock::new_partial(
FetchPlanetMock
.some_call(matching!(123456))
.returns(Ok(Planet {
name: "World".to_string(),
}))
),
123456,
).unwrap();
assert_eq!("Hello World!", hello_string);
This example used Unimock::new_partial to create a mocker that works mostly like Impl, except that the call graph can be short-circuited at arbitrary, run-time configurable points.
The example code goes through three layers (say_hello => fetch_planet_name => fetch_planet), and only the deepest one gets mocked out.
If you instead wish to use a more established mocking crate, there is also support for mockall. Note that mockall has some limitations. Multiple trait bounds are not supported, and deep tests will not work. Also, mockall tends to generate a lot of code, often an order of magnitude more than unimock.
Enabling mockall is done using the mockall entrait option.
There is no cargo feature to turn this on implicitly, because mockall doesn't work well when it's re-exported through another crate.
#[entrait(Foo, mockall)]
fn foo<D>(_: &D) -> u32 {
unimplemented!()
}
fn my_func(deps: &impl Foo) -> u32 {
deps.foo()
}
fn main() {
let mut deps = MockFoo::new();
deps.expect_foo().returning(|| 42);
assert_eq!(42, my_func(&deps));
}
A common technique for Rust application development is to choose a multi-crate architecture. There are usually two main ways to go about it:
The first option is how libraries are normally used: Its functions are just called, without any indirection.
The second option can be referred to as a variant of the dependency inversion principle. This is usually a desirable architectural property, and achieving this with entrait is what this section is about.
The main goal is to be able to express business logic centrally, and avoid depending directly on infrastructure details (onion architecture). All of the examples in this section make some use of traits and trait delegation.
Earlier it was mentioned that when concrete-type dependencies are used, the T in Impl<T>, your application, and the type of the dependency have to match.
But this is only partially true.
It really comes down to which traits are implemented on what types:
pub struct Config {
foo: String,
}
#[entrait_export(pub GetFoo)]
fn get_foo(config: &Config) -> &str {
&config.foo
}
🔬 Inspect the generated code 🔬
trait GetFoo {
fn get_foo(&self) -> &str;
}
impl<T: GetFoo> GetFoo for Impl<T> {
fn get_foo(&self) -> &str {
self.as_ref().get_foo()
}
}
impl GetFoo for Config {
fn get_foo(&self) -> &str {
get_foo(self)
}
}
Here we actually have a trait GetFoo that is implemented two times: for Impl<T> where T: GetFoo and for Config.
The first implementation is delegating to the other one.
For making this work with any downstream application type, we just have to manually implement GetFoo for that application:
struct App {
config: some_upstream_crate::Config,
}
impl some_upstream_crate::GetFoo for App {
fn get_foo(&self) -> &str {
self.config.get_foo()
}
}
Using a concrete type like Config from the first case can be contrived in many situations.
Sometimes a good old hand-written trait definition will do the job much better:
#[entrait]
pub trait System {
fn current_time(&self) -> u128;
}
🔬 Inspect the generated code 🔬
impl<T: System> System for Impl<T> {
fn current_time(&self) -> u128 {
self.as_ref().current_time()
}
}
What the attribute does in this case, is just to generate the correct blanket implementations of the trait: delegation and mocks.
To use with some App, the app type itself should implement the trait.
Sometimes it might be desirable to have a delegation that involves dynamic dispatch.
Entrait has a delegate_by = option, where you can pass an alternative trait to use as part of the delegation strategy.
To enable dynamic dispatch, use ref:
#[entrait(delegate_by=ref)]
trait ReadConfig: 'static {
fn read_config(&self) -> &str;
}
🔬 Inspect the generated code 🔬
impl<T: ::core::convert::AsRef<dyn ReadConfig> + 'static> ReadConfig for Impl<T> {
fn read_config(&self) -> &str {
self.as_ref().as_ref().read_config()
}
}
To use this together with some App, it should implement the [AsRef<dyn ReadConfig>](https://doc.rust-lang.org/stabl
$ claude mcp add entrait \
-- python -m otcore.mcp_server <graph>