Skip to main content

toolkit/
registry.rs

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