Skip to content

Releases: commonsensesoftware/more-rs-di

3.0.0

24 Nov 22:50
Compare
Choose a tag to compare

This release represents a significant improvement in new and existing features.

Features

  • Mutable services
  • Keyed services
  • Improved proc macro attribute injection
  • ScopedServiceProvider allows for injecting a scoped ServiceProvider declaratively
  • Format (fmt) feature for printing and troubleshooting service mappings
  • Aliases (alias) feature to allow user-defined injected type aliases
  • User guide
  • Relaxed 'static lifetime requirements (where possible)
  • Lazy initialization functions (ex: for testing)

Improved #[injectable]

The #[injectable] attribute proc macro can now also be applied to:

  • A struct definition (not just the impl for a struct)
  • A tuple struct
  • A generic struct or tuple struct
  • A struct constructor can now use impl Iterator<Item> for any supported types
  • ServiceProvider::create_scope() is injected for ScopedServiceProvider

The following illustrates a few examples.

use di::*;
use std::collections::HashMap;

trait Worker {
    fn do_work(&self);
}

// apply directly to struct definition
#[injectable(Worker)]
struct WorkerImpl;

impl Worker for WorkerImpl {
    fn do_work(&self) { }
}

// apply to tuple struct
#[injectable]
struct JobSite(Vec<Ref<dyn Worker>>);

// declaratively inject ServiceProvider.create_scope()
#[injectable]
struct RequestContext(ScopedServiceProvider);

trait Feature {
    fn name(&self) -> &str;
}

struct Component {
    features: HashSet<String, Ref<dyn Feature>>,
}

// inject sequence of services without allocating a Vec
#[injectable]
impl Component {
    fn new(features: impl Iterator<Item = Ref<dyn Feature>>) -> Self {
        Self {
            features: features.map(|f| (f.name().into(), f.clone())).collect()
        }
    }
}

Mutable Services

Creating mutable services was previously possible to create, but required additional work by developers. Mutable services are now supported as a first class concept to simplify usage. Services are wrapped in RefCell by default or in RwLock when the async feature is activated.

let mut provider =
    ServiceCollection::new()
        .add(WokerImpl::transient().as_mut())
        .build_provider()
        .unwrap();

// RefMut<dyn Worker> == Ref<RefCell<dyn Worker> == Rc<RefCell<dyn Worker>>
let worker = provider.get_required_mut::<dyn Worker>();

Keyed Services

The need for keyed services should be an infrequent one, but the workaround to make it work in the past was painful. Keyed services now have formal support. Unlikely other DI frameworks, the key must be a type, which provides clarity, uniqueness, and supplants magic strings.

mod key {
    struct Thing1;
    struct Thing2;
}

trait Thing {}

#[injectable(Thing)]
struct FirstThing;

impl Thing for FirstThing {}

#[injectable(Thing)]
struct SecondThing;

impl Thing for SecondThing {}

#[injectable]
struct CatInTheHat {
    thing1: KeyedRef<key::Thing1, dyn Thing>,
    thing2: KeyedRef<key::Thing1, dyn Thing>,
}

fn main() {
    let provider =
        ServiceCollection::new()
            .add(Thing1::transient().with_key<key::Thing1>())
            .add(Thing2::transient().with_key<key::Thing2>())
            .add(CatInTheHat::transient())
            .build_provider()
            .unwrap();

    let cat_in_the_hat = provider.get_required::<CatInTheHat>();
}

For more details and examples, see the user guide.

Breaking Changes

Every effort is made to avoid breaking changes, but sometimes they are inevitable. Great care was taken to minimize the impact of breaking changes and make them source-compatible when possible.

Injectable Trait

Injectable::inject now returns InjectBuilder instead of ServiceDescriptor. This change is required to enable a fluent builder to optionally configure a key and/or mutability. InjectBuilder implements Into<ServiceDescriptor> and ServiceCollection::add now accepts T: Into<ServiceDescriptor>, which will maintain source compatibility. Unless you implemented the Injectable trait yourself, this change should be transparent.

ServicedDescriptorBuilder Struct

The ServicedDescriptorBuilder struct has been moved into the builder feature. InjectBuilder servers the same purpose and ServicedDescriptorBuilder will typically only be used now if you also use the other builder functions.

ServiceRef Type Alias

The ServiceRef<T> type alias has been replaced with Ref<T> to make it more succinct. To avoid excessive code churn, you can redefine the old alias and it will work without any additional configuration; for example: type ServiceRef<T> = di::Ref<T>;.

You may not like either the old or new type alias name, therefore, the new alias feature allows you to define whichever alias you like via the crate dependency configuration. You might want to use Svc<T> instead of Ref<T>. This can be achieved with the simple configuration:

[dependencies]
more-di = { version = "3.0", features = ["alias"], aliases { ref = "Svc" } }

This feature is only necessary if you are using #[injectable] so that it recognizes the injected types; otherwise, it has no effect within the library. The user guide has additional details on how to configure custom type aliases.

2.1.0

09 Feb 00:24
Compare
Choose a tag to compare

Features

  • #[injectable] now supports implementing Injectable for generic structs
  • Lazy-initialization is supported using Lazy<T> when the lazy feature is enabled
    • The updated README outlines how to use the feature
    • Lazy<T> is supported with the #[injectable] macro attribute

Breaking Changes

None

2.0.0

05 Feb 22:05
Compare
Choose a tag to compare

The 2.0 release is largely the same as the 1.0 release, but validation is now supported and enforced on ServiceCollection::build_provider(). This change updates the return type, which is a breaking change.

Features

  • Validation

Validation

The consumers of a ServiceProvider expect that it is correctly configured and ready for use. There are edge cases, however, which could lead to invalid configuration, including:

  • A required, dependent service that has not been registered
  • A circular dependency, which will trigger a stack overflow
  • A singleton service with a scoped service dependency

Intrinsic validation has been added to ensure this cannot happen. The build_provider() function will return Result<ServiceProvider, ValidationError>, which will either contain a valid ServiceProvider or a ValidationError that will detail all of the errors. From that point forward, the ServiceProvider will be considered semantically correct and safe to use. The same validation process can also be invoked imperatively on-demand by using the di::validate function.

The ServiceDescriptorBuilder cannot automatically determine the dependencies your service may require. This means that validation is an explicit, opt-in capability. If you do not configure any dependencies for a ServiceDescriptor, then no validation will occur. ServiceDescriptorBuilder now has a depends_on method to add a ServiceDependency and ServiceDescriptor::dependencies accesses the configured dependencies:

pub enum ServiceCardinality {
    ZeroOrOne,
    ExactlyOne,
    ZeroOrMore,
}

pub struct ServiceDependency {
    injected_type: Type,
    cardinality: ServiceCardinality,
}

To make configuring services more succinct, 3 new utility functions have been added to the builder feature:

  • exactly_one - creates a ServiceDependency with a cardinality of 1:1
  • zero_or_one - creates a ServiceDependency with a cardinality of 0:1
  • zero_or_more - creates a ServiceDependency with a cardinality of 0:*

This enables the ability to define dependent service types and their cardinality to the target service.

fn main() {
    let mut services = ServiceCollection::new();

    services.add(
        singleton::<dyn Foo, FooImpl>()
        .from(|_| Rc::new(FooImpl::default())));
    services.add(
        transient::<dyn Bar, BarImpl>()
        .depends_on(exactly_one::<dyn Foo>())
        .from(|sp| Rc::new(BarImpl::new(sp.get_required::<dyn Foo>()))));

    match services.build_provider() {
        Ok(provider) => {
            let bar = provider.get_required::<dyn Bar>();
            assert_eq!(&bar.speak(), "foo bar");
        },
        Err(error) => {
            println!("The service configuration is invalid.\n{}", &error.to_string());
        }
    }
}

When you use the inject feature and the #[injectable] attribute, validation is automatically generated.

Breaking Changes

  • ServiceCollection::build_provider() now has the signature pub fn build_provider(&self) -> Result<ServiceProvider, ValidationError>