injectable_rs_runtime/singleton.rs
1//! Singleton storage trait — the basis for generated typed storage.
2//!
3//! Instead of `HashMap<TypeId, Box<dyn Any>>`, the framework generates
4//! a struct with typed `OnceCell` fields. This trait provides the
5//! common interface for the generated storage.
6
7// Intentionally does NOT import `std::any::Any` or `std::any::TypeId`.
8// This crate's core principle is to avoid TypeId-based dynamic resolution.
9
10/// Trait for generated typed singleton stores.
11///
12/// The `#[derive(Injectable)]` macro contributes to a single generated
13/// store struct that has one `OnceCell<Arc<T>>` per singleton-scoped
14/// injectable type.
15///
16/// # Design Rationale
17///
18/// Traditional DI containers use `HashMap<TypeId, Box<dyn Any>>` which
19/// requires:
20/// - Runtime type lookup
21/// - Dynamic downcasting with `Any::downcast_ref`
22/// - Loss of compiler guarantees
23///
24/// Our generated store uses named fields with concrete types:
25///
26/// ```rust,ignore
27/// pub struct AppSingletonStore {
28/// database: OnceCell<Arc<Database>>,
29/// cache: OnceCell<Arc<Cache>>,
30/// }
31///
32/// impl AppSingletonStore {
33/// pub async fn database(&self, ctx: &ResolveContext) -> Arc<Database> { ... }
34/// pub async fn cache(&self, ctx: &ResolveContext) -> Arc<Cache> { ... }
35/// }
36/// ```
37///
38/// This is completely typed, zero-cost, and requires no `Any` or `TypeId`.
39pub trait SingletonStore: Send + Sync + 'static {
40 /// Returns the number of singleton entries in the store.
41 fn len(&self) -> usize;
42
43 /// Returns `true` if the store contains no entries.
44 fn is_empty(&self) -> bool {
45 self.len() == 0
46 }
47
48 /// Validate all singleton entries at startup.
49 ///
50 /// This performs basic sanity checks (e.g., no unresolved
51 /// references) and is called during container build.
52 fn validate(&self) -> Result<(), String> {
53 Ok(())
54 }
55}
56
57/// A minimal empty singleton store for containers with no singletons.
58pub struct EmptySingletonStore;
59
60// ─── Type-safe scope markers ─────────────────────────────────────────────────
61//
62// Use these as the `scope=` argument in `#[injectable(scope=Singleton)]`.
63// They are zero-sized marker types — no runtime overhead.
64
65/// One instance per container (the default scope).
66pub struct Singleton;
67
68/// A fresh instance is created on every resolution.
69pub struct Transient;
70
71/// One instance per request/task (reserved for future use).
72pub struct RequestScoped;
73
74impl SingletonStore for EmptySingletonStore {
75 fn len(&self) -> usize {
76 0
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn empty_store_len_zero() {
86 let s = EmptySingletonStore;
87 assert_eq!(s.len(), 0);
88 assert!(s.is_empty());
89 }
90
91 #[test]
92 fn empty_store_validate_ok() {
93 let s = EmptySingletonStore;
94 assert!(s.validate().is_ok());
95 }
96
97 #[test]
98 fn scope_markers_are_zero_sized() {
99 assert_eq!(std::mem::size_of::<Singleton>(), 0);
100 assert_eq!(std::mem::size_of::<Transient>(), 0);
101 assert_eq!(std::mem::size_of::<RequestScoped>(), 0);
102 }
103}