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    pub(crate) name: &'static str,
215    pub(crate) deps: &'static [&'static str],
216    pub(crate) core: Arc<dyn contracts::Gear>,
217    pub(crate) caps: CapabilitySet,
218}
219
220impl GearEntry {
221    /// Returns the gear name.
222    #[must_use]
223    pub fn name(&self) -> &'static str {
224        self.name
225    }
226
227    /// Returns the gear 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 GearEntry {
241    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242        f.debug_struct("GearEntry")
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 GearRegistry {
264    gears: Vec<GearEntry>, // 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 GearRegistry {
270    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271        let names: Vec<&'static str> = self.gears.iter().map(|m| m.name).collect();
272        f.debug_struct("GearRegistry")
273            .field("gears", &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 GearRegistry {
281    #[must_use]
282    pub fn gears(&self) -> &[GearEntry] {
283        &self.gears
284    }
285
286    /// Returns gears ordered by system priority.
287    /// System gears come first, followed by non-system gears.
288    /// Within each group, the original topological order is preserved.
289    #[must_use]
290    pub fn gears_by_system_priority(&self) -> Vec<&GearEntry> {
291        let mut system_mods = Vec::new();
292        let mut non_system_mods = Vec::new();
293
294        for entry in &self.gears {
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 gear 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_gear(&self, name: &str) -> Option<Arc<dyn contracts::Gear>> {
321        self.gears
322            .iter()
323            .find(|e| e.name == name)
324            .map(|e| e.core.clone())
325    }
326}
327
328/// Type alias for gRPC hub gear configuration.
329type GrpcHubEntry = (&'static str, Arc<dyn contracts::GrpcHubCapability>);
330
331/// Internal builder that macro registrators will feed.
332/// Keys are gear **names**; uniqueness enforced at build time.
333#[derive(Default)]
334pub struct RegistryBuilder {
335    core: HashMap<&'static str, Arc<dyn contracts::Gear>>,
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::Gear>,
356    ) {
357        if self.core.contains_key(name) {
358            self.errors
359                .push(format!("Gear '{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 gears 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 gears 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 gears.
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::UnknownGear((*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 gear names reference known core gears
534        for name in self.capabilities.keys() {
535            if !self.core.contains_key(name) {
536                return Err(RegistryError::UnknownGear((*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::UnknownGear((*name).to_owned()));
545        }
546
547        Ok(())
548    }
549
550    /// Build dependency graph and return gear 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::UnknownGear(n.to_owned()))?;
564            for &d in deps {
565                let v = *idx.get(d).ok_or_else(|| RegistryError::UnknownDependency {
566                    gear: n.to_owned(),
567                    depends_on: d.to_owned(),
568                })?;
569                // edge d -> n (dep before gear)
570                adj[v].push(u);
571            }
572        }
573
574        Ok((names, adj, idx))
575    }
576
577    /// Assemble final gear entries in topological order.
578    fn assemble_entries(
579        &self,
580        order: &[usize],
581        names: &[&'static str],
582    ) -> Result<Vec<GearEntry>, 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 gear
598            let mut caps = CapabilitySet::new();
599
600            // Add capabilities from the main capabilities map
601            if let Some(gear_caps) = self.capabilities.get(name) {
602                for cap in gear_caps {
603                    caps.push(cap.clone());
604                }
605            }
606
607            // Add rest_host if this gear is the host
608            if let Some((host_name, gear)) = &self.rest_host
609                && *host_name == name
610            {
611                caps.push(Capability::ApiGateway(gear.clone()));
612            }
613
614            // Add grpc_hub if this gear is the hub
615            if let Some((hub_name, gear)) = &self.grpc_hub
616                && *hub_name == name
617            {
618                caps.push(Capability::GrpcHub(gear.clone()));
619            }
620
621            let entry = GearEntry {
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<GearRegistry, 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            gears = ?entries.iter().map(|e| e.name).collect::<Vec<_>>(),
693            "Gear dependency order resolved (topo)"
694        );
695
696        Ok(GearRegistry {
697            gears: entries,
698            grpc_hub,
699            grpc_services,
700        })
701    }
702}
703
704/// Structured errors for the gear registry.
705#[derive(Debug, Error)]
706pub enum RegistryError {
707    // Phase errors with gear context
708    #[error("pre-init failed for gear '{gear}'")]
709    PreInit {
710        gear: &'static str,
711        #[source]
712        source: anyhow::Error,
713    },
714    #[error("initialization failed for gear '{gear}'")]
715    Init {
716        gear: &'static str,
717        #[source]
718        source: anyhow::Error,
719    },
720    #[error("post-init failed for gear '{gear}'")]
721    PostInit {
722        gear: &'static str,
723        #[source]
724        source: anyhow::Error,
725    },
726    #[error("start failed for '{gear}'")]
727    Start {
728        gear: &'static str,
729        #[source]
730        source: anyhow::Error,
731    },
732
733    #[error("DB migration failed for gear '{gear}'")]
734    DbMigrate {
735        gear: &'static str,
736        #[source]
737        source: anyhow::Error,
738    },
739    #[error("REST prepare failed for host gear '{gear}'")]
740    RestPrepare {
741        gear: &'static str,
742        #[source]
743        source: anyhow::Error,
744    },
745    #[error("REST registration failed for gear '{gear}'")]
746    RestRegister {
747        gear: &'static str,
748        #[source]
749        source: anyhow::Error,
750    },
751    #[error("REST finalize failed for host gear '{gear}'")]
752    RestFinalize {
753        gear: &'static str,
754        #[source]
755        source: anyhow::Error,
756    },
757    #[error("consumer proxy-wiring failed for gear '{gear}'")]
758    ProxyWiring {
759        gear: &'static str,
760        #[source]
761        source: anyhow::Error,
762    },
763    #[error(
764        "REST phase requires an gateway host: gears with capability 'rest' found, but no gear with capability 'rest_host'"
765    )]
766    RestRequiresHost,
767    #[error("multiple 'rest_host' gears detected; exactly one is allowed")]
768    MultipleRestHosts,
769    #[error("REST host gear not found after validation")]
770    RestHostNotFoundAfterValidation,
771    #[error("REST host missing from entry")]
772    RestHostMissingFromEntry,
773
774    // gRPC-related errors
775    #[error("gRPC registration failed for gear '{gear}'")]
776    GrpcRegister {
777        gear: String,
778        #[source]
779        source: anyhow::Error,
780    },
781    #[error(
782        "gRPC phase requires a hub: gears with capability 'grpc' found, but no gear with capability 'grpc_hub'"
783    )]
784    GrpcRequiresHub,
785    #[error("multiple 'grpc_hub' gears detected; exactly one is allowed")]
786    MultipleGrpcHubs,
787
788    // OoP spawn errors
789    #[error("OoP spawn failed for gear '{gear}'")]
790    OopSpawn {
791        gear: String,
792        #[source]
793        source: anyhow::Error,
794    },
795
796    // Cancellation error
797    #[error("operation cancelled by termination signal")]
798    Cancelled,
799
800    // Build/topo-sort errors
801    #[error("unknown gear '{0}'")]
802    UnknownGear(String),
803    #[error("gear '{gear}' depends on unknown '{depends_on}'")]
804    UnknownDependency { gear: String, depends_on: String },
805    #[error("cyclic dependency detected: {}", path.join(" -> "))]
806    CycleDetected { path: Vec<&'static str> },
807    #[error("missing deps for '{0}'")]
808    MissingDeps(String),
809    #[error("core not found for '{0}'")]
810    CoreNotFound(String),
811    #[error("invalid registry configuration:\n{errors:#?}")]
812    InvalidRegistryConfiguration { errors: Vec<String> },
813}
814
815#[cfg(test)]
816#[cfg_attr(coverage_nightly, coverage(off))]
817mod tests {
818    use super::*;
819    use std::sync::Arc;
820
821    // Use the real contracts/context APIs from the crate to avoid type mismatches.
822    use crate::context::GearCtx;
823    use crate::contracts;
824
825    /* --------------------------- Test helpers ------------------------- */
826    #[derive(Default)]
827    struct DummyCore;
828    #[async_trait::async_trait]
829    impl contracts::Gear for DummyCore {
830        async fn init(&self, _ctx: &GearCtx) -> anyhow::Result<()> {
831            Ok(())
832        }
833    }
834
835    /* ------------------------------- Tests ---------------------------- */
836
837    #[test]
838    fn topo_sort_happy_path() {
839        let mut b = RegistryBuilder::default();
840        // cores
841        b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
842        b.register_core_with_meta("core_b", &["core_a"], Arc::new(DummyCore));
843
844        let reg = b.build_topo_sorted().unwrap();
845        let order: Vec<_> = reg.gears().iter().map(|m| m.name).collect();
846        assert_eq!(order, vec!["core_a", "core_b"]);
847    }
848
849    #[test]
850    fn unknown_dependency_error() {
851        let mut b = RegistryBuilder::default();
852        b.register_core_with_meta("core_a", &["missing_dep"], Arc::new(DummyCore));
853
854        let err = b.build_topo_sorted().unwrap_err();
855        match err {
856            RegistryError::UnknownDependency { gear, depends_on } => {
857                assert_eq!(gear, "core_a");
858                assert_eq!(depends_on, "missing_dep");
859            }
860            other => panic!("unexpected error: {other:?}"),
861        }
862    }
863
864    #[test]
865    fn cyclic_dependency_detected() {
866        let mut b = RegistryBuilder::default();
867        b.register_core_with_meta("a", &["b"], Arc::new(DummyCore));
868        b.register_core_with_meta("b", &["a"], Arc::new(DummyCore));
869
870        let err = b.build_topo_sorted().unwrap_err();
871        match err {
872            RegistryError::CycleDetected { path } => {
873                // Should contain both gears in the cycle
874                assert!(path.contains(&"a"));
875                assert!(path.contains(&"b"));
876                assert!(path.len() >= 3); // At least a -> b -> a
877            }
878            other => panic!("expected CycleDetected, got: {other:?}"),
879        }
880    }
881
882    #[test]
883    fn complex_cycle_detection_with_path() {
884        let mut b = RegistryBuilder::default();
885        // Create a more complex cycle: a -> b -> c -> a
886        b.register_core_with_meta("a", &["b"], Arc::new(DummyCore));
887        b.register_core_with_meta("b", &["c"], Arc::new(DummyCore));
888        b.register_core_with_meta("c", &["a"], Arc::new(DummyCore));
889        // Add an unrelated gear to ensure we only detect the actual cycle
890        b.register_core_with_meta("d", &[], Arc::new(DummyCore));
891
892        let err = b.build_topo_sorted().unwrap_err();
893        match err {
894            RegistryError::CycleDetected { path } => {
895                // Should contain all gears in the cycle
896                assert!(path.contains(&"a"));
897                assert!(path.contains(&"b"));
898                assert!(path.contains(&"c"));
899                assert!(!path.contains(&"d")); // Should not include unrelated gear
900                assert!(path.len() >= 4); // At least a -> b -> c -> a
901
902                // Verify the error message is helpful
903                let error_msg = format!("{}", RegistryError::CycleDetected { path: path.clone() });
904                assert!(error_msg.contains("cyclic dependency detected"));
905                assert!(error_msg.contains("->"));
906            }
907            other => panic!("expected CycleDetected, got: {other:?}"),
908        }
909    }
910
911    #[test]
912    fn duplicate_core_reported_in_configuration_errors() {
913        let mut b = RegistryBuilder::default();
914        b.register_core_with_meta("a", &[], Arc::new(DummyCore));
915        // duplicate
916        b.register_core_with_meta("a", &[], Arc::new(DummyCore));
917
918        let err = b.build_topo_sorted().unwrap_err();
919        match err {
920            RegistryError::InvalidRegistryConfiguration { errors } => {
921                assert!(
922                    errors.iter().any(|e| e.contains("already registered")),
923                    "expected duplicate registration error, got {errors:?}"
924                );
925            }
926            other => panic!("unexpected error: {other:?}"),
927        }
928    }
929
930    #[test]
931    fn rest_capability_without_core_fails() {
932        let mut b = RegistryBuilder::default();
933        b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
934        // Register a rest capability for a gear that doesn't exist
935        b.register_rest_with_meta("unknown_gear", Arc::new(DummyRest));
936
937        let err = b.build_topo_sorted().unwrap_err();
938        match err {
939            RegistryError::UnknownGear(name) => {
940                assert_eq!(name, "unknown_gear");
941            }
942            other => panic!("expected UnknownGear, got: {other:?}"),
943        }
944    }
945
946    #[test]
947    #[cfg(feature = "db")]
948    fn db_capability_without_core_fails() {
949        let mut b = RegistryBuilder::default();
950        b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
951        // Register a db capability for a gear that doesn't exist
952        b.register_db_with_meta("unknown_gear", Arc::new(DummyDb));
953
954        let err = b.build_topo_sorted().unwrap_err();
955        match err {
956            RegistryError::UnknownGear(name) => {
957                assert_eq!(name, "unknown_gear");
958            }
959            other => panic!("expected UnknownGear, got: {other:?}"),
960        }
961    }
962
963    #[test]
964    fn stateful_capability_without_core_fails() {
965        let mut b = RegistryBuilder::default();
966        b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
967        // Register a stateful capability for a gear that doesn't exist
968        b.register_stateful_with_meta("unknown_gear", Arc::new(DummyStateful));
969
970        let err = b.build_topo_sorted().unwrap_err();
971        match err {
972            RegistryError::UnknownGear(name) => {
973                assert_eq!(name, "unknown_gear");
974            }
975            other => panic!("expected UnknownGear, got: {other:?}"),
976        }
977    }
978
979    #[test]
980    #[cfg(feature = "db")]
981    fn capability_query_works() {
982        let mut b = RegistryBuilder::default();
983        let gear = Arc::new(DummyCore);
984        b.register_core_with_meta("test", &[], gear);
985        b.register_db_with_meta("test", Arc::new(DummyDb));
986        b.register_rest_with_meta("test", Arc::new(DummyRest));
987
988        let reg = b.build_topo_sorted().unwrap();
989        let entry = &reg.gears()[0];
990
991        assert!(entry.caps.has::<DatabaseCap>());
992        assert!(entry.caps.has::<RestApiCap>());
993        assert!(!entry.caps.has::<SystemCap>());
994
995        assert!(entry.caps.query::<DatabaseCap>().is_some());
996        assert!(entry.caps.query::<RestApiCap>().is_some());
997        assert!(entry.caps.query::<SystemCap>().is_none());
998    }
999
1000    #[test]
1001    fn rest_host_capability_without_core_fails() {
1002        let mut b = RegistryBuilder::default();
1003        b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
1004        // Set rest_host to a gear that doesn't exist
1005        b.register_rest_host_with_meta("unknown_host", Arc::new(DummyRestHost));
1006
1007        let err = b.build_topo_sorted().unwrap_err();
1008        match err {
1009            RegistryError::UnknownGear(name) => {
1010                assert_eq!(name, "unknown_host");
1011            }
1012            other => panic!("expected UnknownGear, got: {other:?}"),
1013        }
1014    }
1015
1016    #[test]
1017    fn gear_entry_getters_work() {
1018        let mut b = RegistryBuilder::default();
1019        b.register_core_with_meta("alpha", &[], Arc::new(DummyCore));
1020        b.register_core_with_meta("beta", &["alpha"], Arc::new(DummyCore));
1021        b.register_rest_with_meta("beta", Arc::new(DummyRest));
1022
1023        let reg = b.build_topo_sorted().unwrap();
1024        let beta = reg.gears().iter().find(|e| e.name() == "beta").unwrap();
1025
1026        assert_eq!(beta.name(), "beta");
1027        assert_eq!(beta.deps(), &["alpha"]);
1028        assert!(beta.caps().has::<RestApiCap>());
1029    }
1030
1031    #[test]
1032    fn test_gear_registry_builds() {
1033        let registry = GearRegistry::discover_and_build();
1034        assert!(registry.is_ok(), "Registry should build successfully");
1035    }
1036
1037    /* Test helper implementations */
1038    #[derive(Default, Clone)]
1039    struct DummyRest;
1040    impl contracts::RestApiCapability for DummyRest {
1041        fn register_rest(
1042            &self,
1043            _ctx: &crate::context::GearCtx,
1044            _router: axum::Router,
1045            _openapi: &dyn crate::api::OpenApiRegistry,
1046        ) -> anyhow::Result<axum::Router> {
1047            Ok(axum::Router::new())
1048        }
1049    }
1050
1051    #[cfg(feature = "db")]
1052    #[derive(Default)]
1053    struct DummyDb;
1054    #[cfg(feature = "db")]
1055    impl contracts::DatabaseCapability for DummyDb {
1056        fn migrations(&self) -> Vec<Box<dyn sea_orm_migration::MigrationTrait>> {
1057            vec![]
1058        }
1059    }
1060
1061    #[derive(Default)]
1062    struct DummyStateful;
1063    #[async_trait::async_trait]
1064    impl contracts::RunnableCapability for DummyStateful {
1065        async fn start(&self, _cancel: tokio_util::sync::CancellationToken) -> anyhow::Result<()> {
1066            Ok(())
1067        }
1068        async fn stop(&self, _cancel: tokio_util::sync::CancellationToken) -> anyhow::Result<()> {
1069            Ok(())
1070        }
1071    }
1072
1073    #[derive(Default)]
1074    struct DummyRestHost;
1075    impl contracts::ApiGatewayCapability for DummyRestHost {
1076        fn rest_prepare(
1077            &self,
1078            _ctx: &crate::context::GearCtx,
1079            router: axum::Router,
1080            _hc_registry: std::sync::Arc<crate::healthcheck::RestHealthcheckRegistry>,
1081        ) -> anyhow::Result<axum::Router> {
1082            Ok(router)
1083        }
1084        fn rest_finalize(
1085            &self,
1086            _ctx: &crate::context::GearCtx,
1087            router: axum::Router,
1088            _hc_registry: std::sync::Arc<crate::healthcheck::RestHealthcheckRegistry>,
1089        ) -> anyhow::Result<axum::Router> {
1090            Ok(router)
1091        }
1092        fn as_registry(&self) -> &dyn crate::contracts::OpenApiRegistry {
1093            panic!("DummyRestHost::as_registry should not be called in tests")
1094        }
1095    }
1096}