Skip to main content

minco_core/
service.rs

1use std::{
2    any::{Any, TypeId},
3    collections::HashMap,
4    sync::Arc,
5};
6use thiserror::Error;
7
8/// Sized wrapper that lets the service and contribution registries store an `Arc<dyn Trait>`
9/// without introducing a global service locator or stringly typed key.
10#[derive(Clone)]
11pub struct Shared<T: ?Sized + Send + Sync + 'static> {
12    inner: Arc<T>,
13}
14
15impl<T: ?Sized + Send + Sync + 'static> Shared<T> {
16    pub const fn new(inner: Arc<T>) -> Self {
17        Self { inner }
18    }
19
20    pub fn inner(&self) -> Arc<T> {
21        Arc::clone(&self.inner)
22    }
23}
24
25impl<T: ?Sized + Send + Sync + 'static> std::fmt::Debug for Shared<T> {
26    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        formatter
28            .debug_struct("Shared")
29            .field("service", &std::any::type_name::<T>())
30            .finish_non_exhaustive()
31    }
32}
33
34#[derive(Default)]
35pub struct ServiceCollection {
36    services: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
37}
38
39impl std::fmt::Debug for ServiceCollection {
40    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        formatter
42            .debug_struct("ServiceCollection")
43            .field("service_count", &self.services.len())
44            .finish()
45    }
46}
47
48impl ServiceCollection {
49    pub fn insert<T>(&mut self, value: Arc<T>) -> Result<(), ServiceError>
50    where
51        T: Any + Send + Sync,
52    {
53        let type_id = TypeId::of::<T>();
54        if self.services.contains_key(&type_id) {
55            return Err(ServiceError::Duplicate(std::any::type_name::<T>()));
56        }
57        self.services.insert(type_id, value);
58        Ok(())
59    }
60
61    pub fn insert_shared<T>(&mut self, value: Arc<T>) -> Result<(), ServiceError>
62    where
63        T: ?Sized + Send + Sync + 'static,
64    {
65        self.insert(Arc::new(Shared::new(value)))
66    }
67
68    pub fn contains<T>(&self) -> bool
69    where
70        T: Any + Send + Sync,
71    {
72        self.services.contains_key(&TypeId::of::<T>())
73    }
74
75    pub fn contains_shared<T>(&self) -> bool
76    where
77        T: ?Sized + Send + Sync + 'static,
78    {
79        self.contains::<Shared<T>>()
80    }
81
82    pub fn get<T>(&self) -> Result<Arc<T>, ServiceError>
83    where
84        T: Any + Send + Sync,
85    {
86        get_service(&self.services)
87    }
88
89    pub fn get_optional<T>(&self) -> Result<Option<Arc<T>>, ServiceError>
90    where
91        T: Any + Send + Sync,
92    {
93        get_optional_service(&self.services)
94    }
95
96    pub fn get_shared<T>(&self) -> Result<Arc<T>, ServiceError>
97    where
98        T: ?Sized + Send + Sync + 'static,
99    {
100        self.get::<Shared<T>>().map(|service| service.inner())
101    }
102
103    pub fn get_optional_shared<T>(&self) -> Result<Option<Arc<T>>, ServiceError>
104    where
105        T: ?Sized + Send + Sync + 'static,
106    {
107        self.get_optional::<Shared<T>>()
108            .map(|service| service.map(|value| value.inner()))
109    }
110
111    pub fn freeze(self) -> FrozenServices {
112        FrozenServices {
113            services: self.services,
114        }
115    }
116}
117
118#[derive(Clone, Default)]
119pub struct FrozenServices {
120    services: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
121}
122
123impl std::fmt::Debug for FrozenServices {
124    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        formatter
126            .debug_struct("FrozenServices")
127            .field("service_count", &self.services.len())
128            .finish()
129    }
130}
131
132impl FrozenServices {
133    pub fn get<T>(&self) -> Result<Arc<T>, ServiceError>
134    where
135        T: Any + Send + Sync,
136    {
137        get_service(&self.services)
138    }
139
140    pub fn get_optional<T>(&self) -> Result<Option<Arc<T>>, ServiceError>
141    where
142        T: Any + Send + Sync,
143    {
144        get_optional_service(&self.services)
145    }
146
147    pub fn get_shared<T>(&self) -> Result<Arc<T>, ServiceError>
148    where
149        T: ?Sized + Send + Sync + 'static,
150    {
151        self.get::<Shared<T>>().map(|service| service.inner())
152    }
153
154    pub fn get_optional_shared<T>(&self) -> Result<Option<Arc<T>>, ServiceError>
155    where
156        T: ?Sized + Send + Sync + 'static,
157    {
158        self.get_optional::<Shared<T>>()
159            .map(|service| service.map(|value| value.inner()))
160    }
161}
162
163fn get_service<T>(
164    services: &HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
165) -> Result<Arc<T>, ServiceError>
166where
167    T: Any + Send + Sync,
168{
169    services
170        .get(&TypeId::of::<T>())
171        .cloned()
172        .ok_or_else(|| ServiceError::Missing(std::any::type_name::<T>()))?
173        .downcast::<T>()
174        .map_err(|_| ServiceError::TypeMismatch(std::any::type_name::<T>()))
175}
176
177fn get_optional_service<T>(
178    services: &HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
179) -> Result<Option<Arc<T>>, ServiceError>
180where
181    T: Any + Send + Sync,
182{
183    services
184        .get(&TypeId::of::<T>())
185        .cloned()
186        .map(|value| {
187            value
188                .downcast::<T>()
189                .map_err(|_| ServiceError::TypeMismatch(std::any::type_name::<T>()))
190        })
191        .transpose()
192}
193
194#[derive(Debug, Error, Clone, PartialEq, Eq)]
195pub enum ServiceError {
196    #[error("service is already registered: {0}")]
197    Duplicate(&'static str),
198    #[error("service is not registered: {0}")]
199    Missing(&'static str),
200    #[error("service has an unexpected concrete type: {0}")]
201    TypeMismatch(&'static str),
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    trait Greeting: Send + Sync {
209        fn text(&self) -> &'static str;
210    }
211
212    #[derive(Debug)]
213    struct Hello;
214    impl Greeting for Hello {
215        fn text(&self) -> &'static str {
216            "hello"
217        }
218    }
219
220    #[test]
221    fn typed_services_are_frozen_and_retrieved_without_string_keys() {
222        let mut services = ServiceCollection::default();
223        services.insert(Arc::new(String::from("hello"))).unwrap();
224        let frozen = services.freeze();
225        assert_eq!(&*frozen.get::<String>().unwrap(), "hello");
226    }
227
228    #[test]
229    fn trait_object_services_are_registered_without_double_arc_call_sites() {
230        let mut services = ServiceCollection::default();
231        services
232            .insert_shared::<dyn Greeting>(Arc::new(Hello))
233            .unwrap();
234        assert_eq!(
235            services.get_shared::<dyn Greeting>().unwrap().text(),
236            "hello"
237        );
238    }
239
240    #[test]
241    fn optional_service_lookup_distinguishes_absence_from_type_errors() {
242        let services = ServiceCollection::default();
243        assert!(services.get_optional::<String>().unwrap().is_none());
244        assert!(
245            services
246                .get_optional_shared::<dyn Greeting>()
247                .unwrap()
248                .is_none()
249        );
250
251        let frozen = services.freeze();
252        assert!(frozen.get_optional::<String>().unwrap().is_none());
253        assert!(
254            frozen
255                .get_optional_shared::<dyn Greeting>()
256                .unwrap()
257                .is_none()
258        );
259    }
260
261    #[test]
262    fn duplicate_service_types_are_rejected() {
263        let mut services = ServiceCollection::default();
264        services.insert(Arc::new(1_u64)).unwrap();
265        assert!(matches!(
266            services.insert(Arc::new(2_u64)),
267            Err(ServiceError::Duplicate(_))
268        ));
269    }
270}