Skip to main content

ferroui_base/
ferro_locator.rs

1use 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
9/// A type-erased service handle: an `Rc<dyn Any>` whose content is the
10/// `Rc<TService>` handle of the service. Boxing the handle rather than the
11/// service is what lets trait objects (`Rc<dyn IFoo>`) be stored and
12/// recovered with their exact type.
13pub type ServiceHandle = Rc<dyn Any>;
14
15type Factory = Rc<dyn Fn() -> Option<ServiceHandle>>;
16
17/// Resolves services by type.
18pub trait IFerroDependencyResolver {
19    /// Returns the service registered for `service_type`, as a handle whose
20    /// content is `Rc<TService>`. Use
21    /// [`LocatorExtensions::get_service`] for the typed form.
22    fn get_service_untyped(&self, service_type: TypeId) -> Option<ServiceHandle>;
23}
24
25/// A scoped service registry.
26///
27/// Services are keyed by the `TypeId` of the service type, which can be a
28/// concrete type (`Foo`, fetched as `Rc<Foo>`) or a trait object type
29/// (`dyn IFoo`, fetched as `Rc<dyn IFoo>`):
30///
31/// ```ignore
32/// FerroLocator::current_mutable().bind::<dyn IFoo>().to_constant(Rc::new(Foo::new()));
33/// let foo: Option<Rc<dyn IFoo>> = FerroLocator::current().get_service::<dyn IFoo>();
34/// ```
35///
36/// Like the rest of the object model the locator is per-thread: the current
37/// locator and its registrations are only visible on the thread that made
38/// them.
39pub 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    /// The resolver services are looked up in.
68    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    /// The locator services are registered in.
78    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    /// Creates a locator that falls back to `parent_scope` for services it
92    /// has no registration for.
93    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    /// Starts the registration of the service type `T`, which may be a trait
103    /// object type.
104    pub fn bind<T: ?Sized + 'static>(&self) -> RegistrationHelper<'_, T> {
105        RegistrationHelper { locator: self, _service: PhantomData }
106    }
107
108    /// Registers `constant` as the service of its own type.
109    pub fn bind_to_self<T: 'static>(&self, constant: Rc<T>) -> &FerroLocator {
110        self.bind::<T>().to_constant(constant)
111    }
112
113    /// Registers a lazily created instance of `T` as the service of its own
114    /// type.
115    pub fn bind_to_self_singleton<T: Default + 'static>(&self) -> &FerroLocator {
116        self.bind::<T>().to_singleton::<T>(|instance| instance)
117    }
118
119    /// Makes a new locator, whose parent is the current resolver, the
120    /// current one. Disposing the returned value restores the previous
121    /// resolver and locator.
122    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        // The factory is cloned out so that it can use the locator itself.
134        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
142/// Binds the service type `T` to an implementation; see
143/// [`FerroLocator::bind`].
144pub 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    /// Every lookup returns `constant`.
151    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    /// Every lookup calls `func`.
158    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    /// The first lookup calls `func`; later lookups return what it produced.
167    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    /// The first lookup creates a `TImpl`, which every lookup then returns.
186    ///
187    /// `upcast` converts the implementation handle to the service handle;
188    /// for a trait object service it is the identity closure
189    /// `|instance| instance`, which performs the unsizing coercion.
190    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    /// Every lookup creates a new `TImpl`. See
206    /// [`to_singleton`](Self::to_singleton) for `upcast`.
207    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
224/// Typed lookups for every [`IFerroDependencyResolver`].
225pub trait LocatorExtensions: IFerroDependencyResolver {
226    /// Returns the service registered for `T`, which may be a trait object
227    /// type (`get_service::<dyn IFoo>()` returns `Option<Rc<dyn IFoo>>`).
228    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    /// Returns the service registered for `service_type`.
233    ///
234    /// # Panics
235    /// Panics when the service is not registered.
236    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    /// Returns the service registered for `T`.
244    ///
245    /// # Panics
246    /// Panics when the service is not registered.
247    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        // The trait object registration does not register the implementation type.
309        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        // The current locator is per-thread, so this test cannot interfere
373        // with tests running on other threads.
374        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        // Inherited from the parent scope.
380        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}