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