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