Defined components

One of the issues with using #[component] and #[subcomponent] is that modules still has to be listed, which means anything using the component will depend on everything. The component is also generated in the crate, so other crates depending on it is not able to expand the dependency graph, which makes multibindings less useful. Additionally, unit tests often needs a different set of modules, so the whole component has to be redefined.

In a large project there maybe tens and even hundreds of modules, and this will become very difficult to manage.

Instead of #[component] and #[subcomponent], Lockjaw also provides #[define_component] and#[define_subcomponent] which automatically collects modules from the entire build dependency tree, so they no longer need to be manually installed.

Root crate

When using #[define_component] the component is not immediately generated, since modules from crates depending on the current crate may still want to add to the dependency graph.

Lockjaw needs to know which crate is the root crate that is not depended on by other crates, which is done by passing the root identifier to the epilogue!() macro

epilogue!(root);

Typically the root crate is the binary. Libraries can also be the root crate but that is probably not a good idea. Lockjaw will fail compilation if a crate using it depends on another crate that is a root crate.

Automatically installing modules

#[modules] can be automatically installed in a component by using the install_in metadata. The metadata takes a path to a #[define_component] trait. Alternatively, it can also be a path to Singleton, which means it should be installed in every #[define_component] but not #[define_subcomponent].

Such modules cannot have fields.

#[module(install_in: MyComponent)]
impl MyModule {
    #[provides]
    pub fn provide_string() -> String {
        "string".to_owned()
    }
}

#[define_component]
pub trait MyComponent {
    fn string(&self) -> String;
}

Entry points

Ideally a component should only be used at the program's entry point, and rest of the program should all use dependency injection, instead of trying to pass the component around. However sometimes callbacks will be called from non-injected context, and the user will need to reach back into the component.

These kinds of usage will cause the requesting methods in a component to bloat, and add redundant dependencies or cycle issues to everyone that uses the component.

With #[define_component] , #[entry_point] can be used.

An #[entry_point] has binding requesting methods just like a component. The install_in metadata needs to be used to install the #[entry_point] in a component. Once installed, the

<dyn FooEntryPoint>::get(component : &dyn FooComponent) -> &dyn FooEntryPoint

method can be used to cast the opaque component into the entry point, and access the dependency graph.

#[entry_point(install_in: MyComponent)]
pub trait MyEntryPoint {
    fn i(&self) -> i32;
}

#[define_component]
pub trait MyComponent {}

#[test]
pub fn main() {
    let component: Box<dyn MyComponent> = <dyn MyComponent>::new();

    assert_eq!(<dyn MyEntryPoint>::get(component.as_ref()).i(), 42)
}

Testing with #[define_component]

While compiling tests, Lockjaw gathers install_in modules only from the [dev-dependencies] section of Cargo.toml instead of the regular [dependencies], even though [dev-dependencies] inherits [dependencies]. This is due to tests often have conflicting modules with prod code. any prod modules that need to be used in tests has to be relisted again in the [dev-dependencies] section.