Crate lockjaw

Source
Expand description

§Lockjaw

Lockjaw is a fully static, compile-time dependency injection framework for Rust inspired by Dagger. It is also what you get when jabbed by a rusty dagger.

Features:

  • Compile time dependency resolution
    • Lockjaw makes sure all dependencies are fulfilled at compile time. The code will fail to compile if a dependency is missing, there are duplicated bindings for the same type, or if the dependency graph has cycles. There will be no runtime errors which are harder to detect.
  • Relatively readable diagnostic messages.
    • When a dependency is missing Lockjaw tries to tell you why it is even in the dependency graph, and where the dependency cycle is.
  • Cross-crate injection
    • Lockjaw is designed to be used across crates. Clients are able to inject bindings provided by libraries if they also use Lockjaw.
  • Minimal generated code surface
    • While procedural macros are utilized heavily by Lockjaw, it avoids directly modifying the code the attributes macros are placed on. Only a few generated methods are visible to the user. This is especially important since most Rust IDEs today does not understand the output of procedural macros, a few extra type hints on let expressions is enough to make autocomplete functional.
  • Optional binding, Multibinding, and generated components for plugin systems
    • Lockjaw allows inversion of control between library crates and their user. A library is able to define hooks for clients that depends on the library to inject. This is especially useful to test multiple clients using a library in isolation.

See user guide for more information.

Example:

use lockjaw::*;
use std::ops::Add;

lockjaw::prologue!("src/lib.rs");

struct GreetCounter {
    counter: ::std::cell::RefCell<i32>
}

// Allow GreetCounter to be created in the dependency graph. These bindings are available anywhere.
#[injectable]
impl GreetCounter {
    // Marks a method as the inject constructor. Lockjaw will call this to create the object.
    #[inject]
    pub fn new() -> Self {
        Self { counter: std::cell::RefCell::new(0) }
    }
}

impl GreetCounter {
    pub fn increment(&self) -> i32 {
        let mut m = self.counter.borrow_mut();
        *m = m.add(1);
        m.clone()
    }
}

pub trait Greeter {
    fn greet(&self) -> String;
}

struct GreeterImpl {
    greet_counter: crate::GreetCounter,
    phrase: String
}

#[injectable]
impl GreeterImpl {
    // Lockjaw will call this with other injectable objects provided.
    #[inject]
    pub fn new(greet_counter: GreetCounter, phrase: String) -> Self {
        Self {
            greet_counter,
            phrase
        }
    }
}

impl Greeter for GreeterImpl {
    fn greet(&self) -> String {
        format!("{} {}", self.phrase, self.greet_counter.increment())
    }
}

// Declare a module so we can do special bindings. These bindings are only available if the
// component installs the module, so different bindings can be used based on the situation.
struct MyModule {}
#[module]
impl MyModule {
    // When ever someone needs a Greeter, use GreeterImpl as the actual implementation 
    #[binds]
    pub fn bind_greeter(_impl: crate::GreeterImpl) -> Cl<dyn Greeter> {}

    // Called when a String is requested
    #[provides]
    pub fn provide_string() -> String {
        "helloworld".to_owned()
    }
}

// Components stitch modules and injectables together into a dependency graph, and can create
// objects in the graph. The component installs modules listed in `modules`
#[component(modules: MyModule)]
trait MyComponent {
    // Allows creating a greeter with the component. The created object has the lifetime of the
    // component
    fn greeter(&self) -> Cl<dyn Greeter>;
}

pub fn main() {
    // Creates the component
    let component = <dyn MyComponent>::new();
    // Creates a greeter.
    let greeter = component.greeter();
    assert_eq!(greeter.greet(), "helloworld 1");
    // Internal states of the greeter is kept.
    assert_eq!(greeter.greet(), "helloworld 2");

    // A new greeter has a new independent set of injected objects.
    assert_eq!(component.greeter().greet(), "helloworld 1");
}
// called at the binary to perform validation and code generation
epilogue!();

A more complicated game example can be found at https://github.com/azureblaze/lockjaw/tree/main/example_game

§Comparison with Dagger

Lockjaw Aims for feature parity with Dagger and uses very similar APIs. If you have used Dagger before, lockjaw should feel familiar.

§Disclaimer

This is not an officially supported Google product.

Lockjaw is currently in early development and all APIs are subjected to changes. Some feature are also implemented in a hacky way. Use at your own risk.

Modules§

component_attributes
Additional nested attributes for items under a #[component]
injectable_attributes
Additional nested attributes for items under a #[injectable]
module_attributes
Additional nested attributes for items under a #[module]

Macros§

epilogue
Resolves the dependency graph and generate component code. Must be called in the crate root (main.rs or lib.rs if it has tests) outside any mod/functions. Libraries do not need to call epilogue!()

Structs§

Lazy
Wraps a binding so it can be lazily created.
Provider
Creates a binding on demand

Enums§

Cl
“Component Lifetime”. Wrapper around an injection that may be scoped(owned by the component) or freestanding(owned by the item injecting it). Deref to access the content.

Traits§

Singleton
Represents “any component” which can be used with the scope and install_in metadata.

Functions§

build_script
Function that must be called inside the cargo build script to set up the lockjaw environment in a binary crate.

Attribute Macros§

builder_modules
Annotates a struct that contains modules instances to be installed in a component with the builder_modules field. If a module contains fields, it cannot be auto generated and must be explicitly provided to COMPONENT.build()
component
Annotates a trait that composes the dependency graph and provides items in the graph (An “injector”) .
component_visible
Annotates a non-public [injectable] struct, a #[module] struct, or a trait used by components so their implementation can be generated.
define_component
Generates a #[component] at the root of the binary.
define_subcomponent
Generates a #[subcomponent] at the root of the binary, similar to #[define_component]
entry_point
Annotates a trait to provide bindings access to an opaque #[define_component] /#[define_subcomponent] component.
injectable
Annotates a struct impl that can be provided to the dependency graph.
module
Annotates a impl block that defines the bindings.
qualifier
Annotates a struct to declare a binding qualifier.
subcomponent
Annotates a trait that composes a sub-dependency graph, which has additional bindings/scoped bindings, and can also access bindings from the parent #[component]/ #[subcomponent].