Skip to main content

nestforge_core/
module.rs

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
31/*
32ControllerBasePath = metadata implemented by #[controller("/...")]
33*/
34pub trait ControllerBasePath {
35    fn base_path() -> &'static str;
36}
37
38/*
39ControllerDefinition = generated by #[routes] for a controller
40*/
41pub 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
342/*
343ModuleDefinition = app module contract (manual for now)
344*/
345pub 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}