Skip to main content

injectable_rs_runtime/
registry.rs

1//! Provider registry for dynamically-registered external types.
2//!
3//! This module provides the [`ProviderRegistry`] — a type-safe store for
4//! [`DynProvider`] instances. It uses `TypeId` + a **token string** internally
5//! for lookup, allowing multiple providers of the same type to coexist as long
6//! as they carry different tokens.
7//!
8//! # Token-based Registration
9//!
10//! Every registration is keyed by `(TypeId<T>, token)`. The token is an
11//! arbitrary `&str` that disambiguates providers of the same type:
12//!
13//! ```rust,ignore
14//! let mut registry = ProviderRegistry::new();
15//!
16//! // Two pools of the same type, differentiated by token
17//! registry.register("primary",  DynProvider::new(|| async { Ok(PrimaryPool::connect()) }));
18//! registry.register("replica",  DynProvider::new(|| async { Ok(ReplicaPool::connect()) }));
19//! registry.register("analytics", DynProvider::new(|| async { Ok(AnalyticsPool::connect()) }));
20//! ```
21//!
22//! Use [`DEFAULT_TOKEN`] (`""`) when only one provider of a type is needed —
23//! this is the canonical, unnamed registration that [`ResolveContext::resolve_external`]
24//! queries without an explicit token.
25//!
26//! # Lookup Strategy
27//!
28//! When `ResolveContext::resolve_external_with_token::<T>(token)` is called:
29//! 1. Check if `(TypeId<T>, token)` is in the registry
30//! 2. If found, invoke its `DynProvider` closure
31//! 3. If the token is the default and no DynProvider is found, fall back to
32//!    `InjectableArcFactory` inventory entries (for Injectable types)
33//! 4. Otherwise, return `MissingDependency` error
34
35use std::any::{Any, TypeId};
36use std::collections::HashMap;
37use std::sync::Arc;
38
39use crate::{DynProvider, InjectableError, InjectableResult, ResolveContext};
40
41/// The default (unnamed) provider token.
42///
43/// Use this when registering or resolving a provider that does not need to be
44/// differentiated from other providers of the same type.  Passing `""` is
45/// identical.
46///
47/// ```rust,ignore
48/// builder.register(DEFAULT_TOKEN, DynProvider::sync(|| Ok(Client::new())));
49/// let client: Client = container.resolve_external_with_token(DEFAULT_TOKEN).await?;
50/// // equivalent:
51/// let client: Client = container.resolve_external::<Client>().await?;
52/// ```
53pub const DEFAULT_TOKEN: &str = "";
54
55pub type ErasedProviderPinnedFuture<'a> = std::pin::Pin<
56    Box<dyn std::future::Future<Output = InjectableResult<Box<dyn Any + Send>>> + Send + 'a>,
57>;
58
59/// A type-erased dynamic provider stored in the registry.
60trait ErasedProvider: Send + Sync + 'static {
61    fn provide_as_any(&self, ctx: Arc<ResolveContext>) -> ErasedProviderPinnedFuture<'_>;
62}
63
64impl<T: Send + Sync + 'static> ErasedProvider for DynProvider<T> {
65    fn provide_as_any(&self, ctx: Arc<ResolveContext>) -> ErasedProviderPinnedFuture<'_> {
66        Box::pin(async move {
67            let value = self.provide(ctx).await?;
68            Ok(Box::new(value) as Box<dyn Any + Send>)
69        })
70    }
71}
72
73/// Registry key: `(TypeId, token)`.
74///
75/// Two registrations are considered the same if and only if both the type
76/// **and** the token match.  This enables multiple providers of the same type
77/// (e.g., several `sqlx::Pool` instances for different databases) to coexist.
78type RegistryKey = (TypeId, String);
79
80/// A registry of dynamically-registered providers for external types.
81///
82/// Every registration is keyed by `(TypeId<T>, token)`.  Use [`DEFAULT_TOKEN`]
83/// (`""`) when you only need one provider for a type; use distinct token strings
84/// when you need multiple providers of the same type.
85///
86/// # Example
87///
88/// ```rust,ignore
89/// let mut registry = ProviderRegistry::new();
90///
91/// // Unnamed (default) provider
92/// registry.register("", DynProvider::sync(|| Ok(reqwest::Client::new())));
93///
94/// // Named providers — two pools of the same type
95/// registry.register("primary", DynProvider::new(|| async { Ok(primary_pool()) }));
96/// registry.register("replica", DynProvider::new(|| async { Ok(replica_pool()) }));
97/// ```
98pub struct ProviderRegistry {
99    providers: HashMap<RegistryKey, Box<dyn ErasedProvider>>,
100    /// `"TypeName[token]"` strings recorded when the same `(type, token)` pair is
101    /// registered more than once, surfaced as errors at `ContainerBuilder::build` time.
102    duplicates: Vec<String>,
103}
104
105impl ProviderRegistry {
106    /// Create a new empty registry.
107    pub fn new() -> Self {
108        Self {
109            providers: HashMap::new(),
110            duplicates: Vec::new(),
111        }
112    }
113
114    fn make_key<T: 'static>(token: &str) -> RegistryKey {
115        (TypeId::of::<T>(), token.to_string())
116    }
117
118    fn duplicate_label<T: 'static>(token: &str) -> String {
119        if token.is_empty() {
120            std::any::type_name::<T>().to_string()
121        } else {
122            format!("{}[{}]", std::any::type_name::<T>(), token)
123        }
124    }
125
126    /// Register a dynamic provider for type `T` under the given `token`.
127    ///
128    /// If the same `(type, token)` pair is registered more than once, the
129    /// duplicate is recorded and surfaced as an error when
130    /// `ContainerBuilder::build` is called.  Use
131    /// [`register_or_replace`](Self::register_or_replace) if you intentionally
132    /// want to override an existing registration.
133    ///
134    /// # Token conventions
135    ///
136    /// - Use [`DEFAULT_TOKEN`] (`""`) for the canonical, unnamed provider.
137    /// - Use a descriptive string (`"primary"`, `"analytics"`) for named variants.
138    ///
139    /// # Example
140    ///
141    /// ```rust,ignore
142    /// // Default registration
143    /// registry.register("", DynProvider::sync(|| Ok(reqwest::Client::new())));
144    ///
145    /// // Named registrations of the same type
146    /// registry.register("primary",  DynProvider::new(|| async { Ok(primary_pool()) }));
147    /// registry.register("replica",  DynProvider::new(|| async { Ok(replica_pool()) }));
148    /// ```
149    pub fn register<T: Send + Sync + 'static>(
150        &mut self,
151        token: impl Into<String>,
152        provider: DynProvider<T>,
153    ) {
154        let token = token.into();
155        let key = Self::make_key::<T>(&token);
156        if self.providers.contains_key(&key) {
157            self.duplicates.push(Self::duplicate_label::<T>(&token));
158        }
159        self.providers.insert(key, Box::new(provider));
160    }
161
162    /// Register a dynamic provider for type `T` under the given `token`,
163    /// silently replacing any previously registered provider for the same
164    /// `(type, token)` pair.
165    ///
166    /// Use this in tests or layered-config scenarios where intentional override
167    /// is expected.
168    pub fn register_or_replace<T: Send + Sync + 'static>(
169        &mut self,
170        token: impl Into<String>,
171        provider: DynProvider<T>,
172    ) {
173        let key = (TypeId::of::<T>(), token.into());
174        self.providers.insert(key, Box::new(provider));
175    }
176
177    /// Return duplicate `"TypeName[token]"` labels recorded so far.
178    pub fn duplicates(&self) -> &[String] {
179        &self.duplicates
180    }
181
182    /// Check if the registry has a provider for type `T` with the default token.
183    ///
184    /// Equivalent to `has_with_token::<T>(DEFAULT_TOKEN)`.
185    pub fn has<T: 'static>(&self) -> bool {
186        self.has_with_token::<T>(DEFAULT_TOKEN)
187    }
188
189    /// Check if the registry has a provider for type `T` with the given `token`.
190    pub fn has_with_token<T: 'static>(&self, token: &str) -> bool {
191        self.providers.contains_key(&Self::make_key::<T>(token))
192    }
193
194    /// Resolve a value of type `T` for the given `token`.
195    ///
196    /// Returns `None` if no provider is registered for `(T, token)`.
197    /// Returns `Some(Err(..))` if the provider fails.
198    ///
199    /// For the default token (`""`), also falls back to `InjectableArcFactory`
200    /// inventory entries so that `#[injectable]` types are resolvable via the
201    /// external path even without an explicit `DynProvider` registration.
202    pub(crate) async fn resolve_with_token<T: Send + Sync + 'static>(
203        &self,
204        token: &str,
205        ctx: Arc<ResolveContext>,
206    ) -> Option<InjectableResult<T>> {
207        let key = Self::make_key::<T>(token);
208
209        // 1. Check explicitly registered DynProvider<T> for this token
210        if let Some(provider) = self.providers.get(&key) {
211            let result = provider.provide_as_any(Arc::clone(&ctx)).await;
212            return Some(
213                result.and_then(|boxed| match boxed.downcast::<T>() {
214                    Ok(t) => Ok(*t),
215                    Err(_) => Err(InjectableError::ConstructionFailed {
216                        type_name: std::any::type_name::<T>(),
217                        reason: "downcast failed (this should never happen with correct TypeId)"
218                            .to_string(),
219                    }),
220                }),
221            );
222        }
223
224        // 2. For the default token only: fall back to InjectableArcFactory entries.
225        //    These are submitted at compile time for every #[injectable] type.
226        if token == DEFAULT_TOKEN {
227            let target_id = TypeId::of::<T>();
228            for factory in inventory::iter::<InjectableArcFactory>() {
229                if factory.type_id() == target_id {
230                    let result = factory.provide(ctx).await;
231                    return Some(result.and_then(|boxed| match boxed.downcast::<T>() {
232                        Ok(t) => Ok(*t),
233                        Err(_) => Err(InjectableError::ConstructionFailed {
234                            type_name: std::any::type_name::<T>(),
235                            reason: "InjectableArcFactory downcast failed".to_string(),
236                        }),
237                    }));
238                }
239            }
240        }
241
242        None
243    }
244
245    /// Returns the total number of registered providers (across all tokens).
246    pub fn len(&self) -> usize {
247        self.providers.len()
248    }
249
250    /// Returns `true` if no providers are registered.
251    pub fn is_empty(&self) -> bool {
252        self.providers.is_empty()
253    }
254}
255
256impl Default for ProviderRegistry {
257    fn default() -> Self {
258        Self::new()
259    }
260}
261
262impl std::fmt::Debug for ProviderRegistry {
263    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264        f.debug_struct("ProviderRegistry")
265            .field("count", &self.providers.len())
266            .finish()
267    }
268}
269
270/// Type alias for the type-erased provide function pointer stored in
271/// inventory-submitted [`InjectableArcFactory`] entries.
272pub type InjectableProvideFnPtr = fn(
273    std::sync::Arc<ResolveContext>,
274) -> std::pin::Pin<
275    Box<
276        dyn std::future::Future<Output = InjectableResult<Box<dyn std::any::Any + Send>>>
277            + Send
278            + 'static,
279    >,
280>;
281
282/// An entry submitted to the inventory by `#[injectable_impl]` and
283/// `#[derive(Injectable)]` macros, allowing Injectable types to be resolved
284/// via the same `try_resolve_external` path as DynProvider-registered types.
285pub struct InjectableArcFactory {
286    /// The type name as a `&'static str`, used for introspection and diagnostics.
287    pub type_name: &'static str,
288    type_id_fn: fn() -> std::any::TypeId,
289    provide_fn: InjectableProvideFnPtr,
290}
291
292impl InjectableArcFactory {
293    /// Create a new factory entry.
294    pub const fn new_const(
295        type_name: &'static str,
296        type_id_fn: fn() -> std::any::TypeId,
297        provide_fn: InjectableProvideFnPtr,
298    ) -> Self {
299        Self {
300            type_name,
301            type_id_fn,
302            provide_fn,
303        }
304    }
305
306    /// Return the `TypeId` of the Injectable type this entry was created for.
307    pub fn type_id(&self) -> std::any::TypeId {
308        (self.type_id_fn)()
309    }
310
311    /// Invoke the provider function and return a type-erased result.
312    pub fn provide(&self, ctx: std::sync::Arc<ResolveContext>) -> ErasedProviderPinnedFuture<'_> {
313        (self.provide_fn)(ctx)
314    }
315}
316
317inventory::collect!(InjectableArcFactory);
318
319// ─── InjectableHooksEntry ────────────────────────────────────────────────────
320
321/// Function pointer that receives a type-erased `Arc<T>` and calls the
322/// `#[injectable(post_construct)]` hook(s) on the instance.
323pub type PostConstructFnPtr = fn(
324    std::sync::Arc<dyn std::any::Any + std::marker::Send + std::marker::Sync>,
325) -> std::pin::Pin<
326    Box<dyn std::future::Future<Output = crate::HookResult> + std::marker::Send + 'static>,
327>;
328
329/// Function pointer that receives a type-erased `Arc<T>` and returns an
330/// `Arc<dyn PreDestruct>` adapter suitable for registering with the context.
331pub type MakePreDestructFnPtr = fn(
332    std::sync::Arc<dyn std::any::Any + std::marker::Send + std::marker::Sync>,
333) -> std::sync::Arc<dyn crate::PreDestruct>;
334
335/// An inventory entry that carries lifecycle hook function pointers for one
336/// Injectable type.
337pub struct InjectableHooksEntry {
338    type_id_fn: fn() -> std::any::TypeId,
339    post_construct_fn: Option<PostConstructFnPtr>,
340    make_pre_destruct_fn: Option<MakePreDestructFnPtr>,
341}
342
343impl InjectableHooksEntry {
344    /// Create a new hooks entry.
345    pub const fn new_const(
346        type_id_fn: fn() -> std::any::TypeId,
347        post_construct_fn: Option<PostConstructFnPtr>,
348        make_pre_destruct_fn: Option<MakePreDestructFnPtr>,
349    ) -> Self {
350        Self {
351            type_id_fn,
352            post_construct_fn,
353            make_pre_destruct_fn,
354        }
355    }
356
357    /// `TypeId` of the Injectable type this entry belongs to.
358    pub fn type_id(&self) -> std::any::TypeId {
359        (self.type_id_fn)()
360    }
361
362    /// Returns the post-construct hook function, if any.
363    pub fn post_construct_fn(&self) -> Option<PostConstructFnPtr> {
364        self.post_construct_fn
365    }
366
367    /// Returns the pre-destruct adapter factory, if any.
368    pub fn make_pre_destruct_fn(&self) -> Option<MakePreDestructFnPtr> {
369        self.make_pre_destruct_fn
370    }
371}
372
373inventory::collect!(InjectableHooksEntry);
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378    use crate::DynProvider;
379
380    #[test]
381    fn new_registry_is_empty() {
382        let r = ProviderRegistry::new();
383        assert!(r.is_empty());
384        assert_eq!(r.len(), 0);
385    }
386
387    #[test]
388    fn has_returns_false_for_unregistered() {
389        let r = ProviderRegistry::new();
390        assert!(!r.has::<u32>());
391        assert!(!r.has_with_token::<u32>("primary"));
392    }
393
394    #[test]
395    fn has_returns_true_after_register_default() {
396        let mut r = ProviderRegistry::new();
397        r.register("", DynProvider::from_value(42u32));
398        assert!(r.has::<u32>());
399        assert!(r.has_with_token::<u32>(""));
400        assert!(!r.has_with_token::<u32>("other"));
401        assert_eq!(r.len(), 1);
402        assert!(!r.is_empty());
403    }
404
405    #[test]
406    fn has_returns_true_after_register_named() {
407        let mut r = ProviderRegistry::new();
408        r.register("primary", DynProvider::from_value(42u32));
409        assert!(!r.has::<u32>(), "default token should be absent");
410        assert!(r.has_with_token::<u32>("primary"));
411        assert!(!r.has_with_token::<u32>("replica"));
412    }
413
414    #[test]
415    fn multiple_tokens_same_type_coexist() {
416        let mut r = ProviderRegistry::new();
417        r.register("primary", DynProvider::from_value(1u32));
418        r.register("replica", DynProvider::from_value(2u32));
419        r.register("", DynProvider::from_value(0u32));
420        assert_eq!(r.len(), 3);
421        assert!(r.has::<u32>());
422        assert!(r.has_with_token::<u32>("primary"));
423        assert!(r.has_with_token::<u32>("replica"));
424    }
425
426    #[test]
427    fn duplicate_same_token_is_recorded() {
428        let mut r = ProviderRegistry::new();
429        r.register("", DynProvider::from_value(1u32));
430        r.register("", DynProvider::from_value(2u32));
431        assert_eq!(r.len(), 1); // replaced, not added
432        assert_eq!(r.duplicates().len(), 1);
433    }
434
435    #[test]
436    fn duplicate_different_tokens_not_recorded() {
437        let mut r = ProviderRegistry::new();
438        r.register("primary", DynProvider::from_value(1u32));
439        r.register("replica", DynProvider::from_value(2u32));
440        assert_eq!(
441            r.duplicates().len(),
442            0,
443            "different tokens are not duplicates"
444        );
445    }
446
447    #[test]
448    fn register_or_replace_does_not_record_duplicate() {
449        let mut r = ProviderRegistry::new();
450        r.register("", DynProvider::from_value(1u32));
451        r.register_or_replace("", DynProvider::from_value(2u32));
452        assert_eq!(r.duplicates().len(), 0);
453    }
454
455    #[test]
456    fn debug_shows_count() {
457        let mut r = ProviderRegistry::new();
458        r.register("", DynProvider::from_value(0u8));
459        let s = format!("{r:?}");
460        assert!(s.contains("ProviderRegistry"));
461        assert!(s.contains('1'));
462    }
463
464    #[test]
465    fn default_creates_empty() {
466        let r = ProviderRegistry::default();
467        assert!(r.is_empty());
468    }
469
470    #[test]
471    fn duplicate_label_includes_token_for_named() {
472        let label = ProviderRegistry::duplicate_label::<u32>("primary");
473        assert!(label.contains("primary"));
474        assert!(label.contains("u32"));
475    }
476
477    #[test]
478    fn duplicate_label_no_token_suffix_for_default() {
479        let label = ProviderRegistry::duplicate_label::<u32>("");
480        assert!(!label.contains('['), "default token adds no bracket suffix");
481    }
482}