kit_rs/container/mod.rs
1//! Application Container for Dependency Injection
2//!
3//! This module provides Laravel-like service container capabilities:
4//! - Singletons: shared instances across the application
5//! - Factories: new instance per resolution
6//! - Trait bindings: bind interfaces to implementations
7//! - Test faking: swap implementations in tests
8//! - Service Providers: bootstrap services with register/boot lifecycle
9//!
10//! # Example
11//!
12//! ```rust,ignore
13//! use kit::{App, bind, singleton, service};
14//!
15//! // Define a service trait with auto-registration
16//! #[service(RealHttpClient)]
17//! pub trait HttpClient {
18//! async fn get(&self, url: &str) -> Result<String, Error>;
19//! }
20//!
21//! // Or register manually using macros
22//! bind!(dyn HttpClient, RealHttpClient::new());
23//! singleton!(CacheService::new());
24//!
25//! // Resolve anywhere in your app
26//! let client: Arc<dyn HttpClient> = App::make::<dyn HttpClient>().unwrap();
27//! ```
28
29pub mod provider;
30pub mod testing;
31
32use std::any::{Any, TypeId};
33use std::cell::RefCell;
34use std::collections::HashMap;
35use std::sync::{Arc, OnceLock, RwLock};
36
37/// Global application container
38static APP_CONTAINER: OnceLock<RwLock<Container>> = OnceLock::new();
39
40// Thread-local test overrides for isolated testing
41thread_local! {
42 pub(crate) static TEST_CONTAINER: RefCell<Option<Container>> = const { RefCell::new(None) };
43}
44
45/// Binding types: either a singleton instance or a factory closure
46#[derive(Clone)]
47enum Binding {
48 /// Shared singleton instance - same instance returned every time
49 Singleton(Arc<dyn Any + Send + Sync>),
50
51 /// Factory closure - creates new instance each time
52 Factory(Arc<dyn Fn() -> Arc<dyn Any + Send + Sync> + Send + Sync>),
53}
54
55/// The main service container
56///
57/// Stores type-erased bindings keyed by TypeId. Supports both concrete types
58/// and trait objects (via Arc<dyn Trait>).
59pub struct Container {
60 /// Type bindings: TypeId -> Binding
61 bindings: HashMap<TypeId, Binding>,
62}
63
64impl Container {
65 /// Create a new empty container
66 pub fn new() -> Self {
67 Self {
68 bindings: HashMap::new(),
69 }
70 }
71
72 /// Register a singleton instance (shared across all resolutions)
73 ///
74 /// # Example
75 /// ```rust,ignore
76 /// container.singleton(DatabaseConnection::new(&url));
77 /// ```
78 pub fn singleton<T: Any + Send + Sync + 'static>(&mut self, instance: T) {
79 let arc: Arc<dyn Any + Send + Sync> = Arc::new(instance);
80 self.bindings.insert(TypeId::of::<T>(), Binding::Singleton(arc));
81 }
82
83 /// Register a factory closure (new instance per resolution)
84 ///
85 /// # Example
86 /// ```rust,ignore
87 /// container.factory(|| RequestLogger::new());
88 /// ```
89 pub fn factory<T, F>(&mut self, factory: F)
90 where
91 T: Any + Send + Sync + 'static,
92 F: Fn() -> T + Send + Sync + 'static,
93 {
94 let wrapped: Arc<dyn Fn() -> Arc<dyn Any + Send + Sync> + Send + Sync> =
95 Arc::new(move || Arc::new(factory()) as Arc<dyn Any + Send + Sync>);
96 self.bindings
97 .insert(TypeId::of::<T>(), Binding::Factory(wrapped));
98 }
99
100 /// Bind a trait object to a concrete implementation (as singleton)
101 ///
102 /// This stores the value under `TypeId::of::<Arc<dyn Trait>>()` which allows
103 /// trait objects to be resolved via `make::<dyn Trait>()`.
104 ///
105 /// # Example
106 /// ```rust,ignore
107 /// container.bind::<dyn HttpClient>(RealHttpClient::new());
108 /// ```
109 pub fn bind<T: ?Sized + Send + Sync + 'static>(&mut self, instance: Arc<T>) {
110 // Store under TypeId of Arc<T> (works for both concrete and trait objects)
111 let type_id = TypeId::of::<Arc<T>>();
112 let arc: Arc<dyn Any + Send + Sync> = Arc::new(instance);
113 self.bindings.insert(type_id, Binding::Singleton(arc));
114 }
115
116 /// Bind a trait object to a factory
117 ///
118 /// # Example
119 /// ```rust,ignore
120 /// container.bind_factory::<dyn HttpClient>(|| Arc::new(RealHttpClient::new()));
121 /// ```
122 pub fn bind_factory<T: ?Sized + Send + Sync + 'static, F>(&mut self, factory: F)
123 where
124 F: Fn() -> Arc<T> + Send + Sync + 'static,
125 {
126 let type_id = TypeId::of::<Arc<T>>();
127 let wrapped: Arc<dyn Fn() -> Arc<dyn Any + Send + Sync> + Send + Sync> =
128 Arc::new(move || Arc::new(factory()) as Arc<dyn Any + Send + Sync>);
129 self.bindings.insert(type_id, Binding::Factory(wrapped));
130 }
131
132 /// Resolve a concrete type (requires Clone)
133 ///
134 /// # Example
135 /// ```rust,ignore
136 /// let db: DatabaseConnection = container.get().unwrap();
137 /// ```
138 pub fn get<T: Any + Send + Sync + Clone + 'static>(&self) -> Option<T> {
139 match self.bindings.get(&TypeId::of::<T>())? {
140 Binding::Singleton(arc) => arc.downcast_ref::<T>().cloned(),
141 Binding::Factory(factory) => {
142 let arc = factory();
143 arc.downcast_ref::<T>().cloned()
144 }
145 }
146 }
147
148 /// Resolve a trait binding - returns Arc<T>
149 ///
150 /// # Example
151 /// ```rust,ignore
152 /// let client: Arc<dyn HttpClient> = container.make::<dyn HttpClient>().unwrap();
153 /// ```
154 pub fn make<T: ?Sized + Send + Sync + 'static>(&self) -> Option<Arc<T>> {
155 let type_id = TypeId::of::<Arc<T>>();
156 match self.bindings.get(&type_id)? {
157 Binding::Singleton(arc) => {
158 // The stored value is Arc<Arc<T>>, so we downcast and clone the inner Arc
159 arc.downcast_ref::<Arc<T>>().cloned()
160 }
161 Binding::Factory(factory) => {
162 let arc = factory();
163 arc.downcast_ref::<Arc<T>>().cloned()
164 }
165 }
166 }
167
168 /// Check if a concrete type is registered
169 pub fn has<T: Any + 'static>(&self) -> bool {
170 self.bindings.contains_key(&TypeId::of::<T>())
171 }
172
173 /// Check if a trait binding is registered
174 pub fn has_binding<T: ?Sized + 'static>(&self) -> bool {
175 self.bindings.contains_key(&TypeId::of::<Arc<T>>())
176 }
177}
178
179impl Default for Container {
180 fn default() -> Self {
181 Self::new()
182 }
183}
184
185/// Application container facade
186///
187/// Provides static methods for service registration and resolution.
188/// Uses a global container with thread-local test overrides.
189///
190/// # Example
191///
192/// ```rust,ignore
193/// use kit::{App, bind, singleton};
194///
195/// // Register services at startup using macros
196/// singleton!(DatabaseConnection::new(&url));
197/// bind!(dyn HttpClient, RealHttpClient::new());
198///
199/// // Resolve anywhere
200/// let db: DatabaseConnection = App::get().unwrap();
201/// let client: Arc<dyn HttpClient> = App::make::<dyn HttpClient>().unwrap();
202/// ```
203pub struct App;
204
205impl App {
206 /// Initialize the application container
207 ///
208 /// Should be called once at application startup. This is automatically
209 /// called by `Server::from_config()`.
210 pub fn init() {
211 APP_CONTAINER.get_or_init(|| RwLock::new(Container::new()));
212 }
213
214 /// Register a singleton instance (shared across all resolutions)
215 ///
216 /// # Example
217 /// ```rust,ignore
218 /// App::singleton(DatabaseConnection::new(&url));
219 /// ```
220 pub fn singleton<T: Any + Send + Sync + 'static>(instance: T) {
221 let container = APP_CONTAINER.get_or_init(|| RwLock::new(Container::new()));
222 if let Ok(mut c) = container.write() {
223 c.singleton(instance);
224 }
225 }
226
227 /// Register a factory binding (new instance per resolution)
228 ///
229 /// # Example
230 /// ```rust,ignore
231 /// App::factory(|| RequestLogger::new());
232 /// ```
233 pub fn factory<T, F>(factory: F)
234 where
235 T: Any + Send + Sync + 'static,
236 F: Fn() -> T + Send + Sync + 'static,
237 {
238 let container = APP_CONTAINER.get_or_init(|| RwLock::new(Container::new()));
239 if let Ok(mut c) = container.write() {
240 c.factory(factory);
241 }
242 }
243
244 /// Bind a trait object to a concrete implementation (as singleton)
245 ///
246 /// # Example
247 /// ```rust,ignore
248 /// App::bind::<dyn HttpClient>(Arc::new(RealHttpClient::new()));
249 /// ```
250 pub fn bind<T: ?Sized + Send + Sync + 'static>(instance: Arc<T>) {
251 let container = APP_CONTAINER.get_or_init(|| RwLock::new(Container::new()));
252 if let Ok(mut c) = container.write() {
253 c.bind(instance);
254 }
255 }
256
257 /// Bind a trait object to a factory
258 ///
259 /// # Example
260 /// ```rust,ignore
261 /// App::bind_factory::<dyn HttpClient>(|| Arc::new(RealHttpClient::new()));
262 /// ```
263 pub fn bind_factory<T: ?Sized + Send + Sync + 'static, F>(factory: F)
264 where
265 F: Fn() -> Arc<T> + Send + Sync + 'static,
266 {
267 let container = APP_CONTAINER.get_or_init(|| RwLock::new(Container::new()));
268 if let Ok(mut c) = container.write() {
269 c.bind_factory(factory);
270 }
271 }
272
273 /// Resolve a concrete type
274 ///
275 /// Checks test overrides first, then falls back to global container.
276 ///
277 /// # Example
278 /// ```rust,ignore
279 /// let db: DatabaseConnection = App::get().unwrap();
280 /// ```
281 pub fn get<T: Any + Send + Sync + Clone + 'static>() -> Option<T> {
282 // Check test overrides first (thread-local)
283 let test_result = TEST_CONTAINER.with(|c| {
284 c.borrow()
285 .as_ref()
286 .and_then(|container| container.get::<T>())
287 });
288
289 if test_result.is_some() {
290 return test_result;
291 }
292
293 // Fall back to global container
294 let container = APP_CONTAINER.get()?;
295 container.read().ok()?.get::<T>()
296 }
297
298 /// Resolve a trait binding - returns Arc<T>
299 ///
300 /// Checks test overrides first, then falls back to global container.
301 ///
302 /// # Example
303 /// ```rust,ignore
304 /// let client: Arc<dyn HttpClient> = App::make::<dyn HttpClient>().unwrap();
305 /// ```
306 pub fn make<T: ?Sized + Send + Sync + 'static>() -> Option<Arc<T>> {
307 // Check test overrides first (thread-local)
308 let test_result = TEST_CONTAINER.with(|c| {
309 c.borrow()
310 .as_ref()
311 .and_then(|container| container.make::<T>())
312 });
313
314 if test_result.is_some() {
315 return test_result;
316 }
317
318 // Fall back to global container
319 let container = APP_CONTAINER.get()?;
320 container.read().ok()?.make::<T>()
321 }
322
323 /// Check if a concrete type is registered
324 pub fn has<T: Any + 'static>() -> bool {
325 // Check test container first
326 let in_test = TEST_CONTAINER.with(|c| {
327 c.borrow()
328 .as_ref()
329 .map(|container| container.has::<T>())
330 .unwrap_or(false)
331 });
332
333 if in_test {
334 return true;
335 }
336
337 APP_CONTAINER
338 .get()
339 .and_then(|c| c.read().ok())
340 .map(|c| c.has::<T>())
341 .unwrap_or(false)
342 }
343
344 /// Check if a trait binding is registered
345 pub fn has_binding<T: ?Sized + 'static>() -> bool {
346 // Check test container first
347 let in_test = TEST_CONTAINER.with(|c| {
348 c.borrow()
349 .as_ref()
350 .map(|container| container.has_binding::<T>())
351 .unwrap_or(false)
352 });
353
354 if in_test {
355 return true;
356 }
357
358 APP_CONTAINER
359 .get()
360 .and_then(|c| c.read().ok())
361 .map(|c| c.has_binding::<T>())
362 .unwrap_or(false)
363 }
364
365 /// Boot all auto-registered services
366 ///
367 /// This registers all services marked with `#[service(ConcreteType)]`.
368 /// Called automatically by `Server::from_config()`.
369 pub fn boot_services() {
370 provider::bootstrap();
371 }
372}
373
374/// Bind a trait to a singleton implementation (auto-wraps in Arc)
375///
376/// # Example
377/// ```rust,ignore
378/// bind!(dyn Database, PostgresDB::connect(&db_url));
379/// bind!(dyn HttpClient, RealHttpClient::new());
380/// ```
381#[macro_export]
382macro_rules! bind {
383 ($trait:ty, $instance:expr) => {
384 $crate::App::bind::<$trait>(::std::sync::Arc::new($instance) as ::std::sync::Arc<$trait>)
385 };
386}
387
388/// Bind a trait to a factory (auto-wraps in Arc, new instance each resolution)
389///
390/// # Example
391/// ```rust,ignore
392/// bind_factory!(dyn HttpClient, || RealHttpClient::new());
393/// ```
394#[macro_export]
395macro_rules! bind_factory {
396 ($trait:ty, $factory:expr) => {{
397 let f = $factory;
398 $crate::App::bind_factory::<$trait, _>(move || {
399 ::std::sync::Arc::new(f()) as ::std::sync::Arc<$trait>
400 })
401 }};
402}
403
404/// Register a singleton instance (concrete type)
405///
406/// # Example
407/// ```rust,ignore
408/// singleton!(DatabaseConnection::new(&url));
409/// ```
410#[macro_export]
411macro_rules! singleton {
412 ($instance:expr) => {
413 $crate::App::singleton($instance)
414 };
415}
416
417/// Register a factory (concrete type, new instance each resolution)
418///
419/// # Example
420/// ```rust,ignore
421/// factory!(|| RequestLogger::new());
422/// ```
423#[macro_export]
424macro_rules! factory {
425 ($factory:expr) => {
426 $crate::App::factory($factory)
427 };
428}