ferroui_base/
ferro_locator.rs1use std::any::{type_name, Any, TypeId};
2use std::cell::{Cell, RefCell};
3use std::collections::HashMap;
4use std::marker::PhantomData;
5use std::rc::Rc;
6
7use crate::reactive::IDisposable;
8
9pub type ServiceHandle = Rc<dyn Any>;
14
15type Factory = Rc<dyn Fn() -> Option<ServiceHandle>>;
16
17pub trait IFerroDependencyResolver {
19 fn get_service_untyped(&self, service_type: TypeId) -> Option<ServiceHandle>;
23}
24
25pub struct FerroLocator {
40 parent_scope: Option<Rc<dyn IFerroDependencyResolver>>,
41 registry: RefCell<HashMap<TypeId, Factory>>,
42}
43
44crate::ferro_thread_local! {
45 static CURRENT: RefCell<Option<(Rc<dyn IFerroDependencyResolver>, Rc<FerroLocator>)>> =
46 const { RefCell::new(None) };
47}
48
49fn with_current<R>(f: impl FnOnce(&mut (Rc<dyn IFerroDependencyResolver>, Rc<FerroLocator>)) -> R) -> R {
50 CURRENT.with(|current| {
51 let mut current = current.borrow_mut();
52 let current = current.get_or_insert_with(|| {
53 let locator = Rc::new(FerroLocator::new());
54 (locator.clone() as Rc<dyn IFerroDependencyResolver>, locator)
55 });
56 f(current)
57 })
58}
59
60impl Default for FerroLocator {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66impl FerroLocator {
67 pub fn current() -> Rc<dyn IFerroDependencyResolver> {
69 with_current(|current| current.0.clone())
70 }
71
72 pub fn set_current(value: Rc<dyn IFerroDependencyResolver>) {
73 let previous = with_current(|current| std::mem::replace(&mut current.0, value));
74 drop(previous);
75 }
76
77 pub fn current_mutable() -> Rc<FerroLocator> {
79 with_current(|current| current.1.clone())
80 }
81
82 pub fn set_current_mutable(value: Rc<FerroLocator>) {
83 let previous = with_current(|current| std::mem::replace(&mut current.1, value));
84 drop(previous);
85 }
86
87 pub fn new() -> Self {
88 Self { parent_scope: None, registry: RefCell::new(HashMap::new()) }
89 }
90
91 pub fn with_parent_scope(parent_scope: Rc<dyn IFerroDependencyResolver>) -> Self {
94 Self { parent_scope: Some(parent_scope), registry: RefCell::new(HashMap::new()) }
95 }
96
97 fn register(&self, service_type: TypeId, factory: Factory) {
98 let previous = self.registry.borrow_mut().insert(service_type, factory);
99 drop(previous);
100 }
101
102 pub fn bind<T: ?Sized + 'static>(&self) -> RegistrationHelper<'_, T> {
105 RegistrationHelper { locator: self, _service: PhantomData }
106 }
107
108 pub fn bind_to_self<T: 'static>(&self, constant: Rc<T>) -> &FerroLocator {
110 self.bind::<T>().to_constant(constant)
111 }
112
113 pub fn bind_to_self_singleton<T: Default + 'static>(&self) -> &FerroLocator {
116 self.bind::<T>().to_singleton::<T>(|instance| instance)
117 }
118
119 pub fn enter_scope() -> Rc<dyn IDisposable> {
123 let (resolver, mutable) = with_current(|current| current.clone());
124 let scope = Rc::new(FerroLocator::with_parent_scope(resolver.clone()));
125 Self::set_current(scope.clone());
126 Self::set_current_mutable(scope);
127 Rc::new(ResolverDisposable { resolver, mutable })
128 }
129}
130
131impl IFerroDependencyResolver for FerroLocator {
132 fn get_service_untyped(&self, service_type: TypeId) -> Option<ServiceHandle> {
133 let factory = self.registry.borrow().get(&service_type).cloned();
135 match factory {
136 Some(factory) => factory(),
137 None => self.parent_scope.as_ref().and_then(|parent| parent.get_service_untyped(service_type)),
138 }
139 }
140}
141
142pub struct RegistrationHelper<'a, T: ?Sized + 'static> {
145 locator: &'a FerroLocator,
146 _service: PhantomData<fn() -> Rc<T>>,
147}
148
149impl<'a, T: ?Sized + 'static> RegistrationHelper<'a, T> {
150 pub fn to_constant(self, constant: Rc<T>) -> &'a FerroLocator {
152 let handle: ServiceHandle = Rc::new(constant);
153 self.locator.register(TypeId::of::<T>(), Rc::new(move || Some(handle.clone())));
154 self.locator
155 }
156
157 pub fn to_func(self, func: impl Fn() -> Option<Rc<T>> + 'static) -> &'a FerroLocator {
159 self.locator.register(
160 TypeId::of::<T>(),
161 Rc::new(move || func().map(|instance| Rc::new(instance) as ServiceHandle)),
162 );
163 self.locator
164 }
165
166 pub fn to_lazy(self, func: impl Fn() -> Option<Rc<T>> + 'static) -> &'a FerroLocator {
168 let constructed = Cell::new(false);
169 let instance: RefCell<Option<ServiceHandle>> = RefCell::new(None);
170 self.locator.register(
171 TypeId::of::<T>(),
172 Rc::new(move || {
173 if !constructed.get() {
174 let created = func().map(|instance| Rc::new(instance) as ServiceHandle);
175 *instance.borrow_mut() = created;
176 constructed.set(true);
177 }
178
179 instance.borrow().clone()
180 }),
181 );
182 self.locator
183 }
184
185 pub fn to_singleton<TImpl: Default + 'static>(self, upcast: fn(Rc<TImpl>) -> Rc<T>) -> &'a FerroLocator {
191 let instance: RefCell<Option<Rc<T>>> = RefCell::new(None);
192 self.to_func(move || {
193 let existing = instance.borrow().clone();
194 Some(match existing {
195 Some(existing) => existing,
196 None => {
197 let created = upcast(Rc::new(TImpl::default()));
198 *instance.borrow_mut() = Some(created.clone());
199 created
200 }
201 })
202 })
203 }
204
205 pub fn to_transient<TImpl: Default + 'static>(self, upcast: fn(Rc<TImpl>) -> Rc<T>) -> &'a FerroLocator {
208 self.to_func(move || Some(upcast(Rc::new(TImpl::default()))))
209 }
210}
211
212struct ResolverDisposable {
213 resolver: Rc<dyn IFerroDependencyResolver>,
214 mutable: Rc<FerroLocator>,
215}
216
217impl IDisposable for ResolverDisposable {
218 fn dispose(&self) {
219 FerroLocator::set_current(self.resolver.clone());
220 FerroLocator::set_current_mutable(self.mutable.clone());
221 }
222}
223
224pub trait LocatorExtensions: IFerroDependencyResolver {
226 fn get_service<T: ?Sized + 'static>(&self) -> Option<Rc<T>> {
229 self.get_service_untyped(TypeId::of::<T>()).and_then(|handle| handle.downcast_ref::<Rc<T>>().cloned())
230 }
231
232 fn get_required_service_untyped(&self, service_type: TypeId) -> ServiceHandle {
237 match self.get_service_untyped(service_type) {
238 Some(service) => service,
239 None => panic!("Unable to locate '{service_type:?}'."),
240 }
241 }
242
243 fn get_required_service<T: ?Sized + 'static>(&self) -> Rc<T> {
248 match self.get_service::<T>() {
249 Some(service) => service,
250 None => panic!("Unable to locate '{}'.", type_name::<T>()),
251 }
252 }
253}
254
255impl<R: IFerroDependencyResolver + ?Sized> LocatorExtensions for R {}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 trait IGreeter {
262 fn greet(&self) -> String;
263 }
264
265 #[derive(Default)]
266 struct English;
267
268 impl IGreeter for English {
269 fn greet(&self) -> String {
270 "hello".to_string()
271 }
272 }
273
274 struct Named(&'static str);
275
276 impl IGreeter for Named {
277 fn greet(&self) -> String {
278 self.0.to_string()
279 }
280 }
281
282 #[derive(Default)]
283 struct Counter {
284 value: Cell<i32>,
285 }
286
287 #[test]
288 fn missing_service_is_none() {
289 let locator = FerroLocator::new();
290 assert!(locator.get_service::<dyn IGreeter>().is_none());
291 assert!(locator.get_service::<Counter>().is_none());
292 }
293
294 #[test]
295 fn to_constant_works_for_trait_objects_and_concrete_types() {
296 let locator = FerroLocator::new();
297 let greeter: Rc<dyn IGreeter> = Rc::new(English);
298 locator.bind::<dyn IGreeter>().to_constant(greeter.clone()).bind_to_self(Rc::new(Counter::default()));
299
300 let resolved = locator.get_service::<dyn IGreeter>().unwrap();
301 assert!(Rc::ptr_eq(&resolved, &greeter));
302 assert_eq!(resolved.greet(), "hello");
303
304 let counter = locator.get_required_service::<Counter>();
305 counter.value.set(3);
306 assert_eq!(locator.get_service::<Counter>().unwrap().value.get(), 3);
307
308 assert!(locator.get_service::<English>().is_none());
310 }
311
312 #[test]
313 fn rebinding_replaces_the_registration() {
314 let locator = FerroLocator::new();
315 locator.bind::<dyn IGreeter>().to_constant(Rc::new(Named("first")));
316 locator.bind::<dyn IGreeter>().to_constant(Rc::new(Named("second")));
317 assert_eq!(locator.get_required_service::<dyn IGreeter>().greet(), "second");
318 }
319
320 #[test]
321 fn to_func_is_called_for_every_lookup() {
322 let locator = FerroLocator::new();
323 let calls = Rc::new(Cell::new(0));
324 let c = calls.clone();
325 locator.bind::<dyn IGreeter>().to_func(move || {
326 c.set(c.get() + 1);
327 Some(Rc::new(English) as Rc<dyn IGreeter>)
328 });
329 let a = locator.get_service::<dyn IGreeter>().unwrap();
330 let b = locator.get_service::<dyn IGreeter>().unwrap();
331 assert!(!Rc::ptr_eq(&a, &b));
332 assert_eq!(calls.get(), 2);
333 }
334
335 #[test]
336 fn to_lazy_constructs_once() {
337 let locator = FerroLocator::new();
338 let calls = Rc::new(Cell::new(0));
339 let c = calls.clone();
340 locator.bind::<dyn IGreeter>().to_lazy(move || {
341 c.set(c.get() + 1);
342 Some(Rc::new(English) as Rc<dyn IGreeter>)
343 });
344 assert_eq!(calls.get(), 0);
345 let a = locator.get_service::<dyn IGreeter>().unwrap();
346 let b = locator.get_service::<dyn IGreeter>().unwrap();
347 assert!(Rc::ptr_eq(&a, &b));
348 assert_eq!(calls.get(), 1);
349 }
350
351 #[test]
352 fn to_singleton_and_to_transient() {
353 let locator = FerroLocator::new();
354 locator.bind::<dyn IGreeter>().to_singleton::<English>(|instance| instance);
355 let a = locator.get_service::<dyn IGreeter>().unwrap();
356 let b = locator.get_service::<dyn IGreeter>().unwrap();
357 assert!(Rc::ptr_eq(&a, &b));
358
359 locator.bind::<dyn IGreeter>().to_transient::<English>(|instance| instance);
360 let a = locator.get_service::<dyn IGreeter>().unwrap();
361 let b = locator.get_service::<dyn IGreeter>().unwrap();
362 assert!(!Rc::ptr_eq(&a, &b));
363
364 locator.bind_to_self_singleton::<Counter>();
365 let a = locator.get_service::<Counter>().unwrap();
366 a.value.set(7);
367 assert_eq!(locator.get_service::<Counter>().unwrap().value.get(), 7);
368 }
369
370 #[test]
371 fn enter_scope_inherits_and_restores() {
372 FerroLocator::current_mutable().bind::<dyn IGreeter>().to_constant(Rc::new(Named("outer")));
375 let outer = FerroLocator::current_mutable();
376
377 let scope = FerroLocator::enter_scope();
378 assert!(!Rc::ptr_eq(&outer, &FerroLocator::current_mutable()));
379 assert_eq!(FerroLocator::current().get_required_service::<dyn IGreeter>().greet(), "outer");
381
382 FerroLocator::current_mutable().bind::<dyn IGreeter>().to_constant(Rc::new(Named("inner")));
383 FerroLocator::current_mutable().bind_to_self(Rc::new(Counter::default()));
384 assert_eq!(FerroLocator::current().get_required_service::<dyn IGreeter>().greet(), "inner");
385
386 scope.dispose();
387 assert!(Rc::ptr_eq(&outer, &FerroLocator::current_mutable()));
388 assert_eq!(FerroLocator::current().get_required_service::<dyn IGreeter>().greet(), "outer");
389 assert!(FerroLocator::current().get_service::<Counter>().is_none());
390 }
391
392 #[test]
393 #[should_panic(expected = "Unable to locate")]
394 fn get_required_service_panics_when_missing() {
395 FerroLocator::new().get_required_service::<dyn IGreeter>();
396 }
397}