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>(
424 container: &Container,
425) -> Result<InitializedModule> {
426 let mut state = ModuleGraphState::default();
427 visit_module(ModuleRef::of::<M>(), container, &mut state)?;
428 state.module_destroy_hooks.reverse();
429 state.application_shutdown_hooks.reverse();
430 Ok(InitializedModule {
431 controllers: state.controllers,
432 module_init_hooks: state.module_init_hooks,
433 module_destroy_hooks: state.module_destroy_hooks,
434 application_bootstrap_hooks: state.application_bootstrap_hooks,
435 application_shutdown_hooks: state.application_shutdown_hooks,
436 })
437}
438
439pub fn initialize_module_graph<M: ModuleDefinition>(
440 container: &Container,
441) -> Result<Vec<Router<Container>>> {
442 Ok(initialize_module_runtime::<M>(container)?.controllers)
443}
444
445pub fn collect_module_route_docs<M: ModuleDefinition>() -> Result<Vec<RouteDocumentation>> {
446 let mut state = DocumentationGraphState::default();
447 visit_module_docs(ModuleRef::of::<M>(), &mut state)?;
448 Ok(state.route_docs)
449}
450
451pub fn collect_module_graph<M: ModuleDefinition>() -> Result<ModuleGraphReport> {
452 let mut state = ModuleIntrospectionState::default();
453 visit_module_graph(ModuleRef::of::<M>(), &mut state)?;
454 Ok(ModuleGraphReport {
455 modules: state.modules,
456 })
457}
458
459#[derive(Default)]
460struct ModuleGraphState {
461 visited: HashSet<&'static str>,
462 visiting: HashSet<&'static str>,
463 stack: Vec<&'static str>,
464 controllers: Vec<Router<Container>>,
465 module_init_hooks: Vec<LifecycleHook>,
466 module_destroy_hooks: Vec<LifecycleHook>,
467 application_bootstrap_hooks: Vec<LifecycleHook>,
468 application_shutdown_hooks: Vec<LifecycleHook>,
469 global_modules: HashSet<&'static str>,
470}
471
472#[derive(Default)]
473struct DocumentationGraphState {
474 visited: HashSet<&'static str>,
475 visiting: HashSet<&'static str>,
476 stack: Vec<&'static str>,
477 route_docs: Vec<RouteDocumentation>,
478}
479
480#[derive(Default)]
481struct ModuleIntrospectionState {
482 visited: HashSet<&'static str>,
483 visiting: HashSet<&'static str>,
484 stack: Vec<&'static str>,
485 modules: Vec<ModuleGraphEntry>,
486}
487
488fn visit_module(
489 module: ModuleRef,
490 container: &Container,
491 state: &mut ModuleGraphState,
492) -> Result<()> {
493 if state.visited.contains(module.name) {
494 return Ok(());
495 }
496
497 if state.visiting.contains(module.name) {
498 let mut cycle = state.stack.clone();
499 cycle.push(module.name);
500 anyhow::bail!("Detected module import cycle: {}", cycle.join(" -> "));
501 }
502
503 state.visiting.insert(module.name);
504 state.stack.push(module.name);
505 framework_log_event("module_register", &[("module", module.name.to_string())]);
506
507 for imported in (module.imports)() {
508 visit_module(imported, container, state)?;
509 }
510
511 (module.register)(container)
512 .map_err(|err| anyhow::anyhow!("Failed to register module `{}`: {}", module.name, err))?;
513
514 state.controllers.extend((module.controllers)());
515 state.module_init_hooks.extend((module.on_module_init)());
516 state
517 .module_destroy_hooks
518 .extend((module.on_module_destroy)());
519 state
520 .application_bootstrap_hooks
521 .extend((module.on_application_bootstrap)());
522 state
523 .application_shutdown_hooks
524 .extend((module.on_application_shutdown)());
525
526 if (module.is_global)() {
527 state.global_modules.insert(module.name);
528 }
529
530 for export in (module.exports)() {
531 let is_registered = container.is_type_registered_name(export).map_err(|err| {
532 anyhow::anyhow!(
533 "Failed to verify exports for module `{}`: {}",
534 module.name,
535 err
536 )
537 })?;
538 if !is_registered {
539 anyhow::bail!(
540 "Module `{}` exports `{}` but that provider is not registered in the container",
541 module.name,
542 export
543 );
544 }
545 }
546
547 state.stack.pop();
548 state.visiting.remove(module.name);
549 state.visited.insert(module.name);
550
551 Ok(())
552}
553
554fn run_lifecycle_hooks(
555 phase: &'static str,
556 hooks: &[LifecycleHook],
557 container: &Container,
558) -> Result<()> {
559 for hook in hooks {
560 framework_log_event("lifecycle_hook_run", &[("phase", phase.to_string())]);
561 hook(container)?;
562 }
563
564 Ok(())
565}
566
567fn block_on_dynamic_registration<T, Fut>(future: Fut) -> Result<T>
568where
569 T: Send + 'static,
570 Fut: Future<Output = Result<T>> + Send + 'static,
571{
572 if tokio::runtime::Handle::try_current().is_ok() {
573 return std::thread::spawn(move || {
574 tokio::runtime::Builder::new_current_thread()
575 .enable_all()
576 .build()
577 .map_err(anyhow::Error::new)?
578 .block_on(future)
579 })
580 .join()
581 .map_err(|_| anyhow::anyhow!("Dynamic module async registration thread panicked"))?;
582 }
583
584 tokio::runtime::Builder::new_current_thread()
585 .enable_all()
586 .build()
587 .map_err(anyhow::Error::new)?
588 .block_on(future)
589}
590
591fn visit_module_docs(module: ModuleRef, state: &mut DocumentationGraphState) -> Result<()> {
592 if state.visited.contains(module.name) {
593 return Ok(());
594 }
595
596 if state.visiting.contains(module.name) {
597 let mut cycle = state.stack.clone();
598 cycle.push(module.name);
599 anyhow::bail!(
600 "Detected module import cycle while collecting route docs: {}",
601 cycle.join(" -> ")
602 );
603 }
604
605 state.visiting.insert(module.name);
606 state.stack.push(module.name);
607
608 for imported in (module.imports)() {
609 visit_module_docs(imported, state)?;
610 }
611
612 state.route_docs.extend((module.route_docs)());
613 state.stack.pop();
614 state.visiting.remove(module.name);
615 state.visited.insert(module.name);
616 Ok(())
617}
618
619fn visit_module_graph(module: ModuleRef, state: &mut ModuleIntrospectionState) -> Result<()> {
620 if state.visited.contains(module.name) {
621 return Ok(());
622 }
623
624 if state.visiting.contains(module.name) {
625 let mut cycle = state.stack.clone();
626 cycle.push(module.name);
627 anyhow::bail!(
628 "Detected module import cycle while collecting module graph: {}",
629 cycle.join(" -> ")
630 );
631 }
632
633 state.visiting.insert(module.name);
634 state.stack.push(module.name);
635
636 let imports = (module.imports)();
637 let import_names = imports.iter().map(|import| import.name).collect::<Vec<_>>();
638
639 for imported in imports {
640 visit_module_graph(imported, state)?;
641 }
642
643 state.modules.push(ModuleGraphEntry {
644 name: module.name,
645 imports: import_names,
646 exports: (module.exports)(),
647 controller_count: (module.controllers)().len(),
648 is_global: (module.is_global)(),
649 });
650
651 state.stack.pop();
652 state.visiting.remove(module.name);
653 state.visited.insert(module.name);
654 Ok(())
655}
656
657#[cfg(test)]
658mod tests {
659 use super::*;
660 use std::sync::{Arc, Mutex};
661
662 struct ImportedConfig;
663 struct AppService;
664
665 struct ImportedModule;
666 impl ModuleDefinition for ImportedModule {
667 fn register(container: &Container) -> Result<()> {
668 container.register(ImportedConfig)?;
669 Ok(())
670 }
671 }
672
673 struct AppModule;
674 impl ModuleDefinition for AppModule {
675 fn imports() -> Vec<ModuleRef> {
676 vec![ModuleRef::of::<ImportedModule>()]
677 }
678
679 fn register(container: &Container) -> Result<()> {
680 let imported = container.resolve::<ImportedConfig>()?;
681 container.register(AppService::from(imported))?;
682 Ok(())
683 }
684 }
685
686 impl From<std::sync::Arc<ImportedConfig>> for AppService {
687 fn from(_: std::sync::Arc<ImportedConfig>) -> Self {
688 Self
689 }
690 }
691
692 #[test]
693 fn registers_imported_modules_before_local_providers() {
694 let container = Container::new();
695 let result = initialize_module_graph::<AppModule>(&container);
696
697 assert!(result.is_ok(), "module graph registration should succeed");
698 assert!(container.resolve::<ImportedConfig>().is_ok());
699 assert!(container.resolve::<AppService>().is_ok());
700 }
701
702 struct SharedImportedModule;
703 impl ModuleDefinition for SharedImportedModule {
704 fn register(container: &Container) -> Result<()> {
705 container.register(SharedMarker)?;
706 Ok(())
707 }
708 }
709
710 struct SharedMarker;
711 struct LeftModule;
712 struct RightModule;
713 struct RootModule;
714
715 impl ModuleDefinition for LeftModule {
716 fn imports() -> Vec<ModuleRef> {
717 vec![ModuleRef::of::<SharedImportedModule>()]
718 }
719
720 fn register(_container: &Container) -> Result<()> {
721 Ok(())
722 }
723 }
724
725 impl ModuleDefinition for RightModule {
726 fn imports() -> Vec<ModuleRef> {
727 vec![ModuleRef::of::<SharedImportedModule>()]
728 }
729
730 fn register(_container: &Container) -> Result<()> {
731 Ok(())
732 }
733 }
734
735 impl ModuleDefinition for RootModule {
736 fn imports() -> Vec<ModuleRef> {
737 vec![
738 ModuleRef::of::<LeftModule>(),
739 ModuleRef::of::<RightModule>(),
740 ]
741 }
742
743 fn register(_container: &Container) -> Result<()> {
744 Ok(())
745 }
746 }
747
748 #[test]
749 fn deduplicates_shared_imported_modules() {
750 let container = Container::new();
751 let result = initialize_module_graph::<RootModule>(&container);
752
753 assert!(
754 result.is_ok(),
755 "shared imported modules should only register once"
756 );
757 assert!(container.resolve::<SharedMarker>().is_ok());
758 }
759
760 struct CycleA;
761 struct CycleB;
762
763 impl ModuleDefinition for CycleA {
764 fn imports() -> Vec<ModuleRef> {
765 vec![ModuleRef::of::<CycleB>()]
766 }
767
768 fn register(_container: &Container) -> Result<()> {
769 Ok(())
770 }
771 }
772
773 impl ModuleDefinition for CycleB {
774 fn imports() -> Vec<ModuleRef> {
775 vec![ModuleRef::of::<CycleA>()]
776 }
777
778 fn register(_container: &Container) -> Result<()> {
779 Ok(())
780 }
781 }
782
783 #[test]
784 fn detects_module_import_cycles() {
785 let container = Container::new();
786 let err = initialize_module_graph::<CycleA>(&container).unwrap_err();
787
788 assert!(
789 err.to_string().contains("Detected module import cycle"),
790 "error should include cycle detection message"
791 );
792 }
793
794 struct MissingDependency;
795 struct BrokenModule;
796
797 impl ModuleDefinition for BrokenModule {
798 fn register(container: &Container) -> Result<()> {
799 let _ = container.resolve_in_module::<MissingDependency>(Self::module_name())?;
800 Ok(())
801 }
802 }
803
804 #[test]
805 fn module_registration_error_includes_module_and_type_context() {
806 let container = Container::new();
807 let err = initialize_module_graph::<BrokenModule>(&container).unwrap_err();
808 let message = err.to_string();
809
810 assert!(message.contains("Failed to register module"));
811 assert!(message.contains("BrokenModule"));
812 assert!(message.contains("MissingDependency"));
813 }
814
815 #[derive(Clone)]
816 struct HookLog(Arc<Mutex<Vec<&'static str>>>);
817
818 fn push_hook(container: &Container, label: &'static str) -> Result<()> {
819 let log = container.resolve::<HookLog>()?;
820 log.0
821 .lock()
822 .expect("hook log should be writable")
823 .push(label);
824 Ok(())
825 }
826
827 fn hook_init(container: &Container) -> Result<()> {
828 push_hook(container, "module_init")
829 }
830
831 fn hook_bootstrap(container: &Container) -> Result<()> {
832 push_hook(container, "application_bootstrap")
833 }
834
835 fn hook_destroy(container: &Container) -> Result<()> {
836 push_hook(container, "module_destroy")
837 }
838
839 fn hook_shutdown(container: &Container) -> Result<()> {
840 push_hook(container, "application_shutdown")
841 }
842
843 struct HookModule;
844
845 impl ModuleDefinition for HookModule {
846 fn register(container: &Container) -> Result<()> {
847 container.register(HookLog(Arc::new(Mutex::new(Vec::new()))))?;
848 Ok(())
849 }
850
851 fn on_module_init() -> Vec<LifecycleHook> {
852 vec![hook_init]
853 }
854
855 fn on_application_bootstrap() -> Vec<LifecycleHook> {
856 vec![hook_bootstrap]
857 }
858
859 fn on_module_destroy() -> Vec<LifecycleHook> {
860 vec![hook_destroy]
861 }
862
863 fn on_application_shutdown() -> Vec<LifecycleHook> {
864 vec![hook_shutdown]
865 }
866 }
867
868 #[test]
869 fn initialized_runtime_runs_lifecycle_hooks_in_expected_order() {
870 let container = Container::new();
871 let runtime = initialize_module_runtime::<HookModule>(&container)
872 .expect("module runtime should initialize");
873
874 runtime
875 .run_module_init(&container)
876 .expect("module init hooks should run");
877 runtime
878 .run_application_bootstrap(&container)
879 .expect("application bootstrap hooks should run");
880 runtime
881 .run_module_destroy(&container)
882 .expect("module destroy hooks should run");
883 runtime
884 .run_application_shutdown(&container)
885 .expect("application shutdown hooks should run");
886
887 let log = container
888 .resolve::<HookLog>()
889 .expect("hook log should resolve");
890 let entries = log.0.lock().expect("hook log should be readable").clone();
891
892 assert_eq!(
893 entries,
894 vec![
895 "module_init",
896 "application_bootstrap",
897 "module_destroy",
898 "application_shutdown"
899 ]
900 );
901 }
902
903 #[derive(Clone)]
904 struct DynamicConfig {
905 label: &'static str,
906 }
907
908 struct DynamicRootModule;
909
910 impl ModuleDefinition for DynamicRootModule {
911 fn imports() -> Vec<ModuleRef> {
912 let config = DynamicConfig { label: "captured" };
913 vec![ModuleRef::dynamic("DynamicConfigModule", move |container| {
914 container.register(config.clone())?;
915 Ok(())
916 })
917 .with_exports(|| vec![std::any::type_name::<DynamicConfig>()])]
918 }
919
920 fn register(_container: &Container) -> Result<()> {
921 Ok(())
922 }
923 }
924
925 #[test]
926 fn dynamic_module_ref_can_capture_runtime_configuration() {
927 let container = Container::new();
928 initialize_module_graph::<DynamicRootModule>(&container)
929 .expect("dynamic module graph should initialize");
930
931 let config = container
932 .resolve::<DynamicConfig>()
933 .expect("dynamic config should resolve");
934 assert_eq!(config.label, "captured");
935 }
936
937 struct BuilderRootModule;
938
939 impl ModuleDefinition for BuilderRootModule {
940 fn imports() -> Vec<ModuleRef> {
941 vec![ModuleRef::builder("BuilderConfigModule")
942 .provider_value(DynamicConfig { label: "builder" })
943 .export::<DynamicConfig>()
944 .build()]
945 }
946
947 fn register(_container: &Container) -> Result<()> {
948 Ok(())
949 }
950 }
951
952 #[test]
953 fn dynamic_module_builder_registers_typed_exports() {
954 let container = Container::new();
955 initialize_module_graph::<BuilderRootModule>(&container)
956 .expect("builder-based dynamic module should initialize");
957
958 let config = container
959 .resolve::<DynamicConfig>()
960 .expect("builder config should resolve");
961 assert_eq!(config.label, "builder");
962 }
963
964 #[derive(Clone)]
965 struct AsyncConfig {
966 label: &'static str,
967 }
968
969 struct AsyncRootModule;
970
971 impl ModuleDefinition for AsyncRootModule {
972 fn imports() -> Vec<ModuleRef> {
973 vec![ModuleRef::builder("AsyncConfigModule")
974 .provider_async(|_container| async { Ok(AsyncConfig { label: "async" }) })
975 .export::<AsyncConfig>()
976 .build()]
977 }
978
979 fn register(_container: &Container) -> Result<()> {
980 Ok(())
981 }
982 }
983
984 #[test]
985 fn dynamic_module_builder_supports_async_provider_registration() {
986 let container = Container::new();
987 initialize_module_graph::<AsyncRootModule>(&container)
988 .expect("async dynamic module should initialize");
989
990 let config = container
991 .resolve::<AsyncConfig>()
992 .expect("async config should resolve");
993 assert_eq!(config.label, "async");
994 }
995
996 #[test]
997 fn collects_module_graph_report_entries() {
998 let report = collect_module_graph::<RootModule>().expect("module graph should collect");
999 let root = report
1000 .module(std::any::type_name::<RootModule>())
1001 .expect("root module should exist");
1002
1003 assert_eq!(root.imports.len(), 2);
1004 assert_eq!(report.modules.len(), 4);
1005 }
1006}