Skip to main content

aro_core/state/
mod.rs

1//! Type-map service container for dependency injection.
2
3use std::any::{Any, TypeId, type_name};
4use std::collections::HashMap;
5use std::sync::Arc;
6
7type CloneFn = Arc<dyn Fn(&dyn Any) -> Box<dyn Any + Send + Sync> + Send + Sync>;
8
9struct Entry {
10    value: Box<dyn Any + Send + Sync>,
11    clone_fn: CloneFn,
12}
13
14impl Clone for Entry {
15    fn clone(&self) -> Self {
16        Self {
17            value: (self.clone_fn)(self.value.as_ref()),
18            clone_fn: Arc::clone(&self.clone_fn),
19        }
20    }
21}
22
23/// A type-map service container for dependency injection.
24#[derive(Clone)]
25pub struct AroState {
26    services: HashMap<TypeId, Entry>,
27}
28
29impl std::fmt::Debug for AroState {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        f.debug_struct("AroState")
32            .field("services", &format!("{} registered", self.services.len()))
33            .finish()
34    }
35}
36
37impl AroState {
38    /// Create a new builder for constructing an `AroState`.
39    pub fn builder() -> AroStateBuilder {
40        AroStateBuilder {
41            services: HashMap::new(),
42        }
43    }
44
45    /// Retrieve a registered service by trait type.
46    pub fn get<T: ?Sized + 'static>(&self) -> Option<Arc<T>>
47    where
48        Arc<T>: 'static,
49    {
50        self.services
51            .get(&TypeId::of::<Arc<T>>())
52            .and_then(|entry| entry.value.downcast_ref::<Arc<T>>())
53            .cloned()
54    }
55
56    /// Merge another `AroState` into this one.
57    #[must_use]
58    pub fn merge(mut self, other: Self) -> Self {
59        self.services.extend(other.services);
60        self
61    }
62
63    /// Validate that an expected dependency is registered.
64    pub fn validate_dep<T: ?Sized + 'static>(&self) -> Result<(), String>
65    where
66        Arc<T>: 'static,
67    {
68        if self.get::<T>().is_some() {
69            Ok(())
70        } else {
71            Err(format!("missing dependency: {}", type_name::<T>()))
72        }
73    }
74}
75
76/// Builder for constructing an [`AroState`] with registered services.
77pub struct AroStateBuilder {
78    services: HashMap<TypeId, Entry>,
79}
80
81impl AroStateBuilder {
82    /// Register a service as `Arc<dyn Trait>`.
83    #[must_use]
84    #[expect(
85        clippy::expect_used,
86        reason = "type mismatch in clone_fn indicates a bug in AroState internals"
87    )]
88    pub fn register<T: ?Sized + 'static>(mut self, service: Arc<T>) -> Self
89    where
90        Arc<T>: Send + Sync + Clone + 'static,
91    {
92        let clone_fn: CloneFn = Arc::new(|any: &dyn Any| {
93            let arc = any
94                .downcast_ref::<Arc<T>>()
95                .expect("type mismatch in AroState clone");
96            Box::new(arc.clone())
97        });
98        self.services.insert(
99            TypeId::of::<Arc<T>>(),
100            Entry {
101                value: Box::new(service),
102                clone_fn,
103            },
104        );
105        self
106    }
107
108    /// Build the `AroState` from registered services.
109    pub fn build(self) -> AroState {
110        AroState {
111            services: self.services,
112        }
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    trait Greeter: Send + Sync {
121        fn greet(&self) -> &'static str;
122    }
123
124    struct HelloGreeter;
125    impl Greeter for HelloGreeter {
126        fn greet(&self) -> &'static str {
127            "hello"
128        }
129    }
130
131    trait Counter: Send + Sync {
132        fn count(&self) -> u32;
133    }
134
135    struct FixedCounter(u32);
136    impl Counter for FixedCounter {
137        fn count(&self) -> u32 {
138            self.0
139        }
140    }
141
142    #[test]
143    fn register_and_retrieve_service() {
144        let state = AroState::builder()
145            .register::<dyn Greeter>(Arc::new(HelloGreeter))
146            .build();
147        let greeter = state.get::<dyn Greeter>().unwrap();
148        assert_eq!(greeter.greet(), "hello");
149    }
150
151    #[test]
152    fn missing_service_returns_none() {
153        let state = AroState::builder().build();
154        assert!(state.get::<dyn Greeter>().is_none());
155    }
156
157    #[test]
158    fn multiple_services() {
159        let state = AroState::builder()
160            .register::<dyn Greeter>(Arc::new(HelloGreeter))
161            .register::<dyn Counter>(Arc::new(FixedCounter(42)))
162            .build();
163        assert_eq!(state.get::<dyn Greeter>().unwrap().greet(), "hello");
164        assert_eq!(state.get::<dyn Counter>().unwrap().count(), 42);
165    }
166
167    #[test]
168    fn validate_dep_succeeds_when_registered() {
169        let state = AroState::builder()
170            .register::<dyn Greeter>(Arc::new(HelloGreeter))
171            .build();
172        assert!(state.validate_dep::<dyn Greeter>().is_ok());
173    }
174
175    #[test]
176    fn validate_dep_fails_when_missing() {
177        let state = AroState::builder().build();
178        let err = state.validate_dep::<dyn Greeter>().unwrap_err();
179        assert!(err.contains("missing dependency"));
180    }
181
182    #[test]
183    fn state_is_clone() {
184        let state = AroState::builder()
185            .register::<dyn Greeter>(Arc::new(HelloGreeter))
186            .build();
187        let cloned = state.clone();
188        assert_eq!(cloned.get::<dyn Greeter>().unwrap().greet(), "hello");
189    }
190
191    #[test]
192    fn merge_preserves_both_types() {
193        let state_a = AroState::builder()
194            .register::<dyn Greeter>(Arc::new(HelloGreeter))
195            .build();
196        let state_b = AroState::builder()
197            .register::<dyn Counter>(Arc::new(FixedCounter(7)))
198            .build();
199
200        let merged = state_a.merge(state_b);
201        assert_eq!(merged.get::<dyn Greeter>().unwrap().greet(), "hello");
202        assert_eq!(merged.get::<dyn Counter>().unwrap().count(), 7);
203    }
204
205    #[test]
206    fn merge_overwrites_existing_type() {
207        struct AltGreeter;
208        impl Greeter for AltGreeter {
209            fn greet(&self) -> &'static str {
210                "hi"
211            }
212        }
213
214        let state_a = AroState::builder()
215            .register::<dyn Greeter>(Arc::new(HelloGreeter))
216            .build();
217        let state_b = AroState::builder()
218            .register::<dyn Greeter>(Arc::new(AltGreeter))
219            .build();
220
221        let merged = state_a.merge(state_b);
222        assert_eq!(merged.get::<dyn Greeter>().unwrap().greet(), "hi");
223    }
224
225    #[test]
226    fn merge_preserves_unrelated_types() {
227        let state_a = AroState::builder()
228            .register::<dyn Greeter>(Arc::new(HelloGreeter))
229            .register::<dyn Counter>(Arc::new(FixedCounter(1)))
230            .build();
231        let state_b = AroState::builder()
232            .register::<dyn Counter>(Arc::new(FixedCounter(99)))
233            .build();
234
235        let merged = state_a.merge(state_b);
236        // Greeter was only in state_a — must still be present
237        assert_eq!(merged.get::<dyn Greeter>().unwrap().greet(), "hello");
238        // Counter was overwritten by state_b
239        assert_eq!(merged.get::<dyn Counter>().unwrap().count(), 99);
240    }
241}