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::{Dependency, ResolveError, Resolver, Service, ServiceCollection, ServiceProvider};
use std::any::type_name;
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
use std::sync::{Arc, Barrier};
use std::thread;
use std::time::Duration;

struct Transaction;

impl Service for Transaction {
    fn dependencies() -> Vec<Dependency> {
        Vec::new()
    }

    fn construct(_: &Resolver<'_>) -> Result<Self, ResolveError> {
        Ok(Transaction)
    }
}

struct Clock;

impl Service for Clock {
    fn dependencies() -> Vec<Dependency> {
        Vec::new()
    }

    fn construct(_: &Resolver<'_>) -> Result<Self, ResolveError> {
        Ok(Clock)
    }
}

struct Handler {
    tx: Arc<Transaction>,
}

impl Service for Handler {
    fn dependencies() -> Vec<Dependency> {
        vec![Dependency::on::<Transaction>()]
    }

    fn construct(resolver: &Resolver<'_>) -> Result<Self, ResolveError> {
        Ok(Handler {
            tx: resolver.resolve()?,
        })
    }
}

struct Cache {
    _tx: Arc<Transaction>,
}

impl Service for Cache {
    fn dependencies() -> Vec<Dependency> {
        vec![Dependency::on::<Transaction>()]
    }

    fn construct(resolver: &Resolver<'_>) -> Result<Self, ResolveError> {
        Ok(Cache {
            _tx: resolver.resolve()?,
        })
    }
}

fn provider() -> ServiceProvider {
    let mut services = ServiceCollection::new();
    services
        .add_scoped::<Transaction>()
        .add_singleton::<Clock>()
        .add_transient::<Handler>()
        .add_singleton::<Cache>();
    services.build().unwrap()
}

#[test]
fn scoped_is_shared_within_scope() {
    let scope = provider().create_scope();

    let first = scope.resolve::<Transaction>().unwrap();
    let second = scope.resolve::<Transaction>().unwrap();

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

#[test]
fn scoped_differs_across_scopes() {
    let provider = provider();

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

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

#[test]
fn singleton_is_shared_across_scopes() {
    let provider = provider();

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

    assert!(Arc::ptr_eq(&first, &second));
    assert!(Arc::ptr_eq(&first, &provider.resolve::<Clock>().unwrap()));
}

#[test]
fn transient_in_scope_can_depend_on_scoped() {
    let scope = provider().create_scope();

    let handler = scope.resolve::<Handler>().unwrap();

    assert!(Arc::ptr_eq(
        &handler.tx,
        &scope.resolve::<Transaction>().unwrap()
    ));
}

#[test]
fn singleton_depending_on_scoped_fails_from_scope() {
    let scope = provider().create_scope();

    let err = scope.resolve::<Cache>().err().unwrap();

    assert!(matches!(&err, ResolveError::ScopedFromRoot { path }
        if path == &[type_name::<Cache>(), type_name::<Transaction>()]));
}

static SLOW_CALLS: AtomicUsize = AtomicUsize::new(0);

struct Slow;

impl Service for Slow {
    fn dependencies() -> Vec<Dependency> {
        Vec::new()
    }

    fn construct(_: &Resolver<'_>) -> Result<Self, ResolveError> {
        SLOW_CALLS.fetch_add(1, SeqCst);
        thread::sleep(Duration::from_millis(20));
        Ok(Slow)
    }
}

#[test]
fn scoped_constructed_once_under_contention() {
    let mut services = ServiceCollection::new();
    services.add_scoped::<Slow>();
    let scope = Arc::new(services.build().unwrap().create_scope());
    let barrier = Arc::new(Barrier::new(16));

    let handles: Vec<_> = (0..16)
        .map(|_| {
            let scope = Arc::clone(&scope);
            let barrier = Arc::clone(&barrier);
            thread::spawn(move || {
                barrier.wait();
                scope.resolve::<Slow>().unwrap()
            })
        })
        .collect();
    let instances: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();

    assert_eq!(SLOW_CALLS.load(SeqCst), 1);
    assert!(instances.iter().all(|i| Arc::ptr_eq(i, &instances[0])));
}

#[test]
fn scope_outlives_provider() {
    let provider = provider();
    let scope = provider.create_scope();
    drop(provider);

    let resolved = thread::spawn(move || {
        scope.resolve::<Clock>().is_ok() && scope.resolve::<Transaction>().is_ok()
    })
    .join()
    .unwrap();

    assert!(resolved);
}