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