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("unknown module '{0}'")]
754 UnknownModule(String),
755 #[error("module '{module}' depends on unknown '{depends_on}'")]
756 UnknownDependency { module: String, depends_on: String },
757 #[error("cyclic dependency detected: {}", path.join(" -> "))]
758 CycleDetected { path: Vec<&'static str> },
759 #[error("missing deps for '{0}'")]
760 MissingDeps(String),
761 #[error("core not found for '{0}'")]
762 CoreNotFound(String),
763 #[error("invalid registry configuration:\n{errors:#?}")]
764 InvalidRegistryConfiguration { errors: Vec<String> },
765}
766
767#[cfg(test)]
768#[cfg_attr(coverage_nightly, coverage(off))]
769mod tests {
770 use super::*;
771 use std::sync::Arc;
772
773 use crate::context::ModuleCtx;
775 use crate::contracts;
776
777 #[derive(Default)]
779 struct DummyCore;
780 #[async_trait::async_trait]
781 impl contracts::Module for DummyCore {
782 async fn init(&self, _ctx: &ModuleCtx) -> anyhow::Result<()> {
783 Ok(())
784 }
785 }
786
787 #[test]
790 fn topo_sort_happy_path() {
791 let mut b = RegistryBuilder::default();
792 b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
794 b.register_core_with_meta("core_b", &["core_a"], Arc::new(DummyCore));
795
796 let reg = b.build_topo_sorted().unwrap();
797 let order: Vec<_> = reg.modules().iter().map(|m| m.name).collect();
798 assert_eq!(order, vec!["core_a", "core_b"]);
799 }
800
801 #[test]
802 fn unknown_dependency_error() {
803 let mut b = RegistryBuilder::default();
804 b.register_core_with_meta("core_a", &["missing_dep"], Arc::new(DummyCore));
805
806 let err = b.build_topo_sorted().unwrap_err();
807 match err {
808 RegistryError::UnknownDependency { module, depends_on } => {
809 assert_eq!(module, "core_a");
810 assert_eq!(depends_on, "missing_dep");
811 }
812 other => panic!("unexpected error: {other:?}"),
813 }
814 }
815
816 #[test]
817 fn cyclic_dependency_detected() {
818 let mut b = RegistryBuilder::default();
819 b.register_core_with_meta("a", &["b"], Arc::new(DummyCore));
820 b.register_core_with_meta("b", &["a"], Arc::new(DummyCore));
821
822 let err = b.build_topo_sorted().unwrap_err();
823 match err {
824 RegistryError::CycleDetected { path } => {
825 assert!(path.contains(&"a"));
827 assert!(path.contains(&"b"));
828 assert!(path.len() >= 3); }
830 other => panic!("expected CycleDetected, got: {other:?}"),
831 }
832 }
833
834 #[test]
835 fn complex_cycle_detection_with_path() {
836 let mut b = RegistryBuilder::default();
837 b.register_core_with_meta("a", &["b"], Arc::new(DummyCore));
839 b.register_core_with_meta("b", &["c"], Arc::new(DummyCore));
840 b.register_core_with_meta("c", &["a"], Arc::new(DummyCore));
841 b.register_core_with_meta("d", &[], Arc::new(DummyCore));
843
844 let err = b.build_topo_sorted().unwrap_err();
845 match err {
846 RegistryError::CycleDetected { path } => {
847 assert!(path.contains(&"a"));
849 assert!(path.contains(&"b"));
850 assert!(path.contains(&"c"));
851 assert!(!path.contains(&"d")); assert!(path.len() >= 4); let error_msg = format!("{}", RegistryError::CycleDetected { path: path.clone() });
856 assert!(error_msg.contains("cyclic dependency detected"));
857 assert!(error_msg.contains("->"));
858 }
859 other => panic!("expected CycleDetected, got: {other:?}"),
860 }
861 }
862
863 #[test]
864 fn duplicate_core_reported_in_configuration_errors() {
865 let mut b = RegistryBuilder::default();
866 b.register_core_with_meta("a", &[], Arc::new(DummyCore));
867 b.register_core_with_meta("a", &[], Arc::new(DummyCore));
869
870 let err = b.build_topo_sorted().unwrap_err();
871 match err {
872 RegistryError::InvalidRegistryConfiguration { errors } => {
873 assert!(
874 errors.iter().any(|e| e.contains("already registered")),
875 "expected duplicate registration error, got {errors:?}"
876 );
877 }
878 other => panic!("unexpected error: {other:?}"),
879 }
880 }
881
882 #[test]
883 fn rest_capability_without_core_fails() {
884 let mut b = RegistryBuilder::default();
885 b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
886 b.register_rest_with_meta("unknown_module", Arc::new(DummyRest));
888
889 let err = b.build_topo_sorted().unwrap_err();
890 match err {
891 RegistryError::UnknownModule(name) => {
892 assert_eq!(name, "unknown_module");
893 }
894 other => panic!("expected UnknownModule, got: {other:?}"),
895 }
896 }
897
898 #[test]
899 fn db_capability_without_core_fails() {
900 let mut b = RegistryBuilder::default();
901 b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
902 b.register_db_with_meta("unknown_module", Arc::new(DummyDb));
904
905 let err = b.build_topo_sorted().unwrap_err();
906 match err {
907 RegistryError::UnknownModule(name) => {
908 assert_eq!(name, "unknown_module");
909 }
910 other => panic!("expected UnknownModule, got: {other:?}"),
911 }
912 }
913
914 #[test]
915 fn stateful_capability_without_core_fails() {
916 let mut b = RegistryBuilder::default();
917 b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
918 b.register_stateful_with_meta("unknown_module", Arc::new(DummyStateful));
920
921 let err = b.build_topo_sorted().unwrap_err();
922 match err {
923 RegistryError::UnknownModule(name) => {
924 assert_eq!(name, "unknown_module");
925 }
926 other => panic!("expected UnknownModule, got: {other:?}"),
927 }
928 }
929
930 #[test]
931 fn capability_query_works() {
932 let mut b = RegistryBuilder::default();
933 let module = Arc::new(DummyCore);
934 b.register_core_with_meta("test", &[], module);
935 b.register_db_with_meta("test", Arc::new(DummyDb));
936 b.register_rest_with_meta("test", Arc::new(DummyRest));
937
938 let reg = b.build_topo_sorted().unwrap();
939 let entry = ®.modules()[0];
940
941 assert!(entry.caps.has::<DatabaseCap>());
942 assert!(entry.caps.has::<RestApiCap>());
943 assert!(!entry.caps.has::<SystemCap>());
944
945 assert!(entry.caps.query::<DatabaseCap>().is_some());
946 assert!(entry.caps.query::<RestApiCap>().is_some());
947 assert!(entry.caps.query::<SystemCap>().is_none());
948 }
949
950 #[test]
951 fn rest_host_capability_without_core_fails() {
952 let mut b = RegistryBuilder::default();
953 b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
954 b.register_rest_host_with_meta("unknown_host", Arc::new(DummyRestHost));
956
957 let err = b.build_topo_sorted().unwrap_err();
958 match err {
959 RegistryError::UnknownModule(name) => {
960 assert_eq!(name, "unknown_host");
961 }
962 other => panic!("expected UnknownModule, got: {other:?}"),
963 }
964 }
965
966 #[test]
967 fn test_module_registry_builds() {
968 let registry = ModuleRegistry::discover_and_build();
969 assert!(registry.is_ok(), "Registry should build successfully");
970 }
971
972 #[derive(Default, Clone)]
974 struct DummyRest;
975 impl contracts::RestApiCapability for DummyRest {
976 fn register_rest(
977 &self,
978 _ctx: &crate::context::ModuleCtx,
979 _router: axum::Router,
980 _openapi: &dyn crate::api::OpenApiRegistry,
981 ) -> anyhow::Result<axum::Router> {
982 Ok(axum::Router::new())
983 }
984 }
985
986 #[derive(Default)]
987 struct DummyDb;
988 impl contracts::DatabaseCapability for DummyDb {
989 fn migrations(&self) -> Vec<Box<dyn sea_orm_migration::MigrationTrait>> {
990 vec![]
991 }
992 }
993
994 #[derive(Default)]
995 struct DummyStateful;
996 #[async_trait::async_trait]
997 impl contracts::RunnableCapability for DummyStateful {
998 async fn start(&self, _cancel: tokio_util::sync::CancellationToken) -> anyhow::Result<()> {
999 Ok(())
1000 }
1001 async fn stop(&self, _cancel: tokio_util::sync::CancellationToken) -> anyhow::Result<()> {
1002 Ok(())
1003 }
1004 }
1005
1006 #[derive(Default)]
1007 struct DummyRestHost;
1008 impl contracts::ApiGatewayCapability for DummyRestHost {
1009 fn rest_prepare(
1010 &self,
1011 _ctx: &crate::context::ModuleCtx,
1012 router: axum::Router,
1013 ) -> anyhow::Result<axum::Router> {
1014 Ok(router)
1015 }
1016 fn rest_finalize(
1017 &self,
1018 _ctx: &crate::context::ModuleCtx,
1019 router: axum::Router,
1020 ) -> anyhow::Result<axum::Router> {
1021 Ok(router)
1022 }
1023 fn as_registry(&self) -> &dyn crate::contracts::OpenApiRegistry {
1024 panic!("DummyRestHost::as_registry should not be called in tests")
1025 }
1026 }
1027}