1use anyhow::Result;
2use axum::Router;
3use std::{collections::HashSet, future::Future, sync::Arc};
4
5use crate::{
6 framework_log_event, Container, DocumentedController, RegisterProvider, RouteDocumentation,
7};
8
9pub type LifecycleHook = fn(&Container) -> Result<()>;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct ModuleGraphEntry {
13 pub name: &'static str,
14 pub imports: Vec<&'static str>,
15 pub exports: Vec<&'static str>,
16 pub controller_count: usize,
17 pub is_global: bool,
18}
19
20#[derive(Debug, Clone, Default, PartialEq, Eq)]
21pub struct ModuleGraphReport {
22 pub modules: Vec<ModuleGraphEntry>,
23}
24
25impl ModuleGraphReport {
26 pub fn module(&self, name: &str) -> Option<&ModuleGraphEntry> {
27 self.modules.iter().find(|module| module.name == name)
28 }
29}
30
31pub trait ControllerBasePath {
35 fn base_path() -> &'static str;
36}
37
38pub trait ControllerDefinition: Send + Sync + 'static {
42 fn router() -> Router<Container>;
43}
44
45#[derive(Clone)]
46pub struct ModuleRef {
47 pub name: &'static str,
48 pub register: Arc<dyn Fn(&Container) -> Result<()> + Send + Sync>,
49 pub controllers: Arc<dyn Fn() -> Vec<Router<Container>> + Send + Sync>,
50 pub route_docs: Arc<dyn Fn() -> Vec<RouteDocumentation> + Send + Sync>,
51 pub on_module_init: Arc<dyn Fn() -> Vec<LifecycleHook> + Send + Sync>,
52 pub on_module_destroy: Arc<dyn Fn() -> Vec<LifecycleHook> + Send + Sync>,
53 pub on_application_bootstrap: Arc<dyn Fn() -> Vec<LifecycleHook> + Send + Sync>,
54 pub on_application_shutdown: Arc<dyn Fn() -> Vec<LifecycleHook> + Send + Sync>,
55 pub imports: Arc<dyn Fn() -> Vec<ModuleRef> + Send + Sync>,
56 pub exports: Arc<dyn Fn() -> Vec<&'static str> + Send + Sync>,
57 pub is_global: Arc<dyn Fn() -> bool + Send + Sync>,
58}
59
60impl ModuleRef {
61 pub fn of<M: ModuleDefinition>() -> Self {
62 Self {
63 name: M::module_name(),
64 register: Arc::new(M::register),
65 controllers: Arc::new(M::controllers),
66 route_docs: Arc::new(M::route_docs),
67 on_module_init: Arc::new(M::on_module_init),
68 on_module_destroy: Arc::new(M::on_module_destroy),
69 on_application_bootstrap: Arc::new(M::on_application_bootstrap),
70 on_application_shutdown: Arc::new(M::on_application_shutdown),
71 imports: Arc::new(M::imports),
72 exports: Arc::new(M::exports),
73 is_global: Arc::new(M::is_global),
74 }
75 }
76
77 pub fn dynamic(
78 name: &'static str,
79 register: impl Fn(&Container) -> Result<()> + Send + Sync + 'static,
80 ) -> Self {
81 Self {
82 name,
83 register: Arc::new(register),
84 controllers: Arc::new(Vec::new),
85 route_docs: Arc::new(Vec::new),
86 on_module_init: Arc::new(Vec::new),
87 on_module_destroy: Arc::new(Vec::new),
88 on_application_bootstrap: Arc::new(Vec::new),
89 on_application_shutdown: Arc::new(Vec::new),
90 imports: Arc::new(Vec::new),
91 exports: Arc::new(Vec::new),
92 is_global: Arc::new(|| false),
93 }
94 }
95
96 pub fn builder(name: &'static str) -> DynamicModuleBuilder {
97 DynamicModuleBuilder::new(name)
98 }
99
100 pub fn with_imports(
101 mut self,
102 imports: impl Fn() -> Vec<ModuleRef> + Send + Sync + 'static,
103 ) -> Self {
104 self.imports = Arc::new(imports);
105 self
106 }
107
108 pub fn with_exports(
109 mut self,
110 exports: impl Fn() -> Vec<&'static str> + Send + Sync + 'static,
111 ) -> Self {
112 self.exports = Arc::new(exports);
113 self
114 }
115
116 pub fn with_controllers(
117 mut self,
118 controllers: impl Fn() -> Vec<Router<Container>> + Send + Sync + 'static,
119 ) -> Self {
120 self.controllers = Arc::new(controllers);
121 self
122 }
123
124 pub fn with_route_docs(
125 mut self,
126 route_docs: impl Fn() -> Vec<RouteDocumentation> + Send + Sync + 'static,
127 ) -> Self {
128 self.route_docs = Arc::new(route_docs);
129 self
130 }
131
132 pub fn with_module_init_hooks(
133 mut self,
134 hooks: impl Fn() -> Vec<LifecycleHook> + Send + Sync + 'static,
135 ) -> Self {
136 self.on_module_init = Arc::new(hooks);
137 self
138 }
139
140 pub fn with_module_destroy_hooks(
141 mut self,
142 hooks: impl Fn() -> Vec<LifecycleHook> + Send + Sync + 'static,
143 ) -> Self {
144 self.on_module_destroy = Arc::new(hooks);
145 self
146 }
147
148 pub fn with_application_bootstrap_hooks(
149 mut self,
150 hooks: impl Fn() -> Vec<LifecycleHook> + Send + Sync + 'static,
151 ) -> Self {
152 self.on_application_bootstrap = Arc::new(hooks);
153 self
154 }
155
156 pub fn with_application_shutdown_hooks(
157 mut self,
158 hooks: impl Fn() -> Vec<LifecycleHook> + Send + Sync + 'static,
159 ) -> Self {
160 self.on_application_shutdown = Arc::new(hooks);
161 self
162 }
163
164 pub fn as_global(mut self) -> Self {
165 self.is_global = Arc::new(|| true);
166 self
167 }
168
169 pub fn with_is_global(mut self, is_global: bool) -> Self {
170 self.is_global = Arc::new(move || is_global);
171 self
172 }
173}
174
175type RegistrationStep = Arc<dyn Fn(&Container) -> Result<()> + Send + Sync>;
176
177pub struct DynamicModuleBuilder {
178 name: &'static str,
179 registration_steps: Vec<RegistrationStep>,
180 imports: Vec<ModuleRef>,
181 exports: Vec<&'static str>,
182 controllers: Vec<Router<Container>>,
183 route_docs: Vec<RouteDocumentation>,
184 on_module_init: Vec<LifecycleHook>,
185 on_module_destroy: Vec<LifecycleHook>,
186 on_application_bootstrap: Vec<LifecycleHook>,
187 on_application_shutdown: Vec<LifecycleHook>,
188 is_global: bool,
189}
190
191impl DynamicModuleBuilder {
192 pub fn new(name: &'static str) -> Self {
193 Self {
194 name,
195 registration_steps: Vec::new(),
196 imports: Vec::new(),
197 exports: Vec::new(),
198 controllers: Vec::new(),
199 route_docs: Vec::new(),
200 on_module_init: Vec::new(),
201 on_module_destroy: Vec::new(),
202 on_application_bootstrap: Vec::new(),
203 on_application_shutdown: Vec::new(),
204 is_global: false,
205 }
206 }
207
208 pub fn register(
209 mut self,
210 register: impl Fn(&Container) -> Result<()> + Send + Sync + 'static,
211 ) -> Self {
212 self.registration_steps.push(Arc::new(register));
213 self
214 }
215
216 pub fn register_provider<P>(self, provider: P) -> Self
217 where
218 P: RegisterProvider + Send + 'static,
219 {
220 let provider = Arc::new(std::sync::Mutex::new(Some(provider)));
221 self.register(move |container| {
222 let provider = provider
223 .lock()
224 .map_err(|_| anyhow::anyhow!("Dynamic module provider lock poisoned"))?
225 .take()
226 .ok_or_else(|| anyhow::anyhow!("Dynamic module provider already registered"))?;
227 provider.register(container)
228 })
229 }
230
231 pub fn provider_value<T>(self, value: T) -> Self
232 where
233 T: Clone + Send + Sync + 'static,
234 {
235 self.register(move |container| {
236 container.register(value.clone())?;
237 Ok(())
238 })
239 }
240
241 pub fn provider_factory<T, F>(self, factory: F) -> Self
242 where
243 T: Send + Sync + 'static,
244 F: Fn(&Container) -> Result<T> + Send + Sync + 'static,
245 {
246 self.register(move |container| {
247 container.register(factory(container)?)?;
248 Ok(())
249 })
250 }
251
252 pub fn provider_async<T, F, Fut>(self, factory: F) -> Self
253 where
254 T: Send + Sync + 'static,
255 F: Fn(Container) -> Fut + Send + Sync + 'static,
256 Fut: Future<Output = Result<T>> + Send + 'static,
257 {
258 self.register(move |container| {
259 let value = block_on_dynamic_registration(factory(container.clone()))?;
260 container.register(value)?;
261 Ok(())
262 })
263 }
264
265 pub fn import(mut self, module: ModuleRef) -> Self {
266 self.imports.push(module);
267 self
268 }
269
270 pub fn export<T>(mut self) -> Self
271 where
272 T: 'static,
273 {
274 self.exports.push(std::any::type_name::<T>());
275 self
276 }
277
278 pub fn controller<C>(mut self) -> Self
279 where
280 C: ControllerDefinition + DocumentedController,
281 {
282 self.controllers.push(C::router());
283 self.route_docs.extend(C::route_docs());
284 self
285 }
286
287 pub fn on_module_init(mut self, hook: LifecycleHook) -> Self {
288 self.on_module_init.push(hook);
289 self
290 }
291
292 pub fn on_module_destroy(mut self, hook: LifecycleHook) -> Self {
293 self.on_module_destroy.push(hook);
294 self
295 }
296
297 pub fn on_application_bootstrap(mut self, hook: LifecycleHook) -> Self {
298 self.on_application_bootstrap.push(hook);
299 self
300 }
301
302 pub fn on_application_shutdown(mut self, hook: LifecycleHook) -> Self {
303 self.on_application_shutdown.push(hook);
304 self
305 }
306
307 pub fn global(mut self) -> Self {
308 self.is_global = true;
309 self
310 }
311
312 pub fn build(self) -> ModuleRef {
313 let registration_steps = Arc::new(self.registration_steps);
314 let imports = self.imports;
315 let exports = self.exports;
316 let controllers = self.controllers;
317 let route_docs = self.route_docs;
318 let on_module_init = self.on_module_init;
319 let on_module_destroy = self.on_module_destroy;
320 let on_application_bootstrap = self.on_application_bootstrap;
321 let on_application_shutdown = self.on_application_shutdown;
322 let is_global = self.is_global;
323
324 ModuleRef::dynamic(self.name, move |container| {
325 for step in registration_steps.iter() {
326 step(container)?;
327 }
328 Ok(())
329 })
330 .with_imports(move || imports.clone())
331 .with_exports(move || exports.clone())
332 .with_controllers(move || controllers.clone())
333 .with_route_docs(move || route_docs.clone())
334 .with_module_init_hooks(move || on_module_init.clone())
335 .with_module_destroy_hooks(move || on_module_destroy.clone())
336 .with_application_bootstrap_hooks(move || on_application_bootstrap.clone())
337 .with_application_shutdown_hooks(move || on_application_shutdown.clone())
338 .with_is_global(is_global)
339 }
340}
341
342pub trait ModuleDefinition: Send + Sync + 'static {
346 fn module_name() -> &'static str {
347 std::any::type_name::<Self>()
348 }
349
350 fn register(container: &Container) -> Result<()>;
351
352 fn imports() -> Vec<ModuleRef> {
353 Vec::new()
354 }
355
356 fn is_global() -> bool {
357 false
358 }
359
360 fn exports() -> Vec<&'static str> {
361 Vec::new()
362 }
363
364 fn controllers() -> Vec<Router<Container>> {
365 Vec::new()
366 }
367
368 fn route_docs() -> Vec<RouteDocumentation> {
369 Vec::new()
370 }
371
372 fn on_module_init() -> Vec<LifecycleHook> {
373 Vec::new()
374 }
375
376 fn on_module_destroy() -> Vec<LifecycleHook> {
377 Vec::new()
378 }
379
380 fn on_application_bootstrap() -> Vec<LifecycleHook> {
381 Vec::new()
382 }
383
384 fn on_application_shutdown() -> Vec<LifecycleHook> {
385 Vec::new()
386 }
387}
388
389pub struct InitializedModule {
390 pub controllers: Vec<Router<Container>>,
391 module_init_hooks: Vec<LifecycleHook>,
392 module_destroy_hooks: Vec<LifecycleHook>,
393 application_bootstrap_hooks: Vec<LifecycleHook>,
394 application_shutdown_hooks: Vec<LifecycleHook>,
395}
396
397impl InitializedModule {
398 pub fn run_module_init(&self, container: &Container) -> Result<()> {
399 run_lifecycle_hooks("module_init", &self.module_init_hooks, container)
400 }
401
402 pub fn run_module_destroy(&self, container: &Container) -> Result<()> {
403 run_lifecycle_hooks("module_destroy", &self.module_destroy_hooks, container)
404 }
405
406 pub fn run_application_bootstrap(&self, container: &Container) -> Result<()> {
407 run_lifecycle_hooks(
408 "application_bootstrap",
409 &self.application_bootstrap_hooks,
410 container,
411 )
412 }
413
414 pub fn run_application_shutdown(&self, container: &Container) -> Result<()> {
415 run_lifecycle_hooks(
416 "application_shutdown",
417 &self.application_shutdown_hooks,
418 container,
419 )
420 }
421}
422
423pub fn initialize_module_runtime<M: ModuleDefinition>(container: &Container) -> Result<InitializedModule> {
424 let mut state = ModuleGraphState::default();
425 visit_module(ModuleRef::of::<M>(), container, &mut state)?;
426 state.module_destroy_hooks.reverse();
427 state.application_shutdown_hooks.reverse();
428 Ok(InitializedModule {
429 controllers: state.controllers,
430 module_init_hooks: state.module_init_hooks,
431 module_destroy_hooks: state.module_destroy_hooks,
432 application_bootstrap_hooks: state.application_bootstrap_hooks,
433 application_shutdown_hooks: state.application_shutdown_hooks,
434 })
435}
436
437pub fn initialize_module_graph<M: ModuleDefinition>(
438 container: &Container,
439) -> Result<Vec<Router<Container>>> {
440 Ok(initialize_module_runtime::<M>(container)?.controllers)
441}
442
443pub fn collect_module_route_docs<M: ModuleDefinition>() -> Result<Vec<RouteDocumentation>> {
444 let mut state = DocumentationGraphState::default();
445 visit_module_docs(ModuleRef::of::<M>(), &mut state)?;
446 Ok(state.route_docs)
447}
448
449pub fn collect_module_graph<M: ModuleDefinition>() -> Result<ModuleGraphReport> {
450 let mut state = ModuleIntrospectionState::default();
451 visit_module_graph(ModuleRef::of::<M>(), &mut state)?;
452 Ok(ModuleGraphReport {
453 modules: state.modules,
454 })
455}
456
457#[derive(Default)]
458struct ModuleGraphState {
459 visited: HashSet<&'static str>,
460 visiting: HashSet<&'static str>,
461 stack: Vec<&'static str>,
462 controllers: Vec<Router<Container>>,
463 module_init_hooks: Vec<LifecycleHook>,
464 module_destroy_hooks: Vec<LifecycleHook>,
465 application_bootstrap_hooks: Vec<LifecycleHook>,
466 application_shutdown_hooks: Vec<LifecycleHook>,
467 global_modules: HashSet<&'static str>,
468}
469
470#[derive(Default)]
471struct DocumentationGraphState {
472 visited: HashSet<&'static str>,
473 visiting: HashSet<&'static str>,
474 stack: Vec<&'static str>,
475 route_docs: Vec<RouteDocumentation>,
476}
477
478#[derive(Default)]
479struct ModuleIntrospectionState {
480 visited: HashSet<&'static str>,
481 visiting: HashSet<&'static str>,
482 stack: Vec<&'static str>,
483 modules: Vec<ModuleGraphEntry>,
484}
485
486fn visit_module(
487 module: ModuleRef,
488 container: &Container,
489 state: &mut ModuleGraphState,
490) -> Result<()> {
491 if state.visited.contains(module.name) {
492 return Ok(());
493 }
494
495 if state.visiting.contains(module.name) {
496 let mut cycle = state.stack.clone();
497 cycle.push(module.name);
498 anyhow::bail!("Detected module import cycle: {}", cycle.join(" -> "));
499 }
500
501 state.visiting.insert(module.name);
502 state.stack.push(module.name);
503 framework_log_event(
504 "module_register",
505 &[("module", module.name.to_string())],
506 );
507
508 for imported in (module.imports)() {
509 visit_module(imported, container, state)?;
510 }
511
512 (module.register)(container)
513 .map_err(|err| anyhow::anyhow!("Failed to register module `{}`: {}", module.name, err))?;
514
515 state.controllers.extend((module.controllers)());
516 state.module_init_hooks.extend((module.on_module_init)());
517 state.module_destroy_hooks.extend((module.on_module_destroy)());
518 state
519 .application_bootstrap_hooks
520 .extend((module.on_application_bootstrap)());
521 state
522 .application_shutdown_hooks
523 .extend((module.on_application_shutdown)());
524
525 if (module.is_global)() {
526 state.global_modules.insert(module.name);
527 }
528
529 for export in (module.exports)() {
530 let is_registered = container.is_type_registered_name(export).map_err(|err| {
531 anyhow::anyhow!(
532 "Failed to verify exports for module `{}`: {}",
533 module.name,
534 err
535 )
536 })?;
537 if !is_registered {
538 anyhow::bail!(
539 "Module `{}` exports `{}` but that provider is not registered in the container",
540 module.name,
541 export
542 );
543 }
544 }
545
546 state.stack.pop();
547 state.visiting.remove(module.name);
548 state.visited.insert(module.name);
549
550 Ok(())
551}
552
553fn run_lifecycle_hooks(
554 phase: &'static str,
555 hooks: &[LifecycleHook],
556 container: &Container,
557) -> Result<()> {
558 for hook in hooks {
559 framework_log_event("lifecycle_hook_run", &[("phase", phase.to_string())]);
560 hook(container)?;
561 }
562
563 Ok(())
564}
565
566fn block_on_dynamic_registration<T, Fut>(future: Fut) -> Result<T>
567where
568 T: Send + 'static,
569 Fut: Future<Output = Result<T>> + Send + 'static,
570{
571 if tokio::runtime::Handle::try_current().is_ok() {
572 return std::thread::spawn(move || {
573 tokio::runtime::Builder::new_current_thread()
574 .enable_all()
575 .build()
576 .map_err(anyhow::Error::new)?
577 .block_on(future)
578 })
579 .join()
580 .map_err(|_| anyhow::anyhow!("Dynamic module async registration thread panicked"))?;
581 }
582
583 tokio::runtime::Builder::new_current_thread()
584 .enable_all()
585 .build()
586 .map_err(anyhow::Error::new)?
587 .block_on(future)
588}
589
590fn visit_module_docs(module: ModuleRef, state: &mut DocumentationGraphState) -> Result<()> {
591 if state.visited.contains(module.name) {
592 return Ok(());
593 }
594
595 if state.visiting.contains(module.name) {
596 let mut cycle = state.stack.clone();
597 cycle.push(module.name);
598 anyhow::bail!(
599 "Detected module import cycle while collecting route docs: {}",
600 cycle.join(" -> ")
601 );
602 }
603
604 state.visiting.insert(module.name);
605 state.stack.push(module.name);
606
607 for imported in (module.imports)() {
608 visit_module_docs(imported, state)?;
609 }
610
611 state.route_docs.extend((module.route_docs)());
612 state.stack.pop();
613 state.visiting.remove(module.name);
614 state.visited.insert(module.name);
615 Ok(())
616}
617
618fn visit_module_graph(module: ModuleRef, state: &mut ModuleIntrospectionState) -> Result<()> {
619 if state.visited.contains(module.name) {
620 return Ok(());
621 }
622
623 if state.visiting.contains(module.name) {
624 let mut cycle = state.stack.clone();
625 cycle.push(module.name);
626 anyhow::bail!(
627 "Detected module import cycle while collecting module graph: {}",
628 cycle.join(" -> ")
629 );
630 }
631
632 state.visiting.insert(module.name);
633 state.stack.push(module.name);
634
635 let imports = (module.imports)();
636 let import_names = imports.iter().map(|import| import.name).collect::<Vec<_>>();
637
638 for imported in imports {
639 visit_module_graph(imported, state)?;
640 }
641
642 state.modules.push(ModuleGraphEntry {
643 name: module.name,
644 imports: import_names,
645 exports: (module.exports)(),
646 controller_count: (module.controllers)().len(),
647 is_global: (module.is_global)(),
648 });
649
650 state.stack.pop();
651 state.visiting.remove(module.name);
652 state.visited.insert(module.name);
653 Ok(())
654}
655
656#[cfg(test)]
657mod tests {
658 use super::*;
659 use std::sync::{Arc, Mutex};
660
661 struct ImportedConfig;
662 struct AppService;
663
664 struct ImportedModule;
665 impl ModuleDefinition for ImportedModule {
666 fn register(container: &Container) -> Result<()> {
667 container.register(ImportedConfig)?;
668 Ok(())
669 }
670 }
671
672 struct AppModule;
673 impl ModuleDefinition for AppModule {
674 fn imports() -> Vec<ModuleRef> {
675 vec![ModuleRef::of::<ImportedModule>()]
676 }
677
678 fn register(container: &Container) -> Result<()> {
679 let imported = container.resolve::<ImportedConfig>()?;
680 container.register(AppService::from(imported))?;
681 Ok(())
682 }
683 }
684
685 impl From<std::sync::Arc<ImportedConfig>> for AppService {
686 fn from(_: std::sync::Arc<ImportedConfig>) -> Self {
687 Self
688 }
689 }
690
691 #[test]
692 fn registers_imported_modules_before_local_providers() {
693 let container = Container::new();
694 let result = initialize_module_graph::<AppModule>(&container);
695
696 assert!(result.is_ok(), "module graph registration should succeed");
697 assert!(container.resolve::<ImportedConfig>().is_ok());
698 assert!(container.resolve::<AppService>().is_ok());
699 }
700
701 struct SharedImportedModule;
702 impl ModuleDefinition for SharedImportedModule {
703 fn register(container: &Container) -> Result<()> {
704 container.register(SharedMarker)?;
705 Ok(())
706 }
707 }
708
709 struct SharedMarker;
710 struct LeftModule;
711 struct RightModule;
712 struct RootModule;
713
714 impl ModuleDefinition for LeftModule {
715 fn imports() -> Vec<ModuleRef> {
716 vec![ModuleRef::of::<SharedImportedModule>()]
717 }
718
719 fn register(_container: &Container) -> Result<()> {
720 Ok(())
721 }
722 }
723
724 impl ModuleDefinition for RightModule {
725 fn imports() -> Vec<ModuleRef> {
726 vec![ModuleRef::of::<SharedImportedModule>()]
727 }
728
729 fn register(_container: &Container) -> Result<()> {
730 Ok(())
731 }
732 }
733
734 impl ModuleDefinition for RootModule {
735 fn imports() -> Vec<ModuleRef> {
736 vec![
737 ModuleRef::of::<LeftModule>(),
738 ModuleRef::of::<RightModule>(),
739 ]
740 }
741
742 fn register(_container: &Container) -> Result<()> {
743 Ok(())
744 }
745 }
746
747 #[test]
748 fn deduplicates_shared_imported_modules() {
749 let container = Container::new();
750 let result = initialize_module_graph::<RootModule>(&container);
751
752 assert!(
753 result.is_ok(),
754 "shared imported modules should only register once"
755 );
756 assert!(container.resolve::<SharedMarker>().is_ok());
757 }
758
759 struct CycleA;
760 struct CycleB;
761
762 impl ModuleDefinition for CycleA {
763 fn imports() -> Vec<ModuleRef> {
764 vec![ModuleRef::of::<CycleB>()]
765 }
766
767 fn register(_container: &Container) -> Result<()> {
768 Ok(())
769 }
770 }
771
772 impl ModuleDefinition for CycleB {
773 fn imports() -> Vec<ModuleRef> {
774 vec![ModuleRef::of::<CycleA>()]
775 }
776
777 fn register(_container: &Container) -> Result<()> {
778 Ok(())
779 }
780 }
781
782 #[test]
783 fn detects_module_import_cycles() {
784 let container = Container::new();
785 let err = initialize_module_graph::<CycleA>(&container).unwrap_err();
786
787 assert!(
788 err.to_string().contains("Detected module import cycle"),
789 "error should include cycle detection message"
790 );
791 }
792
793 struct MissingDependency;
794 struct BrokenModule;
795
796 impl ModuleDefinition for BrokenModule {
797 fn register(container: &Container) -> Result<()> {
798 let _ = container.resolve_in_module::<MissingDependency>(Self::module_name())?;
799 Ok(())
800 }
801 }
802
803 #[test]
804 fn module_registration_error_includes_module_and_type_context() {
805 let container = Container::new();
806 let err = initialize_module_graph::<BrokenModule>(&container).unwrap_err();
807 let message = err.to_string();
808
809 assert!(message.contains("Failed to register module"));
810 assert!(message.contains("BrokenModule"));
811 assert!(message.contains("MissingDependency"));
812 }
813
814 #[derive(Clone)]
815 struct HookLog(Arc<Mutex<Vec<&'static str>>>);
816
817 fn push_hook(container: &Container, label: &'static str) -> Result<()> {
818 let log = container.resolve::<HookLog>()?;
819 log.0.lock().expect("hook log should be writable").push(label);
820 Ok(())
821 }
822
823 fn hook_init(container: &Container) -> Result<()> {
824 push_hook(container, "module_init")
825 }
826
827 fn hook_bootstrap(container: &Container) -> Result<()> {
828 push_hook(container, "application_bootstrap")
829 }
830
831 fn hook_destroy(container: &Container) -> Result<()> {
832 push_hook(container, "module_destroy")
833 }
834
835 fn hook_shutdown(container: &Container) -> Result<()> {
836 push_hook(container, "application_shutdown")
837 }
838
839 struct HookModule;
840
841 impl ModuleDefinition for HookModule {
842 fn register(container: &Container) -> Result<()> {
843 container.register(HookLog(Arc::new(Mutex::new(Vec::new()))))?;
844 Ok(())
845 }
846
847 fn on_module_init() -> Vec<LifecycleHook> {
848 vec![hook_init]
849 }
850
851 fn on_application_bootstrap() -> Vec<LifecycleHook> {
852 vec![hook_bootstrap]
853 }
854
855 fn on_module_destroy() -> Vec<LifecycleHook> {
856 vec![hook_destroy]
857 }
858
859 fn on_application_shutdown() -> Vec<LifecycleHook> {
860 vec![hook_shutdown]
861 }
862 }
863
864 #[test]
865 fn initialized_runtime_runs_lifecycle_hooks_in_expected_order() {
866 let container = Container::new();
867 let runtime = initialize_module_runtime::<HookModule>(&container)
868 .expect("module runtime should initialize");
869
870 runtime
871 .run_module_init(&container)
872 .expect("module init hooks should run");
873 runtime
874 .run_application_bootstrap(&container)
875 .expect("application bootstrap hooks should run");
876 runtime
877 .run_module_destroy(&container)
878 .expect("module destroy hooks should run");
879 runtime
880 .run_application_shutdown(&container)
881 .expect("application shutdown hooks should run");
882
883 let log = container.resolve::<HookLog>().expect("hook log should resolve");
884 let entries = log.0.lock().expect("hook log should be readable").clone();
885
886 assert_eq!(
887 entries,
888 vec![
889 "module_init",
890 "application_bootstrap",
891 "module_destroy",
892 "application_shutdown"
893 ]
894 );
895 }
896
897 #[derive(Clone)]
898 struct DynamicConfig {
899 label: &'static str,
900 }
901
902 struct DynamicRootModule;
903
904 impl ModuleDefinition for DynamicRootModule {
905 fn imports() -> Vec<ModuleRef> {
906 let config = DynamicConfig { label: "captured" };
907 vec![ModuleRef::dynamic("DynamicConfigModule", move |container| {
908 container.register(config.clone())?;
909 Ok(())
910 })
911 .with_exports(|| vec![std::any::type_name::<DynamicConfig>()])]
912 }
913
914 fn register(_container: &Container) -> Result<()> {
915 Ok(())
916 }
917 }
918
919 #[test]
920 fn dynamic_module_ref_can_capture_runtime_configuration() {
921 let container = Container::new();
922 initialize_module_graph::<DynamicRootModule>(&container)
923 .expect("dynamic module graph should initialize");
924
925 let config = container
926 .resolve::<DynamicConfig>()
927 .expect("dynamic config should resolve");
928 assert_eq!(config.label, "captured");
929 }
930
931 struct BuilderRootModule;
932
933 impl ModuleDefinition for BuilderRootModule {
934 fn imports() -> Vec<ModuleRef> {
935 vec![ModuleRef::builder("BuilderConfigModule")
936 .provider_value(DynamicConfig { label: "builder" })
937 .export::<DynamicConfig>()
938 .build()]
939 }
940
941 fn register(_container: &Container) -> Result<()> {
942 Ok(())
943 }
944 }
945
946 #[test]
947 fn dynamic_module_builder_registers_typed_exports() {
948 let container = Container::new();
949 initialize_module_graph::<BuilderRootModule>(&container)
950 .expect("builder-based dynamic module should initialize");
951
952 let config = container
953 .resolve::<DynamicConfig>()
954 .expect("builder config should resolve");
955 assert_eq!(config.label, "builder");
956 }
957
958 #[derive(Clone)]
959 struct AsyncConfig {
960 label: &'static str,
961 }
962
963 struct AsyncRootModule;
964
965 impl ModuleDefinition for AsyncRootModule {
966 fn imports() -> Vec<ModuleRef> {
967 vec![ModuleRef::builder("AsyncConfigModule")
968 .provider_async(|_container| async { Ok(AsyncConfig { label: "async" }) })
969 .export::<AsyncConfig>()
970 .build()]
971 }
972
973 fn register(_container: &Container) -> Result<()> {
974 Ok(())
975 }
976 }
977
978 #[test]
979 fn dynamic_module_builder_supports_async_provider_registration() {
980 let container = Container::new();
981 initialize_module_graph::<AsyncRootModule>(&container)
982 .expect("async dynamic module should initialize");
983
984 let config = container
985 .resolve::<AsyncConfig>()
986 .expect("async config should resolve");
987 assert_eq!(config.label, "async");
988 }
989
990 #[test]
991 fn collects_module_graph_report_entries() {
992 let report = collect_module_graph::<RootModule>().expect("module graph should collect");
993 let root = report
994 .module(std::any::type_name::<RootModule>())
995 .expect("root module should exist");
996
997 assert_eq!(root.imports.len(), 2);
998 assert_eq!(report.modules.len(), 4);
999 }
1000}