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