Binding traits
#[provides]
trait
A trait
can be provided using
the #[provides]
binding.
#[provides]
pub fn provide_i32_maker(impl_: I32MakerImpl) -> Box<dyn I32Maker> {
Box::new(impl_)
}
However, Lockjaw is going to be particular when trying to request a trait object from the dependency
graph. The concrete implementation of the trait may contain reference to the component,
but ideally this is not something the consumer of the trait should care about, so Lockjaw enforces
that any trait it provides must not outlive the component. The worst case 'ComponentLifetime
is
assumed, so consumers don't have to change when it actually happens.
The Box
returned by the component must be bound by the component's lifetime(same as self
).
#[component(modules: [MyModule])]
trait ProvideComponent {
fn i32_maker(&'_ self) -> Box<dyn I32Maker + '_>;
}
#[binds]
trait
While #[provides]
kind of works, binding an implementation to a trait
interface is a common
operation so Lockjaw has
the #[binds]
attribute
to make them easier to use.
For an interface and an implementation:
pub trait Logger {
fn log(&self, msg: &str);
}
struct StdoutLogger;
#[injectable]
impl StdoutLogger {
#[inject]
pub fn new() -> StdoutLogger {
StdoutLogger
}
}
impl Logger for StdoutLogger {
fn log(&self, msg: &str) {
println!("{}", msg);
}
}
#[binds]
can be used to create binding that says "when the Logger
interface is needed,
use StdoutLogger
as the actual implementation":
#[binds]
pub fn bind_stdout_logger(_impl: StdoutLogger) -> Cl<dyn Logger> {}
The method body must be empty, as Lockjaw will replace it.
TheCl
in the return type means
Component lifetimed, which is a wrapper around a type forcing it not outlive the component.
Having this wrapper makes it easier for the compiler to deduce the lifetime.
With the binding defined the Logger
can now be used by other classes, without caring about the
actual implementation.
pub struct Greeter<'component> {
logger: Cl<'component, dyn Logger>,
}
#[injectable]
impl Greeter<'_> {
#[inject]
pub fn new(logger: Cl<dyn Logger>) -> Greeter {
Greeter { logger }
}
pub fn greet(&self) {
self.logger.log("helloworld!");
}
}
Note that Logger
still has to be injected as Cl<dyn Logger>
, and Greeter
is also bound by the
lifetime of the component.
Unit testing with dependency injection
StdoutLogger
writes its output straight to the console, so it is hard to verify Greeter
actually
sends the correct thing. While we can give StdoutLogger
special apis to memorize what it logs and
give access to tests, having test code in prod is generally bad practice.
Instead we can use dependency injection to replace the environment Greeter
runs in. We can create
a TestLogger
that writes the logs to memory and can read it later, bind it to the Logger
with a
module, and install the module in a component for test that has all test bindings. We are than able
to test Greeter
without adding test code to the Greeter
itself:
#[cfg(test)]
pub mod testing {
use crate::{Greeter, Logger};
use lockjaw::{component, injectable, module, Cl};
static mut MESSAGES: Vec<String> = Vec::new();
pub struct TestLogger;
#[injectable]
impl TestLogger {
#[inject]
pub fn new() -> TestLogger {
TestLogger
}
pub fn get_messages(&self) -> Vec<String> {
unsafe { MESSAGES.clone() }
}
}
impl Logger for TestLogger {
fn log(&self, msg: &str) {
unsafe { MESSAGES.push(msg.to_owned()) }
}
}
pub struct TestModule;
#[module]
impl TestModule {
#[binds]
pub fn bind_test_logger(_impl: TestLogger) -> Cl<dyn Logger> {}
}
#[component(modules: [TestModule])]
pub trait TestComponent {
fn greeter(&self) -> Greeter;
fn test_logger(&self) -> TestLogger;
}
#[test]
fn test() {
let component: Box<dyn TestComponent> = <dyn TestComponent>::build();
component.greeter().greet();
assert_eq!(
component.test_logger().get_messages(),
vec!["helloworld!".to_owned()]
);
}
}
Generally, a library should also provide a test implementation and a module that binds the test implementation. The consumer of the library can then test by installing the test module instead of the real module. This allows test infrastructure to easily be shared. Some kind of test scaffolding can also be created to auto generate the component and inject objects into tests, but that is out of scope for lockjaw itself.
Note that in the TestLogger
MESSAGES
is a static mutable, since log()
and get_messages()
is
going to be called on different instances of TestLogger
. This is unsafe
and bad, so in the next
chapter we will discuss how to handle this by forcing a single instance of TestLogger
to be shared
among everything that uses it.
Source of this chapter