hosting-di 0.1.0

Service collection, provider and scopes for the hosting crate
Documentation
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]

use hosting_di::{ResolveError, ServiceCollection};
use std::any::type_name;
use std::io;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};

struct Config {
    port: u16,
}

struct Client {
    port: u16,
}

struct Missing;

#[test]
fn singleton_factory_runs_once_and_resolves_dependencies() {
    let calls = Arc::new(AtomicUsize::new(0));
    let counter = Arc::clone(&calls);
    let mut services = ServiceCollection::new();
    services
        .add_singleton_instance(Config { port: 8080 })
        .add_singleton_with(move |r| {
            counter.fetch_add(1, SeqCst);
            Ok(Client {
                port: r.resolve::<Config>()?.port,
            })
        });
    let provider = services.build().unwrap();

    let first = provider.resolve::<Client>().unwrap();
    let second = provider.resolve::<Client>().unwrap();

    assert_eq!(first.port, 8080);
    assert!(Arc::ptr_eq(&first, &second));
    assert_eq!(calls.load(SeqCst), 1);
}

#[test]
fn transient_factory_runs_per_resolve() {
    let mut services = ServiceCollection::new();
    services.add_transient_with(|_| Ok(Client { port: 1 }));
    let provider = services.build().unwrap();

    let first = provider.resolve::<Client>().unwrap();
    let second = provider.resolve::<Client>().unwrap();

    assert!(!Arc::ptr_eq(&first, &second));
}

#[test]
fn scoped_factory_runs_once_per_scope() {
    let mut services = ServiceCollection::new();
    services.add_scoped_with(|_| Ok(Client { port: 1 }));
    let provider = services.build().unwrap();
    let scope = provider.create_scope();

    let first = scope.resolve::<Client>().unwrap();
    let second = scope.resolve::<Client>().unwrap();
    let other = provider.create_scope().resolve::<Client>().unwrap();

    assert!(Arc::ptr_eq(&first, &second));
    assert!(!Arc::ptr_eq(&first, &other));
}

#[test]
fn factory_error_is_wrapped_with_path() {
    let mut services = ServiceCollection::new();
    services.add_singleton_with::<Client, _>(|_| Err(io::Error::other("refused").into()));
    let provider = services.build().unwrap();

    let err = provider.resolve::<Client>().err().unwrap();

    let ResolveError::Construction { path, source } = &err else {
        panic!("unexpected error: {err}");
    };
    assert_eq!(path, &[type_name::<Client>()]);
    assert!(source.downcast_ref::<io::Error>().is_some());
}

#[test]
fn factory_dependency_error_keeps_original_path() {
    let mut services = ServiceCollection::new();
    services.add_singleton_with(|r| {
        r.resolve::<Missing>()?;
        Ok(Client { port: 1 })
    });
    let provider = services.build().unwrap();

    let err = provider.resolve::<Client>().err().unwrap();

    assert!(matches!(&err, ResolveError::NotRegistered { path }
        if path == &[type_name::<Client>(), type_name::<Missing>()]));
}

#[test]
fn instance_is_returned_as_is() {
    let mut services = ServiceCollection::new();
    services.add_singleton_instance(Config { port: 8080 });
    let provider = services.build().unwrap();

    let first = provider.resolve::<Config>().unwrap();
    let second = provider.create_scope().resolve::<Config>().unwrap();

    assert_eq!(first.port, 8080);
    assert!(Arc::ptr_eq(&first, &second));
}