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 ModuleEntry {
214 pub(crate) name: &'static str,
215 pub(crate) deps: &'static [&'static str],
216 pub(crate) core: Arc<dyn contracts::Module>,
217 pub(crate) caps: CapabilitySet,
218}
219
220impl ModuleEntry {
221 #[must_use]
223 pub fn name(&self) -> &'static str {
224 self.name
225 }
226
227 #[must_use]
229 pub fn deps(&self) -> &'static [&'static str] {
230 self.deps
231 }
232
233 #[must_use]
235 pub fn caps(&self) -> &CapabilitySet {
236 &self.caps
237 }
238}
239
240impl std::fmt::Debug for ModuleEntry {
241 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242 f.debug_struct("ModuleEntry")
243 .field("name", &self.name)
244 .field("deps", &self.deps)
245 .field("has_rest", &self.caps.has::<RestApiCap>())
246 .field("is_rest_host", &self.caps.has::<ApiGatewayCap>())
247 .field("has_db", &self.caps.has_db())
248 .field("has_stateful", &self.caps.has::<RunnableCap>())
249 .field("is_system", &self.caps.has::<SystemCap>())
250 .field("is_grpc_hub", &self.caps.has::<GrpcHubCap>())
251 .field("has_grpc_service", &self.caps.has::<GrpcServiceCap>())
252 .finish_non_exhaustive()
253 }
254}
255
256pub struct Registrator(pub fn(&mut RegistryBuilder));
259
260inventory::collect!(Registrator);
261
262pub struct ModuleRegistry {
264 modules: Vec<ModuleEntry>, pub grpc_hub: Option<String>,
266 pub grpc_services: Vec<(String, Arc<dyn contracts::GrpcServiceCapability>)>,
267}
268
269impl std::fmt::Debug for ModuleRegistry {
270 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271 let names: Vec<&'static str> = self.modules.iter().map(|m| m.name).collect();
272 f.debug_struct("ModuleRegistry")
273 .field("modules", &names)
274 .field("has_grpc_hub", &self.grpc_hub.is_some())
275 .field("grpc_services_count", &self.grpc_services.len())
276 .finish()
277 }
278}
279
280impl ModuleRegistry {
281 #[must_use]
282 pub fn modules(&self) -> &[ModuleEntry] {
283 &self.modules
284 }
285
286 #[must_use]
290 pub fn modules_by_system_priority(&self) -> Vec<&ModuleEntry> {
291 let mut system_mods = Vec::new();
292 let mut non_system_mods = Vec::new();
293
294 for entry in &self.modules {
295 if entry.caps.has::<SystemCap>() {
296 system_mods.push(entry);
297 } else {
298 non_system_mods.push(entry);
299 }
300 }
301
302 system_mods.extend(non_system_mods);
303 system_mods
304 }
305
306 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 #[must_use]
320 pub fn get_module(&self, name: &str) -> Option<Arc<dyn contracts::Module>> {
321 self.modules
322 .iter()
323 .find(|e| e.name == name)
324 .map(|e| e.core.clone())
325 }
326}
327
328type GrpcHubEntry = (&'static str, Arc<dyn contracts::GrpcHubCapability>);
330
331#[derive(Default)]
334pub struct RegistryBuilder {
335 core: HashMap<&'static str, Arc<dyn contracts::Module>>,
336 deps: HashMap<&'static str, &'static [&'static str]>,
337 capabilities: HashMap<&'static str, Vec<Capability>>,
338 rest_host: Option<RestHostEntry>,
339 grpc_hub: Option<GrpcHubEntry>,
340 errors: Vec<String>,
341}
342
343type DependencyGraph = (
345 Vec<&'static str>,
346 Vec<Vec<usize>>,
347 HashMap<&'static str, usize>,
348);
349
350impl RegistryBuilder {
351 pub fn register_core_with_meta(
352 &mut self,
353 name: &'static str,
354 deps: &'static [&'static str],
355 m: Arc<dyn contracts::Module>,
356 ) {
357 if self.core.contains_key(name) {
358 self.errors
359 .push(format!("Module '{name}' is already registered"));
360 return;
361 }
362 self.core.insert(name, m);
363 self.deps.insert(name, deps);
364 }
365
366 pub fn register_rest_with_meta(
367 &mut self,
368 name: &'static str,
369 m: Arc<dyn contracts::RestApiCapability>,
370 ) {
371 self.capabilities
372 .entry(name)
373 .or_default()
374 .push(Capability::RestApi(m));
375 }
376
377 pub fn register_rest_host_with_meta(
378 &mut self,
379 name: &'static str,
380 m: Arc<dyn contracts::ApiGatewayCapability>,
381 ) {
382 if let Some((existing, _)) = &self.rest_host {
383 self.errors.push(format!(
384 "Multiple REST host modules detected: '{existing}' and '{name}'. Only one REST host is allowed."
385 ));
386 return;
387 }
388 self.rest_host = Some((name, m));
389 }
390
391 #[cfg(feature = "db")]
392 pub fn register_db_with_meta(
393 &mut self,
394 name: &'static str,
395 m: Arc<dyn contracts::DatabaseCapability>,
396 ) {
397 self.capabilities
398 .entry(name)
399 .or_default()
400 .push(Capability::Database(m));
401 }
402
403 pub fn register_stateful_with_meta(
404 &mut self,
405 name: &'static str,
406 m: Arc<dyn contracts::RunnableCapability>,
407 ) {
408 self.capabilities
409 .entry(name)
410 .or_default()
411 .push(Capability::Runnable(m));
412 }
413
414 pub fn register_system_with_meta(
415 &mut self,
416 name: &'static str,
417 m: Arc<dyn contracts::SystemCapability>,
418 ) {
419 self.capabilities
420 .entry(name)
421 .or_default()
422 .push(Capability::System(m));
423 }
424
425 pub fn register_grpc_hub_with_meta(
426 &mut self,
427 name: &'static str,
428 m: Arc<dyn contracts::GrpcHubCapability>,
429 ) {
430 if let Some((existing, _)) = &self.grpc_hub {
431 self.errors.push(format!(
432 "Multiple gRPC hub modules detected: '{existing}' and '{name}'. Only one gRPC hub is allowed."
433 ));
434 return;
435 }
436 self.grpc_hub = Some((name, m));
437 }
438
439 pub fn register_grpc_service_with_meta(
440 &mut self,
441 name: &'static str,
442 m: Arc<dyn contracts::GrpcServiceCapability>,
443 ) {
444 self.capabilities
445 .entry(name)
446 .or_default()
447 .push(Capability::GrpcService(m));
448 }
449
450 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, Gray, Black, }
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 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 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 }
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 fn validate_capabilities(&self) -> Result<(), RegistryError> {
519 if let Some((host_name, _)) = &self.rest_host
521 && !self.core.contains_key(host_name)
522 {
523 return Err(RegistryError::UnknownModule((*host_name).to_owned()));
524 }
525
526 if !self.errors.is_empty() {
528 return Err(RegistryError::InvalidRegistryConfiguration {
529 errors: self.errors.clone(),
530 });
531 }
532
533 for name in self.capabilities.keys() {
535 if !self.core.contains_key(name) {
536 return Err(RegistryError::UnknownModule((*name).to_owned()));
537 }
538 }
539
540 if let Some((name, _)) = &self.grpc_hub
542 && !self.core.contains_key(name)
543 {
544 return Err(RegistryError::UnknownModule((*name).to_owned()));
545 }
546
547 Ok(())
548 }
549
550 fn build_dependency_graph(&self) -> Result<DependencyGraph, RegistryError> {
552 let names: Vec<&'static str> = self.core.keys().copied().collect();
553 let mut idx: HashMap<&'static str, usize> = HashMap::new();
554 for (i, &n) in names.iter().enumerate() {
555 idx.insert(n, i);
556 }
557
558 let mut adj = vec![Vec::<usize>::new(); names.len()];
559
560 for (&n, &deps) in &self.deps {
561 let u = *idx
562 .get(n)
563 .ok_or_else(|| RegistryError::UnknownModule(n.to_owned()))?;
564 for &d in deps {
565 let v = *idx.get(d).ok_or_else(|| RegistryError::UnknownDependency {
566 module: n.to_owned(),
567 depends_on: d.to_owned(),
568 })?;
569 adj[v].push(u);
571 }
572 }
573
574 Ok((names, adj, idx))
575 }
576
577 fn assemble_entries(
579 &self,
580 order: &[usize],
581 names: &[&'static str],
582 ) -> Result<Vec<ModuleEntry>, RegistryError> {
583 let mut entries = Vec::with_capacity(order.len());
584 for &i in order {
585 let name = names[i];
586 let deps = *self
587 .deps
588 .get(name)
589 .ok_or_else(|| RegistryError::MissingDeps(name.to_owned()))?;
590
591 let core = self
592 .core
593 .get(name)
594 .cloned()
595 .ok_or_else(|| RegistryError::CoreNotFound(name.to_owned()))?;
596
597 let mut caps = CapabilitySet::new();
599
600 if let Some(module_caps) = self.capabilities.get(name) {
602 for cap in module_caps {
603 caps.push(cap.clone());
604 }
605 }
606
607 if let Some((host_name, module)) = &self.rest_host
609 && *host_name == name
610 {
611 caps.push(Capability::ApiGateway(module.clone()));
612 }
613
614 if let Some((hub_name, module)) = &self.grpc_hub
616 && *hub_name == name
617 {
618 caps.push(Capability::GrpcHub(module.clone()));
619 }
620
621 let entry = ModuleEntry {
622 name,
623 deps,
624 core,
625 caps,
626 };
627 entries.push(entry);
628 }
629 Ok(entries)
630 }
631
632 pub fn build_topo_sorted(self) -> Result<ModuleRegistry, RegistryError> {
637 self.validate_capabilities()?;
639
640 let (names, adj, _idx) = self.build_dependency_graph()?;
642
643 if let Some(cycle_path) = Self::detect_cycle_with_path(&names, &adj) {
645 return Err(RegistryError::CycleDetected { path: cycle_path });
646 }
647
648 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, °ree) 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 let entries = self.assemble_entries(&order, &names)?;
676
677 let grpc_hub = self.grpc_hub.as_ref().map(|(name, _)| (*name).to_owned());
679
680 let mut grpc_services: Vec<(String, Arc<dyn contracts::GrpcServiceCapability>)> =
682 Vec::new();
683 for (name, caps) in &self.capabilities {
684 for cap in caps {
685 if let Capability::GrpcService(service) = cap {
686 grpc_services.push(((*name).to_owned(), service.clone()));
687 }
688 }
689 }
690
691 tracing::info!(
692 modules = ?entries.iter().map(|e| e.name).collect::<Vec<_>>(),
693 "Module dependency order resolved (topo)"
694 );
695
696 Ok(ModuleRegistry {
697 modules: entries,
698 grpc_hub,
699 grpc_services,
700 })
701 }
702}
703
704#[derive(Debug, Error)]
706pub enum RegistryError {
707 #[error("pre-init failed for module '{module}'")]
709 PreInit {
710 module: &'static str,
711 #[source]
712 source: anyhow::Error,
713 },
714 #[error("initialization failed for module '{module}'")]
715 Init {
716 module: &'static str,
717 #[source]
718 source: anyhow::Error,
719 },
720 #[error("post-init failed for module '{module}'")]
721 PostInit {
722 module: &'static str,
723 #[source]
724 source: anyhow::Error,
725 },
726 #[error("start failed for '{module}'")]
727 Start {
728 module: &'static str,
729 #[source]
730 source: anyhow::Error,
731 },
732
733 #[error("DB migration failed for module '{module}'")]
734 DbMigrate {
735 module: &'static str,
736 #[source]
737 source: anyhow::Error,
738 },
739 #[error("REST prepare failed for host module '{module}'")]
740 RestPrepare {
741 module: &'static str,
742 #[source]
743 source: anyhow::Error,
744 },
745 #[error("REST registration failed for module '{module}'")]
746 RestRegister {
747 module: &'static str,
748 #[source]
749 source: anyhow::Error,
750 },
751 #[error("REST finalize failed for host module '{module}'")]
752 RestFinalize {
753 module: &'static str,
754 #[source]
755 source: anyhow::Error,
756 },
757 #[error(
758 "REST phase requires an gateway host: modules with capability 'rest' found, but no module with capability 'rest_host'"
759 )]
760 RestRequiresHost,
761 #[error("multiple 'rest_host' modules detected; exactly one is allowed")]
762 MultipleRestHosts,
763 #[error("REST host module not found after validation")]
764 RestHostNotFoundAfterValidation,
765 #[error("REST host missing from entry")]
766 RestHostMissingFromEntry,
767
768 #[error("gRPC registration failed for module '{module}'")]
770 GrpcRegister {
771 module: String,
772 #[source]
773 source: anyhow::Error,
774 },
775 #[error(
776 "gRPC phase requires a hub: modules with capability 'grpc' found, but no module with capability 'grpc_hub'"
777 )]
778 GrpcRequiresHub,
779 #[error("multiple 'grpc_hub' modules detected; exactly one is allowed")]
780 MultipleGrpcHubs,
781
782 #[error("OoP spawn failed for module '{module}'")]
784 OopSpawn {
785 module: String,
786 #[source]
787 source: anyhow::Error,
788 },
789
790 #[error("operation cancelled by termination signal")]
792 Cancelled,
793
794 #[error("unknown module '{0}'")]
796 UnknownModule(String),
797 #[error("module '{module}' depends on unknown '{depends_on}'")]
798 UnknownDependency { module: String, depends_on: String },
799 #[error("cyclic dependency detected: {}", path.join(" -> "))]
800 CycleDetected { path: Vec<&'static str> },
801 #[error("missing deps for '{0}'")]
802 MissingDeps(String),
803 #[error("core not found for '{0}'")]
804 CoreNotFound(String),
805 #[error("invalid registry configuration:\n{errors:#?}")]
806 InvalidRegistryConfiguration { errors: Vec<String> },
807}
808
809#[cfg(test)]
810#[cfg_attr(coverage_nightly, coverage(off))]
811mod tests {
812 use super::*;
813 use std::sync::Arc;
814
815 use crate::context::ModuleCtx;
817 use crate::contracts;
818
819 #[derive(Default)]
821 struct DummyCore;
822 #[async_trait::async_trait]
823 impl contracts::Module for DummyCore {
824 async fn init(&self, _ctx: &ModuleCtx) -> anyhow::Result<()> {
825 Ok(())
826 }
827 }
828
829 #[test]
832 fn topo_sort_happy_path() {
833 let mut b = RegistryBuilder::default();
834 b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
836 b.register_core_with_meta("core_b", &["core_a"], Arc::new(DummyCore));
837
838 let reg = b.build_topo_sorted().unwrap();
839 let order: Vec<_> = reg.modules().iter().map(|m| m.name).collect();
840 assert_eq!(order, vec!["core_a", "core_b"]);
841 }
842
843 #[test]
844 fn unknown_dependency_error() {
845 let mut b = RegistryBuilder::default();
846 b.register_core_with_meta("core_a", &["missing_dep"], Arc::new(DummyCore));
847
848 let err = b.build_topo_sorted().unwrap_err();
849 match err {
850 RegistryError::UnknownDependency { module, depends_on } => {
851 assert_eq!(module, "core_a");
852 assert_eq!(depends_on, "missing_dep");
853 }
854 other => panic!("unexpected error: {other:?}"),
855 }
856 }
857
858 #[test]
859 fn cyclic_dependency_detected() {
860 let mut b = RegistryBuilder::default();
861 b.register_core_with_meta("a", &["b"], Arc::new(DummyCore));
862 b.register_core_with_meta("b", &["a"], Arc::new(DummyCore));
863
864 let err = b.build_topo_sorted().unwrap_err();
865 match err {
866 RegistryError::CycleDetected { path } => {
867 assert!(path.contains(&"a"));
869 assert!(path.contains(&"b"));
870 assert!(path.len() >= 3); }
872 other => panic!("expected CycleDetected, got: {other:?}"),
873 }
874 }
875
876 #[test]
877 fn complex_cycle_detection_with_path() {
878 let mut b = RegistryBuilder::default();
879 b.register_core_with_meta("a", &["b"], Arc::new(DummyCore));
881 b.register_core_with_meta("b", &["c"], Arc::new(DummyCore));
882 b.register_core_with_meta("c", &["a"], Arc::new(DummyCore));
883 b.register_core_with_meta("d", &[], Arc::new(DummyCore));
885
886 let err = b.build_topo_sorted().unwrap_err();
887 match err {
888 RegistryError::CycleDetected { path } => {
889 assert!(path.contains(&"a"));
891 assert!(path.contains(&"b"));
892 assert!(path.contains(&"c"));
893 assert!(!path.contains(&"d")); assert!(path.len() >= 4); let error_msg = format!("{}", RegistryError::CycleDetected { path: path.clone() });
898 assert!(error_msg.contains("cyclic dependency detected"));
899 assert!(error_msg.contains("->"));
900 }
901 other => panic!("expected CycleDetected, got: {other:?}"),
902 }
903 }
904
905 #[test]
906 fn duplicate_core_reported_in_configuration_errors() {
907 let mut b = RegistryBuilder::default();
908 b.register_core_with_meta("a", &[], Arc::new(DummyCore));
909 b.register_core_with_meta("a", &[], Arc::new(DummyCore));
911
912 let err = b.build_topo_sorted().unwrap_err();
913 match err {
914 RegistryError::InvalidRegistryConfiguration { errors } => {
915 assert!(
916 errors.iter().any(|e| e.contains("already registered")),
917 "expected duplicate registration error, got {errors:?}"
918 );
919 }
920 other => panic!("unexpected error: {other:?}"),
921 }
922 }
923
924 #[test]
925 fn rest_capability_without_core_fails() {
926 let mut b = RegistryBuilder::default();
927 b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
928 b.register_rest_with_meta("unknown_module", Arc::new(DummyRest));
930
931 let err = b.build_topo_sorted().unwrap_err();
932 match err {
933 RegistryError::UnknownModule(name) => {
934 assert_eq!(name, "unknown_module");
935 }
936 other => panic!("expected UnknownModule, got: {other:?}"),
937 }
938 }
939
940 #[test]
941 fn db_capability_without_core_fails() {
942 let mut b = RegistryBuilder::default();
943 b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
944 b.register_db_with_meta("unknown_module", Arc::new(DummyDb));
946
947 let err = b.build_topo_sorted().unwrap_err();
948 match err {
949 RegistryError::UnknownModule(name) => {
950 assert_eq!(name, "unknown_module");
951 }
952 other => panic!("expected UnknownModule, got: {other:?}"),
953 }
954 }
955
956 #[test]
957 fn stateful_capability_without_core_fails() {
958 let mut b = RegistryBuilder::default();
959 b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
960 b.register_stateful_with_meta("unknown_module", Arc::new(DummyStateful));
962
963 let err = b.build_topo_sorted().unwrap_err();
964 match err {
965 RegistryError::UnknownModule(name) => {
966 assert_eq!(name, "unknown_module");
967 }
968 other => panic!("expected UnknownModule, got: {other:?}"),
969 }
970 }
971
972 #[test]
973 fn capability_query_works() {
974 let mut b = RegistryBuilder::default();
975 let module = Arc::new(DummyCore);
976 b.register_core_with_meta("test", &[], module);
977 b.register_db_with_meta("test", Arc::new(DummyDb));
978 b.register_rest_with_meta("test", Arc::new(DummyRest));
979
980 let reg = b.build_topo_sorted().unwrap();
981 let entry = ®.modules()[0];
982
983 assert!(entry.caps.has::<DatabaseCap>());
984 assert!(entry.caps.has::<RestApiCap>());
985 assert!(!entry.caps.has::<SystemCap>());
986
987 assert!(entry.caps.query::<DatabaseCap>().is_some());
988 assert!(entry.caps.query::<RestApiCap>().is_some());
989 assert!(entry.caps.query::<SystemCap>().is_none());
990 }
991
992 #[test]
993 fn rest_host_capability_without_core_fails() {
994 let mut b = RegistryBuilder::default();
995 b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
996 b.register_rest_host_with_meta("unknown_host", Arc::new(DummyRestHost));
998
999 let err = b.build_topo_sorted().unwrap_err();
1000 match err {
1001 RegistryError::UnknownModule(name) => {
1002 assert_eq!(name, "unknown_host");
1003 }
1004 other => panic!("expected UnknownModule, got: {other:?}"),
1005 }
1006 }
1007
1008 #[test]
1009 fn module_entry_getters_work() {
1010 let mut b = RegistryBuilder::default();
1011 b.register_core_with_meta("alpha", &[], Arc::new(DummyCore));
1012 b.register_core_with_meta("beta", &["alpha"], Arc::new(DummyCore));
1013 b.register_rest_with_meta("beta", Arc::new(DummyRest));
1014
1015 let reg = b.build_topo_sorted().unwrap();
1016 let beta = reg.modules().iter().find(|e| e.name() == "beta").unwrap();
1017
1018 assert_eq!(beta.name(), "beta");
1019 assert_eq!(beta.deps(), &["alpha"]);
1020 assert!(beta.caps().has::<RestApiCap>());
1021 }
1022
1023 #[test]
1024 fn test_module_registry_builds() {
1025 let registry = ModuleRegistry::discover_and_build();
1026 assert!(registry.is_ok(), "Registry should build successfully");
1027 }
1028
1029 #[derive(Default, Clone)]
1031 struct DummyRest;
1032 impl contracts::RestApiCapability for DummyRest {
1033 fn register_rest(
1034 &self,
1035 _ctx: &crate::context::ModuleCtx,
1036 _router: axum::Router,
1037 _openapi: &dyn crate::api::OpenApiRegistry,
1038 ) -> anyhow::Result<axum::Router> {
1039 Ok(axum::Router::new())
1040 }
1041 }
1042
1043 #[derive(Default)]
1044 struct DummyDb;
1045 impl contracts::DatabaseCapability for DummyDb {
1046 fn migrations(&self) -> Vec<Box<dyn sea_orm_migration::MigrationTrait>> {
1047 vec![]
1048 }
1049 }
1050
1051 #[derive(Default)]
1052 struct DummyStateful;
1053 #[async_trait::async_trait]
1054 impl contracts::RunnableCapability for DummyStateful {
1055 async fn start(&self, _cancel: tokio_util::sync::CancellationToken) -> anyhow::Result<()> {
1056 Ok(())
1057 }
1058 async fn stop(&self, _cancel: tokio_util::sync::CancellationToken) -> anyhow::Result<()> {
1059 Ok(())
1060 }
1061 }
1062
1063 #[derive(Default)]
1064 struct DummyRestHost;
1065 impl contracts::ApiGatewayCapability for DummyRestHost {
1066 fn rest_prepare(
1067 &self,
1068 _ctx: &crate::context::ModuleCtx,
1069 router: axum::Router,
1070 ) -> anyhow::Result<axum::Router> {
1071 Ok(router)
1072 }
1073 fn rest_finalize(
1074 &self,
1075 _ctx: &crate::context::ModuleCtx,
1076 router: axum::Router,
1077 ) -> anyhow::Result<axum::Router> {
1078 Ok(router)
1079 }
1080 fn as_registry(&self) -> &dyn crate::contracts::OpenApiRegistry {
1081 panic!("DummyRestHost::as_registry should not be called in tests")
1082 }
1083 }
1084}