minco-core 0.1.1

Provider-neutral application graph, static plugin composition, capabilities, and typed services for Minco
Documentation
use std::{
    any::{Any, TypeId},
    collections::HashMap,
    sync::Arc,
};
use thiserror::Error;

#[derive(Default)]
pub struct ServiceCollection {
    services: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
}

impl std::fmt::Debug for ServiceCollection {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("ServiceCollection")
            .field("service_count", &self.services.len())
            .finish()
    }
}

impl ServiceCollection {
    pub fn insert<T>(&mut self, value: Arc<T>) -> Result<(), ServiceError>
    where
        T: Any + Send + Sync,
    {
        let type_id = TypeId::of::<T>();
        if self.services.contains_key(&type_id) {
            return Err(ServiceError::Duplicate(std::any::type_name::<T>()));
        }
        self.services.insert(type_id, value);
        Ok(())
    }

    pub fn contains<T>(&self) -> bool
    where
        T: Any + Send + Sync,
    {
        self.services.contains_key(&TypeId::of::<T>())
    }

    pub fn freeze(self) -> FrozenServices {
        FrozenServices {
            services: self.services,
        }
    }
}

#[derive(Clone, Default)]
pub struct FrozenServices {
    services: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
}

impl std::fmt::Debug for FrozenServices {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("FrozenServices")
            .field("service_count", &self.services.len())
            .finish()
    }
}

impl FrozenServices {
    pub fn get<T>(&self) -> Result<Arc<T>, ServiceError>
    where
        T: Any + Send + Sync,
    {
        self.services
            .get(&TypeId::of::<T>())
            .cloned()
            .ok_or_else(|| ServiceError::Missing(std::any::type_name::<T>()))?
            .downcast::<T>()
            .map_err(|_| ServiceError::TypeMismatch(std::any::type_name::<T>()))
    }
}

#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ServiceError {
    #[error("service is already registered: {0}")]
    Duplicate(&'static str),
    #[error("service is not registered: {0}")]
    Missing(&'static str),
    #[error("service has an unexpected concrete type: {0}")]
    TypeMismatch(&'static str),
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn typed_services_are_frozen_and_retrieved_without_string_keys() {
        let mut services = ServiceCollection::default();
        services.insert(Arc::new(String::from("hello"))).unwrap();
        let frozen = services.freeze();
        assert_eq!(&*frozen.get::<String>().unwrap(), "hello");
    }

    #[test]
    fn duplicate_service_types_are_rejected() {
        let mut services = ServiceCollection::default();
        services.insert(Arc::new(1_u64)).unwrap();
        assert!(matches!(
            services.insert(Arc::new(2_u64)),
            Err(ServiceError::Duplicate(_))
        ));
    }
}