AzureMarker / shaku

Compile Time Dependency lnjection Library for Rust
https://docs.rs/shaku
Apache License 2.0
429 stars 22 forks source link

documentation : How to use shaku with a constructor #13

Closed pgokul closed 3 years ago

pgokul commented 3 years ago

I am new to Rust and and am unable to figure out how to use a constructor with shaku dependency injection.

trait IConfig: Interface {}
#[derive(Component)]
#[shaku(interface = IConfig)]
struct IConfigImpl {
    url: String,
}
impl IConfig for IConfigImpl {}

trait IService: Interface {
    fn register_user(&self, email: &str, pwd: &str) -> String;
}
#[derive(Component)]
#[shaku(interface = IService)]
struct IServiceImpl {
    #[shaku(inject)]
    config: Arc<dyn IConfig>,
    usr_store: RwLock<HashMap<String, String>>,
}

impl IServiceImpl {
    pub fn new() -> IServiceImpl {
        IServiceImpl {
            usr_store: RwLock::new(HashMap::new()),
        }
    }
}

I am getting an error, ' missing field config in initializer of IServiceImpl

Adding default throws an error "the trait std::default::Default is not implemented for dyn IConfig::IConfig"

How can i use a constructor where certain fields of the struct are initialized and certain others are injected.

AzureMarker commented 3 years ago

The Component derive automatically generates a parameters struct (ex. IServiceImplParameters) which can be used to supply all of the non-injected properties during module creation. Here's the section of the documentation which talks about this: https://docs.rs/shaku/0.5.0/shaku/guide/index.html#passing-parameters

If the type of a property implements Default, you don't need to manually pass it in to get the default value for the type. For example, your IServiceImpl seems to only need a default value for RwLock<HashMap<String, String>>, so you don't need to use with_component_parameters on it.

In your example, it looks like you also need to declare a module to hold your components. Your code might end up looking like this:

module! {
    MyModule {
        components = [IConfigImpl, IServiceImpl],
        providers = []
    }
}

fn main() {
    let my_mod = MyModule::builder()
        .with_component_parameters::<IConfigImpl>(IConfigImplParameters {
            url: "insert-url-here".to_string()
        })
        .build();
}

Note: In Rust, traits and structs are normally named without a leading "I". For example, your IService and IServiceImpl would be Service and ServiceImpl.

pgokul commented 3 years ago

Thanks @Mcat12 . Would this mean that the code which creates the modules needs to be aware of the internal details of the component to be created if the component has some properties which require non trivial construction. This would mean the internal details of the object would be leaking into the module creators. Is support planned/available for something like @PostConstruct annotation in java JSR 250.

AzureMarker commented 3 years ago

You can also add an annotation to the property to provide a default, which would move that code closer to the implementation:

#[derive(Component)]
#[shaku(interface = IConfig)]
struct IConfigImpl {
    #[shaku(default = "insert-url-here".to_string())]
    url: String,
}

The default field in the annotation is an expression, so it can be a function call to more complex code.

Alternatively, you could write the Component implementation by hand to have full control over how the service gets created:

struct IServiceImpl {
    config: Arc<dyn IConfig>,
    usr_store: RwLock<HashMap<String, String>>,
}

impl<M: Module + HasComponent<dyn IConfig>> Component<M> for IServiceImpl {
    type Interface = dyn IService;
    type Parameters = ();

    fn build(context: &mut ModuleBuildContext<M>, params: Self::Parameters) -> Box<Self::Interface> {
        // Insert initialization code here
        let usr_store = RwLock::default();

        Box::new(IServiceImpl {
            config: M::build_component(context),
            usr_store
        })
    }
}

Note that the Component derive and shaku annotations are removed, since the impl is written manually. Manually writing the Component impl isn't too complicated, so I think it removes the need for a special @PostConstruct-like function.

pgokul commented 3 years ago

Writing the Component implementation by hand seems to be the cleanest way to do this. I will try this.

AzureMarker commented 3 years ago

Thanks for the question!