audunhalland / entrait

Loosely coupled Rust application design made easy
86 stars 3 forks source link

Support for unimocking modular application by projecting implementations #1

Closed audunhalland closed 2 years ago

audunhalland commented 2 years ago

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 {}
audunhalland commented 2 years ago

Example of the pattern implemented, but without macros, is in https://github.com/audunhalland/rust-realworld-ioc/.

audunhalland commented 2 years ago

Fixed by https://github.com/audunhalland/entrait/commit/7b421df82597787a387b5024df3f483f942792f1, using a completely different and better approach.