Skip to main content

modkit/
registry.rs

1// modkit/src/registry/mod.rs
2use std::collections::{HashMap, VecDeque};
3use std::sync::Arc;
4
5use thiserror::Error;
6
7// Re-exported contracts are referenced but not defined here.
8use crate::contracts;
9
10/// Type alias for REST host module configuration.
11type RestHostEntry = (&'static str, Arc<dyn contracts::ApiGatewayCapability>);
12
13// ============================================================================
14// Capability System
15// ============================================================================
16
17/// A single capability variant that a module can provide.
18#[derive(Clone)]
19pub enum Capability {
20    Database(Arc<dyn contracts::DatabaseCapability>),
21    RestApi(Arc<dyn contracts::RestApiCapability>),
22    ApiGateway(Arc<dyn contracts::ApiGatewayCapability>),
23    Runnable(Arc<dyn contracts::RunnableCapability>),
24    System(Arc<dyn contracts::SystemCapability>),
25    GrpcHub(Arc<dyn contracts::GrpcHubCapability>),
26    GrpcService(Arc<dyn contracts::GrpcServiceCapability>),
27}
28
29impl std::fmt::Debug for Capability {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        match self {
32            Capability::Database(_) => write!(f, "Database(<impl DatabaseCapability>)"),
33            Capability::RestApi(_) => write!(f, "RestApi(<impl RestApiCapability>)"),
34            Capability::ApiGateway(_) => write!(f, "ApiGateway(<impl ApiGatewayCapability>)"),
35            Capability::Runnable(_) => write!(f, "Runnable(<impl RunnableCapability>)"),
36            Capability::System(_) => write!(f, "System(<impl SystemCapability>)"),
37            Capability::GrpcHub(_) => write!(f, "GrpcHub(<impl GrpcHubCapability>)"),
38            Capability::GrpcService(_) => write!(f, "GrpcService(<impl GrpcServiceCapability>)"),
39        }
40    }
41}
42
43/// Trait for capability tags that allow type-safe querying.
44pub trait CapTag {
45    type Out: ?Sized + 'static;
46    fn try_get(cap: &Capability) -> Option<&Arc<Self::Out>>;
47}
48
49/// Tag for querying `DatabaseCapability`.
50pub struct DatabaseCap;
51impl CapTag for DatabaseCap {
52    type Out = dyn contracts::DatabaseCapability;
53    fn try_get(cap: &Capability) -> Option<&Arc<Self::Out>> {
54        match cap {
55            Capability::Database(v) => Some(v),
56            _ => None,
57        }
58    }
59}
60
61/// Tag for querying `RestApiCapability`.
62pub struct RestApiCap;
63impl CapTag for RestApiCap {
64    type Out = dyn contracts::RestApiCapability;
65    fn try_get(cap: &Capability) -> Option<&Arc<Self::Out>> {
66        match cap {
67            Capability::RestApi(v) => Some(v),
68            _ => None,
69        }
70    }
71}
72
73/// Tag for querying `ApiGatewayCapability`.
74pub struct ApiGatewayCap;
75impl CapTag for ApiGatewayCap {
76    type Out = dyn contracts::ApiGatewayCapability;
77    fn try_get(cap: &Capability) -> Option<&Arc<Self::Out>> {
78        match cap {
79            Capability::ApiGateway(v) => Some(v),
80            _ => None,
81        }
82    }
83}
84
85/// Tag for querying `RunnableCapability`.
86pub struct RunnableCap;
87impl CapTag for RunnableCap {
88    type Out = dyn contracts::RunnableCapability;
89    fn try_get(cap: &Capability) -> Option<&Arc<Self::Out>> {
90        match cap {
91            Capability::Runnable(v) => Some(v),
92            _ => None,
93        }
94    }
95}
96
97/// Tag for querying `SystemCapability`.
98pub struct SystemCap;
99impl CapTag for SystemCap {
100    type Out = dyn contracts::SystemCapability;
101    fn try_get(cap: &Capability) -> Option<&Arc<Self::Out>> {
102        match cap {
103            Capability::System(v) => Some(v),
104            _ => None,
105        }
106    }
107}
108
109/// Tag for querying `GrpcHubCapability`.
110pub struct GrpcHubCap;
111impl CapTag for GrpcHubCap {
112    type Out = dyn contracts::GrpcHubCapability;
113    fn try_get(cap: &Capability) -> Option<&Arc<Self::Out>> {
114        match cap {
115            Capability::GrpcHub(v) => Some(v),
116            _ => None,
117        }
118    }
119}
120
121/// Tag for querying `GrpcServiceCapability`.
122pub struct GrpcServiceCap;
123impl CapTag for GrpcServiceCap {
124    type Out = dyn contracts::GrpcServiceCapability;
125    fn try_get(cap: &Capability) -> Option<&Arc<Self::Out>> {
126        match cap {
127            Capability::GrpcService(v) => Some(v),
128            _ => None,
129        }
130    }
131}
132
133/// A set of capabilities that a module provides.
134#[derive(Clone)]
135pub struct CapabilitySet {
136    caps: Vec<Capability>,
137}
138
139impl std::fmt::Debug for CapabilitySet {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        f.debug_struct("CapabilitySet")
142            .field("caps", &self.caps)
143            .finish()
144    }
145}
146
147impl CapabilitySet {
148    /// Create an empty capability set.
149    #[must_use]
150    pub fn new() -> Self {
151        Self { caps: Vec::new() }
152    }
153
154    /// Add a capability to the set.
155    pub fn push(&mut self, cap: Capability) {
156        self.caps.push(cap);
157    }
158
159    /// Check if the set contains a specific capability type.
160    #[must_use]
161    pub fn has<T: CapTag>(&self) -> bool {
162        self.caps.iter().any(|cap| T::try_get(cap).is_some())
163    }
164
165    /// Query for a specific capability type.
166    #[must_use]
167    pub fn query<T: CapTag>(&self) -> Option<Arc<T::Out>> {
168        self.caps.iter().find_map(|cap| T::try_get(cap).cloned())
169    }
170}
171
172impl Default for CapabilitySet {
173    fn default() -> Self {
174        Self::new()
175    }
176}
177
178pub struct ModuleEntry {
179    pub(crate) name: &'static str,
180    pub(crate) deps: &'static [&'static str],
181    pub(crate) core: Arc<dyn contracts::Module>,
182    pub(crate) caps: CapabilitySet,
183}
184
185impl std::fmt::Debug for ModuleEntry {
186    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187        f.debug_struct("ModuleEntry")
188            .field("name", &self.name)
189            .field("deps", &self.deps)
190            .field("has_rest", &self.caps.has::<RestApiCap>())
191            .field("is_rest_host", &self.caps.has::<ApiGatewayCap>())
192            .field("has_db", &self.caps.has::<DatabaseCap>())
193            .field("has_stateful", &self.caps.has::<RunnableCap>())
194            .field("is_system", &self.caps.has::<SystemCap>())
195            .field("is_grpc_hub", &self.caps.has::<GrpcHubCap>())
196            .field("has_grpc_service", &self.caps.has::<GrpcServiceCap>())
197            .finish_non_exhaustive()
198    }
199}
200
201/// The function type submitted by the macro via `inventory::submit!`.
202/// NOTE: It now takes a *builder*, not the final registry.
203pub struct Registrator(pub fn(&mut RegistryBuilder));
204
205inventory::collect!(Registrator);
206
207/// The final, topo-sorted runtime registry.
208pub struct ModuleRegistry {
209    modules: Vec<ModuleEntry>, // topo-sorted
210    pub grpc_hub: Option<String>,
211    pub grpc_services: Vec<(String, Arc<dyn contracts::GrpcServiceCapability>)>,
212}
213
214impl std::fmt::Debug for ModuleRegistry {
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        let names: Vec<&'static str> = self.modules.iter().map(|m| m.name).collect();
217        f.debug_struct("ModuleRegistry")
218            .field("modules", &names)
219            .field("has_grpc_hub", &self.grpc_hub.is_some())
220            .field("grpc_services_count", &self.grpc_services.len())
221            .finish()
222    }
223}
224
225impl ModuleRegistry {
226    #[must_use]
227    pub fn modules(&self) -> &[ModuleEntry] {
228        &self.modules
229    }
230
231    /// Returns modules ordered by system priority.
232    /// System modules come first, followed by non-system modules.
233    /// Within each group, the original topological order is preserved.
234    #[must_use]
235    pub fn modules_by_system_priority(&self) -> Vec<&ModuleEntry> {
236        let mut system_mods = Vec::new();
237        let mut non_system_mods = Vec::new();
238
239        for entry in &self.modules {
240            if entry.caps.has::<SystemCap>() {
241                system_mods.push(entry);
242            } else {
243                non_system_mods.push(entry);
244            }
245        }
246
247        system_mods.extend(non_system_mods);
248        system_mods
249    }
250
251    /// Discover via inventory, have registrators fill the builder, then build & topo-sort.
252    ///
253    /// # Errors
254    /// Returns `RegistryError` if module discovery or dependency resolution fails.
255    pub fn discover_and_build() -> Result<Self, RegistryError> {
256        let mut b = RegistryBuilder::default();
257        for r in ::inventory::iter::<Registrator> {
258            r.0(&mut b);
259        }
260        b.build_topo_sorted()
261    }
262
263    /// (Optional) quick lookup if you need it.
264    #[must_use]
265    pub fn get_module(&self, name: &str) -> Option<Arc<dyn contracts::Module>> {
266        self.modules
267            .iter()
268            .find(|e| e.name == name)
269            .map(|e| e.core.clone())
270    }
271}
272
273/// Type alias for gRPC hub module configuration.
274type GrpcHubEntry = (&'static str, Arc<dyn contracts::GrpcHubCapability>);
275
276/// Internal builder that macro registrators will feed.
277/// Keys are module **names**; uniqueness enforced at build time.
278#[derive(Default)]
279pub struct RegistryBuilder {
280    core: HashMap<&'static str, Arc<dyn contracts::Module>>,
281    deps: HashMap<&'static str, &'static [&'static str]>,
282    capabilities: HashMap<&'static str, Vec<Capability>>,
283    rest_host: Option<RestHostEntry>,
284    grpc_hub: Option<GrpcHubEntry>,
285    errors: Vec<String>,
286}
287
288/// Type alias for dependency graph: (names, adjacency list, index map)
289type DependencyGraph = (
290    Vec<&'static str>,
291    Vec<Vec<usize>>,
292    HashMap<&'static str, usize>,
293);
294
295impl RegistryBuilder {
296    pub fn register_core_with_meta(
297        &mut self,
298        name: &'static str,
299        deps: &'static [&'static str],
300        m: Arc<dyn contracts::Module>,
301    ) {
302        if self.core.contains_key(name) {
303            self.errors
304                .push(format!("Module '{name}' is already registered"));
305            return;
306        }
307        self.core.insert(name, m);
308        self.deps.insert(name, deps);
309    }
310
311    pub fn register_rest_with_meta(
312        &mut self,
313        name: &'static str,
314        m: Arc<dyn contracts::RestApiCapability>,
315    ) {
316        self.capabilities
317            .entry(name)
318            .or_default()
319            .push(Capability::RestApi(m));
320    }
321
322    pub fn register_rest_host_with_meta(
323        &mut self,
324        name: &'static str,
325        m: Arc<dyn contracts::ApiGatewayCapability>,
326    ) {
327        if let Some((existing, _)) = &self.rest_host {
328            self.errors.push(format!(
329                "Multiple REST host modules detected: '{existing}' and '{name}'. Only one REST host is allowed."
330            ));
331            return;
332        }
333        self.rest_host = Some((name, m));
334    }
335
336    pub fn register_db_with_meta(
337        &mut self,
338        name: &'static str,
339        m: Arc<dyn contracts::DatabaseCapability>,
340    ) {
341        self.capabilities
342            .entry(name)
343            .or_default()
344            .push(Capability::Database(m));
345    }
346
347    pub fn register_stateful_with_meta(
348        &mut self,
349        name: &'static str,
350        m: Arc<dyn contracts::RunnableCapability>,
351    ) {
352        self.capabilities
353            .entry(name)
354            .or_default()
355            .push(Capability::Runnable(m));
356    }
357
358    pub fn register_system_with_meta(
359        &mut self,
360        name: &'static str,
361        m: Arc<dyn contracts::SystemCapability>,
362    ) {
363        self.capabilities
364            .entry(name)
365            .or_default()
366            .push(Capability::System(m));
367    }
368
369    pub fn register_grpc_hub_with_meta(
370        &mut self,
371        name: &'static str,
372        m: Arc<dyn contracts::GrpcHubCapability>,
373    ) {
374        if let Some((existing, _)) = &self.grpc_hub {
375            self.errors.push(format!(
376                "Multiple gRPC hub modules detected: '{existing}' and '{name}'. Only one gRPC hub is allowed."
377            ));
378            return;
379        }
380        self.grpc_hub = Some((name, m));
381    }
382
383    pub fn register_grpc_service_with_meta(
384        &mut self,
385        name: &'static str,
386        m: Arc<dyn contracts::GrpcServiceCapability>,
387    ) {
388        self.capabilities
389            .entry(name)
390            .or_default()
391            .push(Capability::GrpcService(m));
392    }
393
394    /// Detect cycles in the dependency graph using DFS with path tracking.
395    /// Returns the cycle path if found, None otherwise.
396    fn detect_cycle_with_path(
397        names: &[&'static str],
398        adj: &[Vec<usize>],
399    ) -> Option<Vec<&'static str>> {
400        #[derive(Clone, Copy, PartialEq)]
401        enum Color {
402            White, // unvisited
403            Gray,  // visiting (on current path)
404            Black, // visited (finished)
405        }
406
407        fn dfs(
408            node: usize,
409            names: &[&'static str],
410            adj: &[Vec<usize>],
411            colors: &mut [Color],
412            path: &mut Vec<usize>,
413        ) -> Option<Vec<&'static str>> {
414            colors[node] = Color::Gray;
415            path.push(node);
416
417            for &neighbor in &adj[node] {
418                match colors[neighbor] {
419                    Color::Gray => {
420                        // Found a back edge - cycle detected
421                        // Find the cycle start in the current path
422                        if let Some(cycle_start) = path.iter().position(|&n| n == neighbor) {
423                            let cycle_indices = &path[cycle_start..];
424                            let mut cycle_path: Vec<&'static str> =
425                                cycle_indices.iter().map(|&i| names[i]).collect();
426                            // Close the cycle by adding the first node again
427                            cycle_path.push(names[neighbor]);
428                            return Some(cycle_path);
429                        }
430                    }
431                    Color::White => {
432                        if let Some(cycle) = dfs(neighbor, names, adj, colors, path) {
433                            return Some(cycle);
434                        }
435                    }
436                    Color::Black => {
437                        // Already processed, no cycle through this path
438                    }
439                }
440            }
441
442            path.pop();
443            colors[node] = Color::Black;
444            None
445        }
446
447        let mut colors = vec![Color::White; names.len()];
448        let mut path = Vec::new();
449
450        for i in 0..names.len() {
451            if colors[i] == Color::White
452                && let Some(cycle) = dfs(i, names, adj, &mut colors, &mut path)
453            {
454                return Some(cycle);
455            }
456        }
457
458        None
459    }
460
461    /// Validate that all capabilities reference known core modules.
462    fn validate_capabilities(&self) -> Result<(), RegistryError> {
463        // Check rest_host early
464        if let Some((host_name, _)) = &self.rest_host
465            && !self.core.contains_key(host_name)
466        {
467            return Err(RegistryError::UnknownModule((*host_name).to_owned()));
468        }
469
470        // Check for configuration errors
471        if !self.errors.is_empty() {
472            return Err(RegistryError::InvalidRegistryConfiguration {
473                errors: self.errors.clone(),
474            });
475        }
476
477        // Validate all capability module names reference known core modules
478        for name in self.capabilities.keys() {
479            if !self.core.contains_key(name) {
480                return Err(RegistryError::UnknownModule((*name).to_owned()));
481            }
482        }
483
484        // Validate grpc_hub
485        if let Some((name, _)) = &self.grpc_hub
486            && !self.core.contains_key(name)
487        {
488            return Err(RegistryError::UnknownModule((*name).to_owned()));
489        }
490
491        Ok(())
492    }
493
494    /// Build dependency graph and return module names, adjacency list, and index mapping.
495    fn build_dependency_graph(&self) -> Result<DependencyGraph, RegistryError> {
496        let names: Vec<&'static str> = self.core.keys().copied().collect();
497        let mut idx: HashMap<&'static str, usize> = HashMap::new();
498        for (i, &n) in names.iter().enumerate() {
499            idx.insert(n, i);
500        }
501
502        let mut adj = vec![Vec::<usize>::new(); names.len()];
503
504        for (&n, &deps) in &self.deps {
505            let u = *idx
506                .get(n)
507                .ok_or_else(|| RegistryError::UnknownModule(n.to_owned()))?;
508            for &d in deps {
509                let v = *idx.get(d).ok_or_else(|| RegistryError::UnknownDependency {
510                    module: n.to_owned(),
511                    depends_on: d.to_owned(),
512                })?;
513                // edge d -> n (dep before module)
514                adj[v].push(u);
515            }
516        }
517
518        Ok((names, adj, idx))
519    }
520
521    /// Assemble final module entries in topological order.
522    fn assemble_entries(
523        &self,
524        order: &[usize],
525        names: &[&'static str],
526    ) -> Result<Vec<ModuleEntry>, RegistryError> {
527        let mut entries = Vec::with_capacity(order.len());
528        for &i in order {
529            let name = names[i];
530            let deps = *self
531                .deps
532                .get(name)
533                .ok_or_else(|| RegistryError::MissingDeps(name.to_owned()))?;
534
535            let core = self
536                .core
537                .get(name)
538                .cloned()
539                .ok_or_else(|| RegistryError::CoreNotFound(name.to_owned()))?;
540
541            // Build the capability set for this module
542            let mut caps = CapabilitySet::new();
543
544            // Add capabilities from the main capabilities map
545            if let Some(module_caps) = self.capabilities.get(name) {
546                for cap in module_caps {
547                    caps.push(cap.clone());
548                }
549            }
550
551            // Add rest_host if this module is the host
552            if let Some((host_name, module)) = &self.rest_host
553                && *host_name == name
554            {
555                caps.push(Capability::ApiGateway(module.clone()));
556            }
557
558            // Add grpc_hub if this module is the hub
559            if let Some((hub_name, module)) = &self.grpc_hub
560                && *hub_name == name
561            {
562                caps.push(Capability::GrpcHub(module.clone()));
563            }
564
565            let entry = ModuleEntry {
566                name,
567                deps,
568                core,
569                caps,
570            };
571            entries.push(entry);
572        }
573        Ok(entries)
574    }
575
576    /// Finalize & topo-sort; verify deps & capability binding to known cores.
577    ///
578    /// # Errors
579    /// Returns `RegistryError` if validation fails or a dependency cycle is detected.
580    pub fn build_topo_sorted(self) -> Result<ModuleRegistry, RegistryError> {
581        // 1) Validate all capabilities
582        self.validate_capabilities()?;
583
584        // 2) Build dependency graph
585        let (names, adj, _idx) = self.build_dependency_graph()?;
586
587        // 3) Cycle detection using DFS with path tracking
588        if let Some(cycle_path) = Self::detect_cycle_with_path(&names, &adj) {
589            return Err(RegistryError::CycleDetected { path: cycle_path });
590        }
591
592        // 4) Kahn's algorithm for topological sorting
593        let mut indeg = vec![0usize; names.len()];
594        for adj_list in &adj {
595            for &target in adj_list {
596                indeg[target] += 1;
597            }
598        }
599
600        let mut q = VecDeque::new();
601        for (i, &degree) in indeg.iter().enumerate() {
602            if degree == 0 {
603                q.push_back(i);
604            }
605        }
606
607        let mut order = Vec::with_capacity(names.len());
608        while let Some(u) = q.pop_front() {
609            order.push(u);
610            for &w in &adj[u] {
611                indeg[w] -= 1;
612                if indeg[w] == 0 {
613                    q.push_back(w);
614                }
615            }
616        }
617
618        // 5) Assemble final entries
619        let entries = self.assemble_entries(&order, &names)?;
620
621        // Collect grpc_hub and grpc_services for the final registry
622        let grpc_hub = self.grpc_hub.as_ref().map(|(name, _)| (*name).to_owned());
623
624        // Collect grpc_services from capabilities
625        let mut grpc_services: Vec<(String, Arc<dyn contracts::GrpcServiceCapability>)> =
626            Vec::new();
627        for (name, caps) in &self.capabilities {
628            for cap in caps {
629                if let Capability::GrpcService(service) = cap {
630                    grpc_services.push(((*name).to_owned(), service.clone()));
631                }
632            }
633        }
634
635        tracing::info!(
636            modules = ?entries.iter().map(|e| e.name).collect::<Vec<_>>(),
637            "Module dependency order resolved (topo)"
638        );
639
640        Ok(ModuleRegistry {
641            modules: entries,
642            grpc_hub,
643            grpc_services,
644        })
645    }
646}
647
648/// Structured errors for the module registry.
649#[derive(Debug, Error)]
650pub enum RegistryError {
651    // Phase errors with module context
652    #[error("pre-init failed for module '{module}'")]
653    PreInit {
654        module: &'static str,
655        #[source]
656        source: anyhow::Error,
657    },
658    #[error("initialization failed for module '{module}'")]
659    Init {
660        module: &'static str,
661        #[source]
662        source: anyhow::Error,
663    },
664    #[error("post-init failed for module '{module}'")]
665    PostInit {
666        module: &'static str,
667        #[source]
668        source: anyhow::Error,
669    },
670    #[error("start failed for '{module}'")]
671    Start {
672        module: &'static str,
673        #[source]
674        source: anyhow::Error,
675    },
676
677    #[error("DB migration failed for module '{module}'")]
678    DbMigrate {
679        module: &'static str,
680        #[source]
681        source: anyhow::Error,
682    },
683    #[error("REST prepare failed for host module '{module}'")]
684    RestPrepare {
685        module: &'static str,
686        #[source]
687        source: anyhow::Error,
688    },
689    #[error("REST registration failed for module '{module}'")]
690    RestRegister {
691        module: &'static str,
692        #[source]
693        source: anyhow::Error,
694    },
695    #[error("REST finalize failed for host module '{module}'")]
696    RestFinalize {
697        module: &'static str,
698        #[source]
699        source: anyhow::Error,
700    },
701    #[error(
702        "REST phase requires an gateway host: modules with capability 'rest' found, but no module with capability 'rest_host'"
703    )]
704    RestRequiresHost,
705    #[error("multiple 'rest_host' modules detected; exactly one is allowed")]
706    MultipleRestHosts,
707    #[error("REST host module not found after validation")]
708    RestHostNotFoundAfterValidation,
709    #[error("REST host missing from entry")]
710    RestHostMissingFromEntry,
711
712    // gRPC-related errors
713    #[error("gRPC registration failed for module '{module}'")]
714    GrpcRegister {
715        module: String,
716        #[source]
717        source: anyhow::Error,
718    },
719    #[error(
720        "gRPC phase requires a hub: modules with capability 'grpc' found, but no module with capability 'grpc_hub'"
721    )]
722    GrpcRequiresHub,
723    #[error("multiple 'grpc_hub' modules detected; exactly one is allowed")]
724    MultipleGrpcHubs,
725
726    // OoP spawn errors
727    #[error("OoP spawn failed for module '{module}'")]
728    OopSpawn {
729        module: String,
730        #[source]
731        source: anyhow::Error,
732    },
733
734    // Build/topo-sort errors
735    #[error("unknown module '{0}'")]
736    UnknownModule(String),
737    #[error("module '{module}' depends on unknown '{depends_on}'")]
738    UnknownDependency { module: String, depends_on: String },
739    #[error("cyclic dependency detected: {}", path.join(" -> "))]
740    CycleDetected { path: Vec<&'static str> },
741    #[error("missing deps for '{0}'")]
742    MissingDeps(String),
743    #[error("core not found for '{0}'")]
744    CoreNotFound(String),
745    #[error("invalid registry configuration:\n{errors:#?}")]
746    InvalidRegistryConfiguration { errors: Vec<String> },
747}
748
749#[cfg(test)]
750#[cfg_attr(coverage_nightly, coverage(off))]
751mod tests {
752    use super::*;
753    use std::sync::Arc;
754
755    // Use the real contracts/context APIs from the crate to avoid type mismatches.
756    use crate::context::ModuleCtx;
757    use crate::contracts;
758
759    /* --------------------------- Test helpers ------------------------- */
760    #[derive(Default)]
761    struct DummyCore;
762    #[async_trait::async_trait]
763    impl contracts::Module for DummyCore {
764        async fn init(&self, _ctx: &ModuleCtx) -> anyhow::Result<()> {
765            Ok(())
766        }
767    }
768
769    /* ------------------------------- Tests ---------------------------- */
770
771    #[test]
772    fn topo_sort_happy_path() {
773        let mut b = RegistryBuilder::default();
774        // cores
775        b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
776        b.register_core_with_meta("core_b", &["core_a"], Arc::new(DummyCore));
777
778        let reg = b.build_topo_sorted().unwrap();
779        let order: Vec<_> = reg.modules().iter().map(|m| m.name).collect();
780        assert_eq!(order, vec!["core_a", "core_b"]);
781    }
782
783    #[test]
784    fn unknown_dependency_error() {
785        let mut b = RegistryBuilder::default();
786        b.register_core_with_meta("core_a", &["missing_dep"], Arc::new(DummyCore));
787
788        let err = b.build_topo_sorted().unwrap_err();
789        match err {
790            RegistryError::UnknownDependency { module, depends_on } => {
791                assert_eq!(module, "core_a");
792                assert_eq!(depends_on, "missing_dep");
793            }
794            other => panic!("unexpected error: {other:?}"),
795        }
796    }
797
798    #[test]
799    fn cyclic_dependency_detected() {
800        let mut b = RegistryBuilder::default();
801        b.register_core_with_meta("a", &["b"], Arc::new(DummyCore));
802        b.register_core_with_meta("b", &["a"], Arc::new(DummyCore));
803
804        let err = b.build_topo_sorted().unwrap_err();
805        match err {
806            RegistryError::CycleDetected { path } => {
807                // Should contain both modules in the cycle
808                assert!(path.contains(&"a"));
809                assert!(path.contains(&"b"));
810                assert!(path.len() >= 3); // At least a -> b -> a
811            }
812            other => panic!("expected CycleDetected, got: {other:?}"),
813        }
814    }
815
816    #[test]
817    fn complex_cycle_detection_with_path() {
818        let mut b = RegistryBuilder::default();
819        // Create a more complex cycle: a -> b -> c -> a
820        b.register_core_with_meta("a", &["b"], Arc::new(DummyCore));
821        b.register_core_with_meta("b", &["c"], Arc::new(DummyCore));
822        b.register_core_with_meta("c", &["a"], Arc::new(DummyCore));
823        // Add an unrelated module to ensure we only detect the actual cycle
824        b.register_core_with_meta("d", &[], Arc::new(DummyCore));
825
826        let err = b.build_topo_sorted().unwrap_err();
827        match err {
828            RegistryError::CycleDetected { path } => {
829                // Should contain all modules in the cycle
830                assert!(path.contains(&"a"));
831                assert!(path.contains(&"b"));
832                assert!(path.contains(&"c"));
833                assert!(!path.contains(&"d")); // Should not include unrelated module
834                assert!(path.len() >= 4); // At least a -> b -> c -> a
835
836                // Verify the error message is helpful
837                let error_msg = format!("{}", RegistryError::CycleDetected { path: path.clone() });
838                assert!(error_msg.contains("cyclic dependency detected"));
839                assert!(error_msg.contains("->"));
840            }
841            other => panic!("expected CycleDetected, got: {other:?}"),
842        }
843    }
844
845    #[test]
846    fn duplicate_core_reported_in_configuration_errors() {
847        let mut b = RegistryBuilder::default();
848        b.register_core_with_meta("a", &[], Arc::new(DummyCore));
849        // duplicate
850        b.register_core_with_meta("a", &[], Arc::new(DummyCore));
851
852        let err = b.build_topo_sorted().unwrap_err();
853        match err {
854            RegistryError::InvalidRegistryConfiguration { errors } => {
855                assert!(
856                    errors.iter().any(|e| e.contains("already registered")),
857                    "expected duplicate registration error, got {errors:?}"
858                );
859            }
860            other => panic!("unexpected error: {other:?}"),
861        }
862    }
863
864    #[test]
865    fn rest_capability_without_core_fails() {
866        let mut b = RegistryBuilder::default();
867        b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
868        // Register a rest capability for a module that doesn't exist
869        b.register_rest_with_meta("unknown_module", Arc::new(DummyRest));
870
871        let err = b.build_topo_sorted().unwrap_err();
872        match err {
873            RegistryError::UnknownModule(name) => {
874                assert_eq!(name, "unknown_module");
875            }
876            other => panic!("expected UnknownModule, got: {other:?}"),
877        }
878    }
879
880    #[test]
881    fn db_capability_without_core_fails() {
882        let mut b = RegistryBuilder::default();
883        b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
884        // Register a db capability for a module that doesn't exist
885        b.register_db_with_meta("unknown_module", Arc::new(DummyDb));
886
887        let err = b.build_topo_sorted().unwrap_err();
888        match err {
889            RegistryError::UnknownModule(name) => {
890                assert_eq!(name, "unknown_module");
891            }
892            other => panic!("expected UnknownModule, got: {other:?}"),
893        }
894    }
895
896    #[test]
897    fn stateful_capability_without_core_fails() {
898        let mut b = RegistryBuilder::default();
899        b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
900        // Register a stateful capability for a module that doesn't exist
901        b.register_stateful_with_meta("unknown_module", Arc::new(DummyStateful));
902
903        let err = b.build_topo_sorted().unwrap_err();
904        match err {
905            RegistryError::UnknownModule(name) => {
906                assert_eq!(name, "unknown_module");
907            }
908            other => panic!("expected UnknownModule, got: {other:?}"),
909        }
910    }
911
912    #[test]
913    fn capability_query_works() {
914        let mut b = RegistryBuilder::default();
915        let module = Arc::new(DummyCore);
916        b.register_core_with_meta("test", &[], module);
917        b.register_db_with_meta("test", Arc::new(DummyDb));
918        b.register_rest_with_meta("test", Arc::new(DummyRest));
919
920        let reg = b.build_topo_sorted().unwrap();
921        let entry = &reg.modules()[0];
922
923        assert!(entry.caps.has::<DatabaseCap>());
924        assert!(entry.caps.has::<RestApiCap>());
925        assert!(!entry.caps.has::<SystemCap>());
926
927        assert!(entry.caps.query::<DatabaseCap>().is_some());
928        assert!(entry.caps.query::<RestApiCap>().is_some());
929        assert!(entry.caps.query::<SystemCap>().is_none());
930    }
931
932    #[test]
933    fn rest_host_capability_without_core_fails() {
934        let mut b = RegistryBuilder::default();
935        b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
936        // Set rest_host to a module that doesn't exist
937        b.register_rest_host_with_meta("unknown_host", Arc::new(DummyRestHost));
938
939        let err = b.build_topo_sorted().unwrap_err();
940        match err {
941            RegistryError::UnknownModule(name) => {
942                assert_eq!(name, "unknown_host");
943            }
944            other => panic!("expected UnknownModule, got: {other:?}"),
945        }
946    }
947
948    #[test]
949    fn test_module_registry_builds() {
950        let registry = ModuleRegistry::discover_and_build();
951        assert!(registry.is_ok(), "Registry should build successfully");
952    }
953
954    /* Test helper implementations */
955    #[derive(Default, Clone)]
956    struct DummyRest;
957    impl contracts::RestApiCapability for DummyRest {
958        fn register_rest(
959            &self,
960            _ctx: &crate::context::ModuleCtx,
961            _router: axum::Router,
962            _openapi: &dyn crate::api::OpenApiRegistry,
963        ) -> anyhow::Result<axum::Router> {
964            Ok(axum::Router::new())
965        }
966    }
967
968    #[derive(Default)]
969    struct DummyDb;
970    #[async_trait::async_trait]
971    impl contracts::DatabaseCapability for DummyDb {
972        async fn migrate(&self, _db: &modkit_db::DbHandle) -> anyhow::Result<()> {
973            Ok(())
974        }
975    }
976
977    #[derive(Default)]
978    struct DummyStateful;
979    #[async_trait::async_trait]
980    impl contracts::RunnableCapability for DummyStateful {
981        async fn start(&self, _cancel: tokio_util::sync::CancellationToken) -> anyhow::Result<()> {
982            Ok(())
983        }
984        async fn stop(&self, _cancel: tokio_util::sync::CancellationToken) -> anyhow::Result<()> {
985            Ok(())
986        }
987    }
988
989    #[derive(Default)]
990    struct DummyRestHost;
991    impl contracts::ApiGatewayCapability for DummyRestHost {
992        fn rest_prepare(
993            &self,
994            _ctx: &crate::context::ModuleCtx,
995            router: axum::Router,
996        ) -> anyhow::Result<axum::Router> {
997            Ok(router)
998        }
999        fn rest_finalize(
1000            &self,
1001            _ctx: &crate::context::ModuleCtx,
1002            router: axum::Router,
1003        ) -> anyhow::Result<axum::Router> {
1004            Ok(router)
1005        }
1006        fn as_registry(&self) -> &dyn crate::contracts::OpenApiRegistry {
1007            panic!("DummyRestHost::as_registry should not be called in tests")
1008        }
1009    }
1010}