1use std::collections::{HashMap, VecDeque};
3use std::sync::Arc;
4
5use thiserror::Error;
6
7use crate::contracts;
9
10type RestHostEntry = (&'static str, Arc<dyn contracts::ApiGatewayCapability>);
12
13#[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
45pub trait CapTag {
47 type Out: ?Sized + 'static;
48 fn try_get(cap: &Capability) -> Option<&Arc<Self::Out>>;
49}
50
51#[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
65pub 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
77pub 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
89pub 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
101pub 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
113pub 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
125pub 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#[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 #[must_use]
154 pub fn new() -> Self {
155 Self { caps: Vec::new() }
156 }
157
158 pub fn push(&mut self, cap: Capability) {
160 self.caps.push(cap);
161 }
162
163 #[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 #[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 #[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 #[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,
216 pub(crate) deps: &'static [&'static str],
218 pub(crate) core: Arc<dyn contracts::Gear>,
220 pub(crate) caps: CapabilitySet,
222}
223
224impl GearEntry {
225 #[must_use]
227 pub fn name(&self) -> &'static str {
228 self.name
229 }
230
231 #[must_use]
233 pub fn deps(&self) -> &'static [&'static str] {
234 self.deps
235 }
236
237 #[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
260pub struct Registrator(pub fn(&mut RegistryBuilder));
263
264inventory::collect!(Registrator);
265
266pub struct GearRegistry {
268 gears: Vec<GearEntry>, 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 #[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 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 #[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
332type GrpcHubEntry = (&'static str, Arc<dyn contracts::GrpcHubCapability>);
334
335#[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
347type 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 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, Gray, Black, }
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 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 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 }
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 fn validate_capabilities(&self) -> Result<(), RegistryError> {
523 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 if !self.errors.is_empty() {
532 return Err(RegistryError::InvalidRegistryConfiguration {
533 errors: self.errors.clone(),
534 });
535 }
536
537 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 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 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 adj[v].push(u);
575 }
576 }
577
578 Ok((names, adj, idx))
579 }
580
581 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 let mut caps = CapabilitySet::new();
603
604 if let Some(gear_caps) = self.capabilities.get(name) {
606 for cap in gear_caps {
607 caps.push(cap.clone());
608 }
609 }
610
611 if let Some((host_name, gear)) = &self.rest_host
613 && *host_name == name
614 {
615 caps.push(Capability::ApiGateway(gear.clone()));
616 }
617
618 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 pub fn build_topo_sorted(self) -> Result<GearRegistry, RegistryError> {
641 self.validate_capabilities()?;
643
644 let (names, adj, _idx) = self.build_dependency_graph()?;
646
647 if let Some(cycle_path) = Self::detect_cycle_with_path(&names, &adj) {
649 return Err(RegistryError::CycleDetected { path: cycle_path });
650 }
651
652 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, °ree) 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 let entries = self.assemble_entries(&order, &names)?;
680
681 let grpc_hub = self.grpc_hub.as_ref().map(|(name, _)| (*name).to_owned());
683
684 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#[derive(Debug, Error)]
710pub enum RegistryError {
711 #[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 #[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 #[error("OoP spawn failed for gear '{gear}'")]
794 OopSpawn {
795 gear: String,
796 #[source]
797 source: anyhow::Error,
798 },
799
800 #[error("operation cancelled by termination signal")]
802 Cancelled,
803
804 #[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 crate::context::GearCtx;
827 use crate::contracts;
828
829 #[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 #[test]
842 fn topo_sort_happy_path() {
843 let mut b = RegistryBuilder::default();
844 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 assert!(path.contains(&"a"));
879 assert!(path.contains(&"b"));
880 assert!(path.len() >= 3); }
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 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 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 assert!(path.contains(&"a"));
901 assert!(path.contains(&"b"));
902 assert!(path.contains(&"c"));
903 assert!(!path.contains(&"d")); assert!(path.len() >= 4); 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 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 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 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 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 = ®.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 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 #[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}