Injecting Objects

Lockjaw can create objects for you, but you need to let lockjaw know how to create the object and what is needed to create them. These recipes for object creation is called bindings, which forms nodes in a dependency graph, and add edges to other bindings it depends on.

The most simple binding is constructor injection binding.

Constructor injection

A binding can be created for a struct by using a constructor method, which is a static method associated to the struct that returns a new instance. The field constructor ( Foo {a : 0, ...}) is not directly used by Lockjaw since methods are more expressive when a none injected field needs a default value or when transformations are needed on the input.

A struct can be made injectable by marking a struct impl block with the #[injectable] attribute, and then mark the constructor method as #[inject].

struct Foo {}

#[injectable]
impl Foo {
    #[inject]
    pub fn new() -> Foo {
        Foo {}
    }
}

Now Lockjaw understands when trying to create Foo, it should call Foo::new().

Note that since it is an associated method, constructor injection only works on a type you own ( you can actually change its implementation). For foreign types like imported crates a different method will be discussed in the providing objects chapter.

Constructor dependencies

To create an object, it may many need objects of other types. This is called dependencies. In this example, Bar depends on having an instance of Foo to be created.

In constructor injections, dependencies are listed with its parameters. Lockjaw will try to use available bindings to create all arguments and pass them to the constructor. If a parameter does not have a binding, Lockjaw will fail compilation with missing bindings.

Note that since we are not asking Lockjaw to actually create the object yet, binding validation won't be performed. Lockjaw is assuming there are some bindings else where it does not know about yet.

struct Bar {
    foo: Foo,
    i: i32,
}

#[injectable]
impl Bar {
    #[inject]
    pub fn new(foo: Foo) -> Bar {
        Bar { foo, i: 42 }
    }
}

If the struct has other fields that can be initialized without injection, like i, it can be directly assigned. If the object needs a runtime value (for example, if i needs to be assigned by the caller with a user input), then factories will be needed, which will be discussed later.

Manual injection

For a moment let's forget about Lockjaw, and try to do dependency injection manually. With the binding information we have we can write a factory that can create the objects we just defined:

struct Factory {}

impl Factory {
    pub fn create_foo(&self) -> Foo {
        Foo::new()
    }

    pub fn create_bar(&self) -> Bar {
        Bar::new(self.create_foo())
    }
}

#[test]
fn test() {
    let factory = Factory {};

    let _foo = factory.create_foo();
    let _bar = factory.create_bar();
}

Note that there is one method for each binding, only taking &self and returning the binding type. Inside the method it calls the constructor method we just marked, and calls other binding methods to generate the argument.

The factory is an object instead of just methods, since it might need to carry states in the future (For example, returning a reference to a shared object owned by the factory.)

Writing the factory by hand gets complicated and boring fast. In the next chapter we will ask Lockjaw to generate it.

Source of this chapter