Giter Club home page Giter Club logo

entrait's Introduction

entrait

crates.io docs.rs CI

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).

Introduction

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}")
}

Multiple dependencies

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.

Runtime and implementation

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:

  • All trait bounds used deeper in the graph will implicitly "bubble up" to the entrypoint level, so the type we eventually use will need to implement all those traits in order to type check.
  • The implementations of these traits need to do the correct thing: Actually call the entraited function, so that the dependency graph is turned into an actual call graph.

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?

Concrete dependencies

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.

Module support

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.

Testing

Trait mocking with Unimock

The 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));
Deep integration testing with unimock

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.

Alternative mocking: Mockall

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));
}

Multi-crate architecture

A common technique for Rust application development is to choose a multi-crate architecture. There are usually two main ways to go about it:

  1. The call graph and crate dependency go in the same direction.
  2. The call graph and crate dependency go in opposite directions.

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.

Case 1: Concrete leaf dependencies

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()
    }
}

Case 2: Hand-written trait as a leaf dependency

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.

Case 3: Hand-written trait as a leaf dependency using dynamic dispatch

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> trait.

Case 4: Truly inverted internal dependencies - static dispatch

All cases up to this point have been leaf dependencies. Leaf dependencies are delegations that exit from the Impl<T> layer, using delegation targets involving concete T's. This means that it is impossible to continue to use the entrait pattern and extend your application behind those abstractions.

To make your abstraction extendable and your dependency internal, we have to keep the T generic inside the [Impl] type. To make this work, we have to make use of two helper traits:

#[entrait(RepositoryImpl, delegate_by = DelegateRepository)]
pub trait Repository {
    fn fetch(&self) -> i32;
}
๐Ÿ”ฌ Inspect the generated code ๐Ÿ”ฌ
pub trait RepositoryImpl<T> {
    fn fetch(_impl: &Impl<T>) -> i32;
}
pub trait DelegateRepository<T> {
    type Target: RepositoryImpl<T>;
}
impl<T: DelegateRepository<T>> Repository for Impl<T> {
    fn fetch(&self) -> i32 {
        <T as DelegateRepository<T>>::Target::fetch(self)
    }
}

This syntax introduces a total of three traits:

  • Repository: The dependency, what the rest of the application directly calls.
  • RepositoryImpl<T>: The delegation target, a trait which needs to be implemented by some Target type.
  • DelegateRepository<T>: The delegation selector, that selects the specific Target type to be used for some specific App.

This design makes it possible to separate concerns into three different crates, ordered from most-upstream to most-downstream:

  1. Core logic: Depend on and call Repository methods.
  2. External system integration: Provide some implementation of the repository, by implementing RepositoryImpl<T>.
  3. Executable: Construct an App that selects a specific repository implementation from crate 2.

All delegation from Repository to RepositoryImpl<T> goes via the DelegateRepository<T> trait. The method signatures in RepositoryImpl<T> are static, and receives the &Impl<T> via a normal parameter. This allows us to continue using entrait patterns within those implementations!

In crate 2, we have to provide an implementation of RepositoryImpl<T>. This can either be done manually, or by using the [entrait] attribute on an impl block:

pub struct MyRepository;

#[entrait]
impl crate1::RepositoryImpl for MyRepository {
    // this function has the now-familiar entrait-compatible signature:
    fn fetch<D>(deps: &D) -> i32 {
        unimplemented!()
    }
}
๐Ÿ”ฌ Inspect the generated code ๐Ÿ”ฌ
impl MyRepository {
    fn fetch<D>(deps: &D) -> i32 {
        unimplemented!()
    }
}
impl<T> crate1::RepositoryImpl<T> for MyRepository {
    #[inline]
    fn fetch(_impl: &Impl<T>) -> i32 {
        Self::fetch(_impl)
    }
}

Entrait will split this trait implementation block in two: An inherent one containing the original code, and a proper trait implementation which performs the delegation.

In the end, we just have to implement our DelegateRepository<T>:

// in crate3:
struct App;
impl crate1::DelegateRepository<Self> for App {
    type Target = crate2::MyRepository;
}
fn main() { /* ... */ }

Case 5: Truly inverted internal dependencies - dynamic dispatch

A small variation of case 4: Use delegate_by=ref instead of a custom trait. This makes the delegation happen using dynamic dispatch.

The implementation syntax is almost the same as in case 4, only that the entrait attribute must now be #[entrait(ref)]:

#[entrait(RepositoryImpl, delegate_by=ref)]
pub trait Repository {
    fn fetch(&self) -> i32;
}

pub struct MyRepository;

#[entrait(ref)]
impl RepositoryImpl for MyRepository {
    fn fetch<D>(deps: &D) -> i32 {
        unimplemented!()
    }
}

The app must now implement AsRef<dyn RepositoryImpl<Self>>.

Options and features

Trait visibility

by default, entrait generates a trait that is module-private (no visibility keyword). To change this, just put a visibility specifier before the trait name:

use entrait::*;
#[entrait(pub Foo)]   // <-- public trait
fn foo<D>(deps: &D) { // <-- private function
}
async support

Since Rust at the time of writing does not natively support async methods in traits, you may opt in to having #[async_trait] generated for your trait. Enable the boxed-futures cargo feature and pass the box_future option like this:

#[entrait(Foo, box_future)]
async fn foo<D>(deps: &D) {
}

This is designed to be forwards compatible with static async fn in traits. When that day comes, you should be able to just remove that option and get a proper zero-cost future.

There is a cargo feature to automatically apply #[async_trait] to every generated async trait: use-boxed-futures.

Zero-cost async inversion of control - preview mode

Entrait has experimental support for zero-cost futures. A nightly Rust compiler is needed for this feature.

The entrait option is called associated_future, and uses GATs and feature(type_alias_impl_trait). This feature generates an associated future inside the trait, and the implementations use impl Trait syntax to infer the resulting type of the future:

#![feature(type_alias_impl_trait)]

use entrait::*;

#[entrait(Foo, associated_future)]
async fn foo<D>(deps: &D) {
}

There is a feature for turning this on everywhere: use-associated-futures.

Integrating with other fn-targeting macros, and no_deps

Some macros are used to transform the body of a function, or generate a body from scratch. For example, we can use feignhttp to generate an HTTP client. Entrait will try as best as it can to co-exist with macros like these. Since entrait is a higher-level macro that does not touch fn bodies (it does not even try to parse them), entrait should be processed after, which means it should be placed before lower level macros. Example:

#[entrait(FetchThing, no_deps)]
#[feignhttp::get("https://my.api.org/api/{param}")]
async fn fetch_thing(#[path] param: String) -> feignhttp::Result<String> {}

Here we had to use the no_deps entrait option. This is used to tell entrait that the function does not have a deps parameter as its first input. Instead, all the function's inputs get promoted to the generated trait method.

Conditional compilation of mocks

Most often, you will only need to generate mock implementations for test code, and skip this for production code. A notable exception to this is when building libraries. When an application consists of several crates, downstream crates would likely want to mock out functionality from libraries.

Entrait calls this exporting, and it unconditionally turns on autogeneration of mock implementations:

#[entrait_export(pub Bar)]
fn bar(deps: &()) {}

or

#[entrait(pub Foo, export)]
fn foo(deps: &()) {}

It is also possible to reduce noise by doing use entrait::entrait_export as entrait.

Feature overview
Feature Implies Description
unimock Adds the [unimock] dependency, and turns on Unimock implementations for all traits.
use-boxed-futures boxed-futures Automatically applies the [async_trait] macro to async trait methods.
use-associated-futures Automatically transforms the return type of async trait methods into an associated future by using type-alias-impl-trait syntax. Requires a nightly compiler.
boxed-futures Pulls in the [async_trait] optional dependency, enabling the box_future entrait option (macro parameter).

"Philosophy"

The entrait crate is central to the entrait pattern, an opinionated yet flexible and Rusty way to build testable applications/business logic.

To understand the entrait model and how to achieve Dependency Injection (DI) with it, we can compare it with a more widely used and classical alternative pattern: Object-Oriented DI.

In object-oriented DI, each named dependency is a separate object instance. Each dependency exports a set of public methods, and internally points to a set of private dependencies. A working application is built by fully instantiating such an object graph of interconnected dependencies.

Entrait was built to address two drawbacks inherent to this design:

  • Representing a graph of objects (even if acyclic) in Rust usually requires reference counting/heap allocation.
  • Each "dependency" abstraction often contains a lot of different functionality. As an example, consider DDD-based applications consisting of DomainServices. There will typically be one such class per domain object, with a lot of methods in each. This results in dependency graphs with fewer nodes overall, but the number of possible call graphs is much larger. A common problem with this is that the actual dependenciesโ€”the functions actually getting calledโ€”are encapsulated and hidden away from public interfaces. To construct valid dependency mocks in unit tests, a developer will have to read through full function bodies instead of looking at signatures.

entrait solves this by:

  • Representing dependencies as traits instead of types, automatically profiting from Rust's builtin zero-cost abstraction tool.
  • Giving users a choice between fine and coarse dependency granularity, by enabling both single-function traits and module-based traits.
  • Always declaring dependencies at the function signature level, close to call sites, instead of at module level.

Limitations

This section lists known limitations of entrait:

Cyclic dependency graphs

Cyclic dependency graphs are impossible with entrait. In fact, this is not a limit of entrait itself, but with Rust's trait solver. It is not able to prove that a type implements a trait if it needs to prove that it does in order to prove it.

While this is a limitation, it is not necessarily a bad one. One might say that a layered application architecture should never contain cycles. If you do need recursive algorithms, you could model this as utility functions outside of the entraited APIs of the application.

entrait's People

Contributors

audunhalland avatar mrjerb avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

mrjerb

entrait's Issues

Associated types fail to compile

Hey @audunhalland, another associated type issue here!

#[entrait::entrait]
pub trait Client {
    type DatabaseImpl;

    fn database(&self, name: &str) -> Self::DatabaseImpl;
}

The associated type disappears from the generated trait:

pub trait MongoClient {
    fn database(&self, name: &str) -> Self::DatabaseImpl;
}

It also needs to be bound in the entrait::Impl block as:

type DatabaseImpl = <EntraitT as MongoClient>::DatabaseImpl;

cargo-rdme does not render code examples under feature flags

i.e. #[cfg_attr(feature = "?", doc = "some code example")]

Possible solutions:

  1. Find a way to make doctests unconditional, exclude doctests from cargo hack runs, then only run doctests with specific features turned on. But there seems to be no way to exclude doctests from a test run.
  2. Fix cargo rdme so that it can interpret feature flags
  3. Write a new readme generator (a lot of work?)
  4. Don't autogenerate readme file (also a lot of manual sync work)
  5. Make README a much smaller, standalone document, with links to docs.rs for specific features

Axum example doesn't compile

Cloned main and tried to compile the axum example but was met with (r1.65.0):

error[E0433]: failed to resolve: could not find `__async_trait` in `entrait`
  --> examples/axum/src/main.rs:14:36
   |
14 |     #[entrait(pub GetFoo, no_deps, box_future, mock_api=GetFooMock)]
   |                                    ^^^^^^^^^^ could not find `__async_trait` in `entrait`

error[E0706]: functions in traits cannot be declared `async`
  --> examples/axum/src/main.rs:14:5
   |
14 |     #[entrait(pub GetFoo, no_deps, box_future, mock_api=GetFooMock)]
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
15 |     async fn get_foo() -> Foo {
   |     ----- `async` because of this
   |
   = note: `async` trait functions are not currently supported
   = note: consider using the `async-trait` crate: https://crates.io/crates/async-trait
   = note: see issue #91611 <https://github.com/rust-lang/rust/issues/91611> for more information
   = note: this error originates in the attribute macro `entrait` (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0706]: functions in traits cannot be declared `async`
  --> examples/axum/src/main.rs:15:5
   |
15 |     async fn get_foo() -> Foo {
   |     -----^^^^^^^^^^^^^^^^^^^^
   |     |
   |     `async` because of this
   |
   = note: `async` trait functions are not currently supported
   = note: consider using the `async-trait` crate: https://crates.io/crates/async-trait
   = note: see issue #91611 <https://github.com/rust-lang/rust/issues/91611> for more information

error[E0277]: the trait bound `fn(Extension<A>) -> impl Future<Output = Json<Foo>> {Routes::<A>::get_foo}: Handler<_, _>` is not satisfied
   --> examples/axum/src/main.rs:36:51
    |
36  |             axum::Router::new().route("/foo", get(Self::get_foo))
    |                                               --- ^^^^^^^^^^^^^ the trait `Handler<_, _>` is not implemented for fn item `fn(Extension<A>) -> impl Future<Output = Json<Foo>> {Routes::<A>::get_foo}`
    |                                               |
    |                                               required by a bound introduced by this call
    |
    = help: the trait `Handler<T, ReqBody>` is implemented for `Layered<S, T>`
note: required by a bound in `axum::routing::get`
   --> /home/dkolsoi/.cargo/registry/src/github.com-1ecc6299db9ec823/axum-0.5.17/src/routing/method_routing.rs:397:1
    |
397 | top_level_handler_fn!(get, GET);
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `axum::routing::get`
    = note: this error originates in the macro `top_level_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info)

Impl expansion incorrectly adding lifetime from method

Hi there,

Thanks for the library as well as the concept! I might have discovered a case of incorrect expansion in a certain scenario. I'll try to explain below:

I have a handwritten trait named Foo in crate A which I then want to implement as MyFoo in crate B.
The expansion in crate B is causing errors due to the lifetime specified in the method I prescribed in my handwritten trait.

The DoFoo method in my trait contains a lifetime parameter (my real implementation involves tokenizing some input string, and preferably I would like to avoid clones).

Crate A (no issues)

Crate A:

#[entrait(FooImpl, delegate_by = FooDelegate)]
trait Foo {
    fn DoFoo<'a>(&self, input: &'a str) -> &'a str;
}

Crate A expanded (no issues):

trait Foo {
    fn DoFoo<'a>(&self, input: &'a str) -> &'a str;
}

trait FooImpl<EntraitT>: 'static {
    fn DoFoo<'a>(__impl: &::entrait::Impl<EntraitT>, input: &'a str) -> &'a str;
}
pub trait FooDelegate<T> {
    type Target: FooImpl<T>;
}

impl<EntraitT: Sync> Foo for ::entrait::Impl<EntraitT>
where
    EntraitT: FooDelegate<EntraitT> + Sync + 'static,
{
    #[inline]
    fn DoFoo<'a>(&self, input: &'a str) -> &'a str {
        <EntraitT::Target as FooImpl<EntraitT>>::DoFoo(self, input)
    }
}
Crate B

Crate B

struct MyFoo;

#[entrait]
impl FooImpl for MyFoo {
    fn DoFoo<'a, D>(deps: &D, input: &'a str) -> &'a str {
        input
    }
}

Crate B expanded (issue is here)

impl MyFoo {
    fn DoFoo<'a, D>(deps: &D, input: &'a str) -> &'a str {
        input
    }
}

// Issue lies here: as the lifetime is added to the impl for the trait and in the wrong order (it is not needed at all)
impl<EntraitT: Sync, 'a> FooImpl<EntraitT, 'a> for MyFoo {
    #[inline]
    fn DoFoo<'a>(__impl: &::entrait::Impl<EntraitT>, input: &'a str) -> &'a str {
        Self::DoFoo(__impl, input)
    }
}

I am using version 0.5.3 of the crate.

`use entrait::*` often clashes with `use unimock`, since there is a `unimock` module in entrait

should reconsider the idea of different "entrait scopes" for mocks.

Alternatives:

  1. always require attribute options for generating mocks
  2. control mock generation via cargo features

the second option could be interesting, because then libraries could more easily support entrait annotations without any cost: i.e. no features would generate no extra code; everything is opt-in. The applications selects the mocking framework it wants via a feature selection. An important thing to consider in that case is that features must have an additive design, features will be the same in every usage of entrait, regardless of which crate invoked it.

An important point to remember about entrait is that (I think) some entrait patterns will be uniform within a single crate, some patterns will be uniform within a whole binary:

feature "width"
mock library application/binary wide
mock cfg(test) crate wide

Revise how unimock types are exported

Currently, the naming of generated unimock types is very inconsistent:

entrait unimock top-level item MockFn path
#[entrait(Foo)] fn foo() {} new module foo foo::Fn
#[entrait(Foo)] mod foo { fn bar() {} } flattened inside foo foo::bar::Fn
#[entrait] trait Foo { fn bar() } top-level struct Foo__bar

I wish to make these more consistent. The way Mockall does it seems better: always make some Mock* struct that represents the mockable trait.

This is the suggestion:

entrait unimock top-level item MockFn path notes
#[entrait(Foo)] fn foo() {} MockFoo MockFoo() the MockFn struct is defined as an empty tuple struct
#[entrait(Foo)] mod foo { fn bar() {} } MockFoo MockFoo::bar()
#[entrait] trait Foo { fn bar(); } MockFoo MockFoo::bar()

This should be part of the next major version of unimock (0.4), where the defaults will be changed from Foo__bar to Foo::bar()

Investigate always taking self by value

I don't know whether this will work, but consider:

impl<T> Trait for Impl<T> {
    fn foo(&self) {}
}

vs.

impl<T> Trait for &Impl<T> {
    fn foo(self) {}
}

If the traits always take self by value, it might not be necessary to duplicate the traits when doing dependency inversion, because then we can make a newtype without reborrowing:

struct NewType<'a, T>(&'a Impl<T>);

impl<'a, T> Trait for NewType<'a, T> {
    fn foo(self) { self.0.foo() }
}

The main reason it was necessary to duplicate the trait with the current design, was that it was impossible to make futures implement Send when "reborrowing" for a struct like NewType. NewType had to be passed as a reference into the delegation-target, referencing the value just created on the stack, and things referencing a value owned by the stack (vs. the heap) can't be sent to another thread. If NewType is passed by-value, this restriction is likely lifted.

Traits with multiple methods

I'm really intrigued by this implementation of dependency injection in Rust! One issue I was running into was how to create a trait with multiple methods.

I tried this

#[entrait]
pub trait Environment {
    fn read_var(&self, var: &str) -> Result<String> {
        todo!()
    }

    fn read_var_maybe(&self, var: &str) -> Option<String> {
        todo!()
    }
}

but then I get an error about entrait::Impl not implementing Environment, when I try to use the app in my code.

This is the relevant generated code:

pub trait Environment {
    fn read_var(&self, var: &str) -> Result<String>;
    fn read_var_maybe(&self, var: &str) -> Option<String>;
}
impl<EntraitT: Sync> Environment for ::entrait::Impl<EntraitT>
where
    EntraitT: Environment + Sync,
{
    #[inline]
    fn read_var(&self, var: &str) -> Result<String> {
        self.as_ref().read_var(var)
    }
    #[inline]
    fn read_var_maybe(&self, var: &str) -> Option<String> {
        self.as_ref().read_var_maybe(var)
    }
}

I can get it working if I remove the entrait macro and modify it to this:

pub trait Environment {
    fn read_var(&self, var: &str) -> Result<String>;
    fn read_var_maybe(&self, var: &str) -> Option<String>;
}

fn read_var(_deps: &impl std::any::Any, var: &str) -> Result<String> {
    unimplemented!()
}

fn read_var_maybe(_deps: &impl std::any::Any, var: &str) -> Option<String> {
    unimplemented!()
}

impl<EntraitT: Sync> Environment for ::entrait::Impl<EntraitT>
where
    EntraitT: Sync,
{
    #[inline]
    fn read_var(&self, var: &str) -> Result<String> {
        read_var(self, var)
    }
    #[inline]
    fn read_var_maybe(&self, var: &str) -> Option<String> {
        read_var_maybe(self, var)
    }
}

I have to remove the Environment trait bound and change how read_var and read_var_maybe are called. I could definitely be doing something wrong, but it seems like the generated output doesn't compile for traits with multiple methods. Do you have any insights here? If it doesn't work yet, is this a use-case you'd be willing to support?

REAL dependency inversion: Support internal dispatch by using a pair of traits

TL;DR: The crate that defines an interface should not need to be the crate that implements it. The way entrait is designed, using delegating blanket implementations, requires some more delegation "magic".

The entrait-for-trait feature #[entrait(delegate_by = Borrow)] only works for leaf dependencies.

Here is an idea for how to do something similar, but without exiting the Impl layer.

We have some API that we want to potentially implement (downstream) in different dynamic ways:

pub trait Facade {
    fn foo(&self) -> i32;
}

We want to implement this trait for Impl<T> so it can be used as a dependency. But that, by definition, is the only implementation. We want to have a delegation through dynamic dispatch to reach the final destination. Since that trait is already implemented for Impl<T>, we need another trait! We can use the entrait syntax to generate a trait from a trait ๐Ÿ˜ฌ Let's call the new trait Backend:

#[entrait(pub Backend)]
pub trait Facade {
    fn foo(&self) -> i32;
}

The generated trait is generic, and has two receivers:

pub trait Backend<T>: 'static {
    fn foo(&self, _impl: &entrait::Impl<T>) -> i32;
}

The generated delegation looks like this:

impl<T: 'static> Facade for Impl<T>
where
    T: core::borrow::Borrow<dyn Backend<T>>,
{
    fn foo(&self) -> i32 {
        self.borrow().foo(self)
    }
}

Now the application has to implement Borrow<dyn Backend<Self>>. This is the manual part.

To define an implementation of Backend<T>, we can write the following module:

pub struct MyImpl;

#[entrait_impl(some_crate::Backend for MyImpl)] <--- proposed syntax
#[entrait_impl(dyn some_crate::Backend for MyImpl)] <--- dyn version
mod my_impl {
    fn foo(deps: &impl Whatever) -> i32 {
         42
    }
}

The functions inside this module need to match the backend interface. I think the compile errors will be good enough. The implementation is of course auto-generated:

pub struct MyImpl;

impl<T> Backend<T> for MyImpl
where
    Impl<T>: Whatever,
{
    fn foo(&self, _impl: &Impl<T>) -> i32 {
        foo(_impl)
    }
}

The Borrow part of the application could either be a Box or some enum. Avoiding allocation could look like this:

impl Borrow<dyn facade::Backend<Self>> for App {
    fn borrow(&self) -> &dyn Backend<Self> {
        match &self.facade_impl {
            AppFacadeImpl::My(my_impl) => my_impl,
            AppFacadeImpl::Other(other_impl) => other_impl,
        }
    }
}

Mocking traits with &impl and &mut impl parameters

Both functions give me compiler errrors for cargo build --tests. I use entrait with the unimock feature as dependency. Is there a way to make them work with entrait/unimock?

use entrait::entrait;

pub trait MyTrait {}

#[entrait(Foo, mock_api=FooMock)]
fn foo<D>(_: &D, _foo: &impl MyTrait) -> i32 {
    unimplemented!()
}

#[entrait(Bar, mock_api=BarMock)]
fn bar<D>(_: &D, _bar: &mut impl MyTrait) -> i32 {
    unimplemented!()
}

fn main() {}

Errors:

error[E0658]: `impl Trait` in type aliases is unstable
 --> src/main.rs:6:25
  |
6 | fn foo<D>(_: &D, _foo: &impl MyTrait) -> i32 {
  |                         ^^^^^^^^^^^^
  |
  = note: see issue #63063 <https://github.com/rust-lang/rust/issues/63063> for more information

error[E0658]: `impl Trait` in type aliases is unstable
  --> src/main.rs:11:29
   |
11 | fn bar<D>(_: &D, _bar: &mut impl MyTrait) -> i32 {
   |                             ^^^^^^^^^^^^
   |
   = note: see issue #63063 <https://github.com/rust-lang/rust/issues/63063> for more information

error: unconstrained opaque type
 --> src/main.rs:6:25
  |
6 | fn foo<D>(_: &D, _foo: &impl MyTrait) -> i32 {
  |                         ^^^^^^^^^^^^
  |
  = note: `Inputs` must be used in combination with a concrete type within the same impl

error[E0599]: the method `unimock_try_debug` exists for mutable reference `&mut _::<impl MockFn for BarMock>::Inputs<'_>::{opaque#0}`, but its trait bounds were not satisfied
  --> src/main.rs:10:1
   |
10 | #[entrait(Bar, mock_api=BarMock)]
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ method cannot be called due to unsatisfied trait bounds
   |
   = note: the following trait bounds were not satisfied:
           `&mut _::<impl MockFn for BarMock>::Inputs<'_>::{opaque#0}: Debug`
           which is required by `&mut _::<impl MockFn for BarMock>::Inputs<'_>::{opaque#0}: ProperDebug`
           `_::<impl MockFn for BarMock>::Inputs<'_>::{opaque#0}: Debug`
           which is required by `_::<impl MockFn for BarMock>::Inputs<'_>::{opaque#0}: ProperDebug`
   = note: this error originates in the attribute macro `::entrait::__unimock::unimock` (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0308]: mismatched types
   --> src/main.rs:6:18
    |
5   | #[entrait(Foo, mock_api=FooMock)]
    | --------------------------------- arguments to this function are incorrect
6   | fn foo<D>(_: &D, _foo: &impl MyTrait) -> i32 {
    |                  ^^^^   ------------
    |                  |      |
    |                  |      the expected opaque type
    |                  |      this type parameter
    |                  expected `&_::<impl MockFn for ...>::Inputs<'_>::{opaque#0}`, found `&impl MyTrait`
    |
    = note: expected reference `&_::<impl MockFn for FooMock>::Inputs<'_>::{opaque#0}`
               found reference `&impl MyTrait`
    = help: type parameters must be constrained to match other types
    = note: for more information, visit https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters
note: function defined here
   --> /Users/ander/.cargo/registry/src/github.com-1ecc6299db9ec823/unimock-0.4.12/src/macro_api.rs:209:8
    |
209 | pub fn eval<'u, 'i, F>(unimock: &'u Unimock, inputs: F::Inputs<'i>) -> Evaluation<'u, 'i, F>
    |        ^^^^

error[E0308]: mismatched types
   --> src/main.rs:11:18
    |
10  | #[entrait(Bar, mock_api=BarMock)]
    | --------------------------------- arguments to this function are incorrect
11  | fn bar<D>(_: &D, _bar: &mut impl MyTrait) -> i32 {
    |                  ^^^^       ------------
    |                  |          |
    |                  |          the expected opaque type
    |                  |          this type parameter
    |                  expected `&mut _::<impl MockFn for ...>::Inputs<'_>::{opaque#0}`, found `&mut impl MyTrait`
    |
    = note: expected mutable reference `&mut _::<impl MockFn for BarMock>::Inputs<'_>::{opaque#0}`
               found mutable reference `&mut impl MyTrait`
    = help: type parameters must be constrained to match other types
    = note: for more information, visit https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters
note: function defined here
   --> /Users/ander/.cargo/registry/src/github.com-1ecc6299db9ec823/unimock-0.4.12/src/macro_api.rs:209:8
    |
209 | pub fn eval<'u, 'i, F>(unimock: &'u Unimock, inputs: F::Inputs<'i>) -> Evaluation<'u, 'i, F>
    |        ^^^^

Some errors have detailed explanations: E0308, E0599, E0658.
For more information about an error, try `rustc --explain E0308`.

Multiple implementations of the same trait?

Hi,

I wonder if there's a way to have multiple implementations of the same trait?

Something like:

fn login(deps: &impl UserRepository, username: &str) {
   deps.user_exists(&username)
} 

fn postgres_user_exist(pool: &PgPool, username: &str) {
   // do pg stuff
}

fn inmemory_user_exist(data: &Map, username: &str) {
  // check in memory
}

And in the main.rs I would like to switch between postgres and in-memory based on, say, a config variable.

Thanks! ๐Ÿ™‚

`#[entrait_impl] mod {}` should just be replaced by an impl block

Instead of

#[entrait_impl]
mod some_mod {
    #[derive_impl(super::SomeTrait)]
    pub struct SomeType;
}

The macro should instead work with:

pub struct SomeType;

#[entrait_impl] // ?
impl SomeTrait for SomeType {
    fn foo(deps: &impl SomethingElse) {}
}

which would expand to:

impl SomeType { // <--- removed "SomeTrait for" from this location
    fn foo(deps: &impl SomethingElse) {}
}
impl<T> SomeTrait<T> for SomeType { // <--- move into this place
    fn foo(__impl: &Impl<T>) {
        Self::foo(__impl)
    }
}

Find a working design pattern for modelling transaction-like things

Database transactions require some kind of context value representing a session with the database. The transaction either succeeds or is cancelled in case of an error. In SQLx the transaction object requires a call to .commit() at the end.

Designing a DB abstraction this way requires an associated type representing the transaction object:

async fn some_db_op(deps: &impl Repository) {
     let mut tx = deps.transaction();
     deps.insert_something(some_value, &mut tx)?;
     tx.commit().await?;
     Ok(())
}

(the transaction object is likely different for each implementation of Repository).

I'm not completely sold on the need to explicitly call commit. What about taking advantage of the Result each db call returns? If it returns some kind of Result<Transaction<T>, Error>. That Ok context could be used somehow in subsequent calls. At the end of the operation chain, the T needs to be dug out from the transaction, and that's where the commit happens.

This issue represent exploring the design space around transaction modelling.

Generics in app state

I am continuing to work with your crate, it's really useful.

One thing where I am stuck: I have an object from an external crate that comes in as an &mut imp SomeTrait. It is a storage interface. Is there any way I can store this in the application state? I tried, but I get into trouble with Send and Sync traits.

Trait method cfgs aren't honored

Suppose I have something like:

#[cfg(feature = "baz")]
struct Baz;

#[entrait]
trait Foo {
    fn bar();
    #[cfg(feature = "baz")]
    fn baz() -> Baz;
}

You'll get an error saying Baz doesn't exist when you don't provide the baz feature flag - entrait doesn't seem to propagate cfgs to the output code.

Support for unimocking modular application by projecting implementations

Entrait apps have to be wrapped in ::implementation::Impl because of specialization issues. Traits get implemented for Impl<T> and Unimock.

Some applications need to be modular/composed of different crates, and linked together at the main crate. Imagine an app consisting of a library and a "final" app in the main crate:

// some lib crate
struct AppModule;
// main crate
struct App {
    app_module: Impl<AppModule>
}

In the main crate, we have some functions which want to call into entraited functions from the lib crate. But traits are not implemented for Impl<App>, but for Impl<AppModule>. So we need a way to get there, without naming the App. We only want to mention traits. We need access to all the traits implemented by Impl<AppModule> while still supporting unimock.

So we can imagine a trait for accessing the app module:

// main crate
trait GetAppModule {
    type Target: lib::Trait1 + lib::Trait2 + Send + Sync + 'static;
    
    fn get_app_module(&self) -> &Self::Target;
}

(this cannot be a generic trait because of custom bounds on Target)

This trait gets implemented for Impl<App> and Unimock:

impl GetAppModule for Impl<App> {
    type Target = Impl<AppModule>;
    
    fn get_app_module(&self) -> &Self::Target {
        &self.app_module
    }
}

impl GetAppModule for Unimock {
    type Target = Unimock;
    
    fn get_app_module(&self) -> &Self::Target {
        self
    }
}

This would work fine and we can then write things like:

fn do_with_module(deps: &impl GetAppModule) {
    deps.get_app_module().some_lib_function();
}

But it's a lot of boilerplate to generate these impls, and I'd like to make it easier to express this relationship using a macro.

A key point is that we need to repeat all the lib traits as bounds for GetAppModule, but it would be better if the lib exported its "public API" traits. It could do that by exporting a "meta trait" that inherits from all its public traits:

// lib
pub trait AppModuleMeta: Trait1, Trait2, Trait2 {}

impl AppModuleMeta for Impl<AppModule> {}
impl AppModuleMeta for Unimock {}

Entrait 0.5 plans

Change default behaviour for mockability

In entrait 0.5, I want more focus on dependency inversion and less focus on mocking out every single function.

The main advantages of the entrait pattern for functions are:

  1. Abstracting away the application, all business logic is available as extension methods on Impl<T>
  2. Each function only specifies its direct dependencies. The trait engine automatically takes care of verifying that transitive dependencies are fulfilled.

Support for mocking every little function when unimock is enabled is potentially generating a lot more code than necessary.
Instead, by default, we could try to generate a very simple impl<T> Trait for T where T: Dependencies that could cover both Impl<T> and mocked environments. Mocking a simple function will become opt-in, with something like #[entrait(Foo, mock)]).

Mock support should be enabled by default on inverted dependencies - the ones that have no local implementation.

Rename async-trait to boxed-futures.

async-trait is just an implementation detail on the way to get boxed futures, and this does not need to be exposed.

## Investigate trait redesign
#11

Dependency issues when using unimock

Hi there, a couple of issues with dependencies I noticed. Thanks for this elegant library, I searched high and low for a solid dependency injection in Rust and this is the only one I think makes sense.

I use cargo build --tests to build.

Using the unimock option for the entrait macro

Given this cargo.toml

[package]
name = "entrait-issue"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
entrait = "0.5.3"

and this main.rs

use entrait::entrait;

#[entrait(Foo, unimock, mock_api=FooMock)]
fn foo<D>(_: &D) -> i32 {
    unimplemented!()
}

#[entrait(MyMod, unimock, mock_api=mock)]
mod my_mod {
    pub fn bar<D>(_: &D) -> i32 {
        unimplemented!()
    }
}

fn main(){}

I get the error error[E0433]: failed to resolve: could not find `__unimock` in `entrait

Using the unimock feature

When I switch to the unimock feature in cargo.toml things get better.

[package]
name = "entrait-issue"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
entrait = { version = "0.5.3", features = ["unimock"] }

[dev-dependencies]
unimock = "0.5.3"

Here is the main.rs for this case

use entrait::entrait;

#[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()
}

fn main() {}

#[cfg(test)]
mod tests {
    use super::*;
    use unimock::*;    

    #[test]
    fn test() {
        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));
    }
}

Still, the compiler complains no method named `each_call` found for struct `FooMock` in the current scope

What I did to work around this problem was to import entrait::__unimock::* instead of use unimock::*;.

Am I overlooking something (quite likely) or are these bugs?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.