Closed eckyputrady closed 2 years ago
Hmm, interesting! I think #[entrait] Trait {}
can be used for this. I managed to write the following thing, which works:
use entrait::*;
#[entrait(Login)]
fn login(deps: &impl UserRepository, username: &str) {
deps.user_exists(&username)
}
#[entrait]
trait UserRepository {
fn user_exists(&self, username: &str);
}
struct PgApp;
struct InMemoryApp;
impl UserRepository for PgApp {
fn user_exists(&self, username: &str) {}
}
impl UserRepository for InMemoryApp {
fn user_exists(&self, username: &str) {}
}
fn test_mocked() {
login(&unimock::mock(None), "username");
}
fn test_real() {
// login(&Impl::new(PgApp), "username");
// login(&Impl::new(InMemoryApp), "username");
Impl::new(PgApp).login("username");
Impl::new(InMemoryApp).login("username");
}
edit: Changed the Impl examples to call the Login trait method instead of the function
A problem with my first suggestion is that all of the application code will be duplicated (monomorphized) for PgApp
and InMemoryApp
. If you want to use a dyn
solution instead, maybe something like this works:
use entrait::*;
#[entrait(Login)]
fn login(deps: &impl GetUserRepository, username: &str) {
deps.get_user_repository().user_exists(username);
}
#[entrait(GetUserRepository)]
fn get_user_repository(app: &App) -> &dyn UserRepository {
app.user_repository.as_ref()
}
#[entrait]
pub trait UserRepository {
fn user_exists(&self, username: &str);
}
struct App {
user_repository: Box<dyn UserRepository + Sync>,
}
struct PgUserRepository {}
impl UserRepository for PgUserRepository {
fn user_exists(&self, username: &str) {}
}
fn test_real() {
let app = Impl::new(App {
user_repository: Box::new(PgUserRepository {}),
});
app.login("username");
}
edit: Well, the problem with this is that now it's not as easy to mock calls to UserRepository
, because those methods are not direct dependencies any more. 🤔
I'm thinking of a feature that might simplify the scenario of dynamic leaf dependencies.
There's this trait:
trait UserRepository: 'static {
fn user_exists(&self, username: &str) -> bool;
}
Using the #[entrait]
attribute on this trait will generate an implementation for Impl<T>
that requires that T
also implements the trait. But that's not always what's wanted. In any case, we want to delegate som call from Impl<T>
into some type that contains the real implementation. But going through T: Trait
is evidently not always the best strategy.
So I'm thinking about a way to delegate by dynamically borrowing the implementation:
impl<T> UserRepository for Impl<T>
where
T: Borrow<dyn UserRepository> + 'static,
{
fn user_exists(&self, username: &str) {
self.as_ref().borrow().user_exists(username);
}
}
This implementation could be generated by e.g. #[entrait(dyn_borrow)]
:
#[entrait(dyn_borrow)]
trait UserRepository: 'static {
fn user_exists(&self, username: &str) -> bool;
}
The way to make it work for some application (this code must be written manually):
struct App {
user_repository: Box<dyn UserRepository + Sync>,
}
impl std::borrow::Borrow<dyn UserRepository> for App {
fn borrow(&self) -> &dyn UserRepository {
self.user_repository.as_ref()
}
}
struct PgUserRepository;
impl UserRepository for PgUserRepository {
fn user_exists(&self, username: &str) {}
}
Implemented in https://github.com/audunhalland/entrait/commit/784a1249f3077a586465c9c2655e82a0fac9c201, I hope this is sufficient for your use case!
Released version 0.4.2 with this feature. Closing bug. Feel free to reopen if the new feature is still not adequate enough for your use case!
This is awesome. Thanks @audunhalland !
See also https://github.com/audunhalland/entrait/issues/8 for an idea of how this could be improved on even more. I.e. not require things like this to be "leaf dependencies" - and to be able to do real dependency inversion. Which would mean that you can define a trait in an upstream crate ("inner layer"), implement it in an intermediate layer, and link things together at the main crate layer.
Hi,
I wonder if there's a way to have multiple implementations of the same trait?
Something like:
And in the main.rs I would like to switch between postgres and in-memory based on, say, a config variable.
Thanks! :slightly_smiling_face: