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 Database(Arc<dyn contracts::DatabaseCapability>),
21 RestApi(Arc<dyn contracts::RestApiCapability>),
22 ApiGateway(Arc<dyn contracts::ApiGatewayCapability>),
23 Runnable(Arc<dyn contracts::RunnableCapability>),
24 System(Arc<dyn contracts::SystemCapability>),
25 GrpcHub(Arc<dyn contracts::GrpcHubCapability>),
26 GrpcService(Arc<dyn contracts::GrpcServiceCapability>),
27}
28
29impl std::fmt::Debug for Capability {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 match self {
32 Capability::Database(_) => write!(f, "Database(<impl DatabaseCapability>)"),
33 Capability::RestApi(_) => write!(f, "RestApi(<impl RestApiCapability>)"),
34 Capability::ApiGateway(_) => write!(f, "ApiGateway(<impl ApiGatewayCapability>)"),
35 Capability::Runnable(_) => write!(f, "Runnable(<impl RunnableCapability>)"),
36 Capability::System(_) => write!(f, "System(<impl SystemCapability>)"),
37 Capability::GrpcHub(_) => write!(f, "GrpcHub(<impl GrpcHubCapability>)"),
38 Capability::GrpcService(_) => write!(f, "GrpcService(<impl GrpcServiceCapability>)"),
39 }
40 }
41}
42
43pub trait CapTag {
45 type Out: ?Sized + 'static;
46 fn try_get(cap: &Capability) -> Option<&Arc<Self::Out>>;
47}
48
49pub struct DatabaseCap;
51impl CapTag for DatabaseCap {
52 type Out = dyn contracts::DatabaseCapability;
53 fn try_get(cap: &Capability) -> Option<&Arc<Self::Out>> {
54 match cap {
55 Capability::Database(v) => Some(v),
56 _ => None,
57 }
58 }
59}
60
61pub struct RestApiCap;
63impl CapTag for RestApiCap {
64 type Out = dyn contracts::RestApiCapability;
65 fn try_get(cap: &Capability) -> Option<&Arc<Self::Out>> {
66 match cap {
67 Capability::RestApi(v) => Some(v),
68 _ => None,
69 }
70 }
71}
72
73pub struct ApiGatewayCap;
75impl CapTag for ApiGatewayCap {
76 type Out = dyn contracts::ApiGatewayCapability;
77 fn try_get(cap: &Capability) -> Option<&Arc<Self::Out>> {
78 match cap {
79 Capability::ApiGateway(v) => Some(v),
80 _ => None,
81 }
82 }
83}
84
85pub struct RunnableCap;
87impl CapTag for RunnableCap {
88 type Out = dyn contracts::RunnableCapability;
89 fn try_get(cap: &Capability) -> Option<&Arc<Self::Out>> {
90 match cap {
91 Capability::Runnable(v) => Some(v),
92 _ => None,
93 }
94 }
95}
96
97pub struct SystemCap;
99impl CapTag for SystemCap {
100 type Out = dyn contracts::SystemCapability;
101 fn try_get(cap: &Capability) -> Option<&Arc<Self::Out>> {
102 match cap {
103 Capability::System(v) => Some(v),
104 _ => None,
105 }
106 }
107}
108
109pub struct GrpcHubCap;
111impl CapTag for GrpcHubCap {
112 type Out = dyn contracts::GrpcHubCapability;
113 fn try_get(cap: &Capability) -> Option<&Arc<Self::Out>> {
114 match cap {
115 Capability::GrpcHub(v) => Some(v),
116 _ => None,
117 }
118 }
119}
120
121pub struct GrpcServiceCap;
123impl CapTag for GrpcServiceCap {
124 type Out = dyn contracts::GrpcServiceCapability;
125 fn try_get(cap: &Capability) -> Option<&Arc<Self::Out>> {
126 match cap {
127 Capability::GrpcService(v) => Some(v),
128 _ => None,
129 }
130 }
131}
132
133#[derive(Clone)]
135pub struct CapabilitySet {
136 caps: Vec<Capability>,
137}
138
139impl std::fmt::Debug for CapabilitySet {
140 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141 f.debug_struct("CapabilitySet")
142 .field("caps", &self.caps)
143 .finish()
144 }
145}
146
147impl CapabilitySet {
148 #[must_use]
150 pub fn new() -> Self {
151 Self { caps: Vec::new() }
152 }
153
154 pub fn push(&mut self, cap: Capability) {
156 self.caps.push(cap);
157 }
158
159 #[must_use]
161 pub fn has<T: CapTag>(&self) -> bool {
162 self.caps.iter().any(|cap| T::try_get(cap).is_some())
163 }
164
165 #[must_use]
167 pub fn query<T: CapTag>(&self) -> Option<Arc<T::Out>> {
168 self.caps.iter().find_map(|cap| T::try_get(cap).cloned())
169 }
170}
171
172impl Default for CapabilitySet {
173 fn default() -> Self {
174 Self::new()
175 }
176}
177
178pub struct ModuleEntry {
179 pub(crate) name: &'static str,
180 pub(crate) deps: &'static [&'static str],
181 pub(crate) core: Arc<dyn contracts::Module>,
182 pub(crate) caps: CapabilitySet,
183}
184
185impl std::fmt::Debug for ModuleEntry {
186 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187 f.debug_struct("ModuleEntry")
188 .field("name", &self.name)
189 .field("deps", &self.deps)
190 .field("has_rest", &self.caps.has::<RestApiCap>())
191 .field("is_rest_host", &self.caps.has::<ApiGatewayCap>())
192 .field("has_db", &self.caps.has::<DatabaseCap>())
193 .field("has_stateful", &self.caps.has::<RunnableCap>())
194 .field("is_system", &self.caps.has::<SystemCap>())
195 .field("is_grpc_hub", &self.caps.has::<GrpcHubCap>())
196 .field("has_grpc_service", &self.caps.has::<GrpcServiceCap>())
197 .finish_non_exhaustive()
198 }
199}
200
201pub struct Registrator(pub fn(&mut RegistryBuilder));
204
205inventory::collect!(Registrator);
206
207pub struct ModuleRegistry {
209 modules: Vec<ModuleEntry>, pub grpc_hub: Option<String>,
211 pub grpc_services: Vec<(String, Arc<dyn contracts::GrpcServiceCapability>)>,
212}
213
214impl std::fmt::Debug for ModuleRegistry {
215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216 let names: Vec<&'static str> = self.modules.iter().map(|m| m.name).collect();
217 f.debug_struct("ModuleRegistry")
218 .field("modules", &names)
219 .field("has_grpc_hub", &self.grpc_hub.is_some())
220 .field("grpc_services_count", &self.grpc_services.len())
221 .finish()
222 }
223}
224
225impl ModuleRegistry {
226 #[must_use]
227 pub fn modules(&self) -> &[ModuleEntry] {
228 &self.modules
229 }
230
231 #[must_use]
235 pub fn modules_by_system_priority(&self) -> Vec<&ModuleEntry> {
236 let mut system_mods = Vec::new();
237 let mut non_system_mods = Vec::new();
238
239 for entry in &self.modules {
240 if entry.caps.has::<SystemCap>() {
241 system_mods.push(entry);
242 } else {
243 non_system_mods.push(entry);
244 }
245 }
246
247 system_mods.extend(non_system_mods);
248 system_mods
249 }
250
251 pub fn discover_and_build() -> Result<Self, RegistryError> {
256 let mut b = RegistryBuilder::default();
257 for r in ::inventory::iter::<Registrator> {
258 r.0(&mut b);
259 }
260 b.build_topo_sorted()
261 }
262
263 #[must_use]
265 pub fn get_module(&self, name: &str) -> Option<Arc<dyn contracts::Module>> {
266 self.modules
267 .iter()
268 .find(|e| e.name == name)
269 .map(|e| e.core.clone())
270 }
271}
272
273type GrpcHubEntry = (&'static str, Arc<dyn contracts::GrpcHubCapability>);
275
276#[derive(Default)]
279pub struct RegistryBuilder {
280 core: HashMap<&'static str, Arc<dyn contracts::Module>>,
281 deps: HashMap<&'static str, &'static [&'static str]>,
282 capabilities: HashMap<&'static str, Vec<Capability>>,
283 rest_host: Option<RestHostEntry>,
284 grpc_hub: Option<GrpcHubEntry>,
285 errors: Vec<String>,
286}
287
288type DependencyGraph = (
290 Vec<&'static str>,
291 Vec<Vec<usize>>,
292 HashMap<&'static str, usize>,
293);
294
295impl RegistryBuilder {
296 pub fn register_core_with_meta(
297 &mut self,
298 name: &'static str,
299 deps: &'static [&'static str],
300 m: Arc<dyn contracts::Module>,
301 ) {
302 if self.core.contains_key(name) {
303 self.errors
304 .push(format!("Module '{name}' is already registered"));
305 return;
306 }
307 self.core.insert(name, m);
308 self.deps.insert(name, deps);
309 }
310
311 pub fn register_rest_with_meta(
312 &mut self,
313 name: &'static str,
314 m: Arc<dyn contracts::RestApiCapability>,
315 ) {
316 self.capabilities
317 .entry(name)
318 .or_default()
319 .push(Capability::RestApi(m));
320 }
321
322 pub fn register_rest_host_with_meta(
323 &mut self,
324 name: &'static str,
325 m: Arc<dyn contracts::ApiGatewayCapability>,
326 ) {
327 if let Some((existing, _)) = &self.rest_host {
328 self.errors.push(format!(
329 "Multiple REST host modules detected: '{existing}' and '{name}'. Only one REST host is allowed."
330 ));
331 return;
332 }
333 self.rest_host = Some((name, m));
334 }
335
336 pub fn register_db_with_meta(
337 &mut self,
338 name: &'static str,
339 m: Arc<dyn contracts::DatabaseCapability>,
340 ) {
341 self.capabilities
342 .entry(name)
343 .or_default()
344 .push(Capability::Database(m));
345 }
346
347 pub fn register_stateful_with_meta(
348 &mut self,
349 name: &'static str,
350 m: Arc<dyn contracts::RunnableCapability>,
351 ) {
352 self.capabilities
353 .entry(name)
354 .or_default()
355 .push(Capability::Runnable(m));
356 }
357
358 pub fn register_system_with_meta(
359 &mut self,
360 name: &'static str,
361 m: Arc<dyn contracts::SystemCapability>,
362 ) {
363 self.capabilities
364 .entry(name)
365 .or_default()
366 .push(Capability::System(m));
367 }
368
369 pub fn register_grpc_hub_with_meta(
370 &mut self,
371 name: &'static str,
372 m: Arc<dyn contracts::GrpcHubCapability>,
373 ) {
374 if let Some((existing, _)) = &self.grpc_hub {
375 self.errors.push(format!(
376 "Multiple gRPC hub modules detected: '{existing}' and '{name}'. Only one gRPC hub is allowed."
377 ));
378 return;
379 }
380 self.grpc_hub = Some((name, m));
381 }
382
383 pub fn register_grpc_service_with_meta(
384 &mut self,
385 name: &'static str,
386 m: Arc<dyn contracts::GrpcServiceCapability>,
387 ) {
388 self.capabilities
389 .entry(name)
390 .or_default()
391 .push(Capability::GrpcService(m));
392 }
393
394 fn detect_cycle_with_path(
397 names: &[&'static str],
398 adj: &[Vec<usize>],
399 ) -> Option<Vec<&'static str>> {
400 #[derive(Clone, Copy, PartialEq)]
401 enum Color {
402 White, Gray, Black, }
406
407 fn dfs(
408 node: usize,
409 names: &[&'static str],
410 adj: &[Vec<usize>],
411 colors: &mut [Color],
412 path: &mut Vec<usize>,
413 ) -> Option<Vec<&'static str>> {
414 colors[node] = Color::Gray;
415 path.push(node);
416
417 for &neighbor in &adj[node] {
418 match colors[neighbor] {
419 Color::Gray => {
420 if let Some(cycle_start) = path.iter().position(|&n| n == neighbor) {
423 let cycle_indices = &path[cycle_start..];
424 let mut cycle_path: Vec<&'static str> =
425 cycle_indices.iter().map(|&i| names[i]).collect();
426 cycle_path.push(names[neighbor]);
428 return Some(cycle_path);
429 }
430 }
431 Color::White => {
432 if let Some(cycle) = dfs(neighbor, names, adj, colors, path) {
433 return Some(cycle);
434 }
435 }
436 Color::Black => {
437 }
439 }
440 }
441
442 path.pop();
443 colors[node] = Color::Black;
444 None
445 }
446
447 let mut colors = vec![Color::White; names.len()];
448 let mut path = Vec::new();
449
450 for i in 0..names.len() {
451 if colors[i] == Color::White
452 && let Some(cycle) = dfs(i, names, adj, &mut colors, &mut path)
453 {
454 return Some(cycle);
455 }
456 }
457
458 None
459 }
460
461 fn validate_capabilities(&self) -> Result<(), RegistryError> {
463 if let Some((host_name, _)) = &self.rest_host
465 && !self.core.contains_key(host_name)
466 {
467 return Err(RegistryError::UnknownModule((*host_name).to_owned()));
468 }
469
470 if !self.errors.is_empty() {
472 return Err(RegistryError::InvalidRegistryConfiguration {
473 errors: self.errors.clone(),
474 });
475 }
476
477 for name in self.capabilities.keys() {
479 if !self.core.contains_key(name) {
480 return Err(RegistryError::UnknownModule((*name).to_owned()));
481 }
482 }
483
484 if let Some((name, _)) = &self.grpc_hub
486 && !self.core.contains_key(name)
487 {
488 return Err(RegistryError::UnknownModule((*name).to_owned()));
489 }
490
491 Ok(())
492 }
493
494 fn build_dependency_graph(&self) -> Result<DependencyGraph, RegistryError> {
496 let names: Vec<&'static str> = self.core.keys().copied().collect();
497 let mut idx: HashMap<&'static str, usize> = HashMap::new();
498 for (i, &n) in names.iter().enumerate() {
499 idx.insert(n, i);
500 }
501
502 let mut adj = vec![Vec::<usize>::new(); names.len()];
503
504 for (&n, &deps) in &self.deps {
505 let u = *idx
506 .get(n)
507 .ok_or_else(|| RegistryError::UnknownModule(n.to_owned()))?;
508 for &d in deps {
509 let v = *idx.get(d).ok_or_else(|| RegistryError::UnknownDependency {
510 module: n.to_owned(),
511 depends_on: d.to_owned(),
512 })?;
513 adj[v].push(u);
515 }
516 }
517
518 Ok((names, adj, idx))
519 }
520
521 fn assemble_entries(
523 &self,
524 order: &[usize],
525 names: &[&'static str],
526 ) -> Result<Vec<ModuleEntry>, RegistryError> {
527 let mut entries = Vec::with_capacity(order.len());
528 for &i in order {
529 let name = names[i];
530 let deps = *self
531 .deps
532 .get(name)
533 .ok_or_else(|| RegistryError::MissingDeps(name.to_owned()))?;
534
535 let core = self
536 .core
537 .get(name)
538 .cloned()
539 .ok_or_else(|| RegistryError::CoreNotFound(name.to_owned()))?;
540
541 let mut caps = CapabilitySet::new();
543
544 if let Some(module_caps) = self.capabilities.get(name) {
546 for cap in module_caps {
547 caps.push(cap.clone());
548 }
549 }
550
551 if let Some((host_name, module)) = &self.rest_host
553 && *host_name == name
554 {
555 caps.push(Capability::ApiGateway(module.clone()));
556 }
557
558 if let Some((hub_name, module)) = &self.grpc_hub
560 && *hub_name == name
561 {
562 caps.push(Capability::GrpcHub(module.clone()));
563 }
564
565 let entry = ModuleEntry {
566 name,
567 deps,
568 core,
569 caps,
570 };
571 entries.push(entry);
572 }
573 Ok(entries)
574 }
575
576 pub fn build_topo_sorted(self) -> Result<ModuleRegistry, RegistryError> {
581 self.validate_capabilities()?;
583
584 let (names, adj, _idx) = self.build_dependency_graph()?;
586
587 if let Some(cycle_path) = Self::detect_cycle_with_path(&names, &adj) {
589 return Err(RegistryError::CycleDetected { path: cycle_path });
590 }
591
592 let mut indeg = vec![0usize; names.len()];
594 for adj_list in &adj {
595 for &target in adj_list {
596 indeg[target] += 1;
597 }
598 }
599
600 let mut q = VecDeque::new();
601 for (i, °ree) in indeg.iter().enumerate() {
602 if degree == 0 {
603 q.push_back(i);
604 }
605 }
606
607 let mut order = Vec::with_capacity(names.len());
608 while let Some(u) = q.pop_front() {
609 order.push(u);
610 for &w in &adj[u] {
611 indeg[w] -= 1;
612 if indeg[w] == 0 {
613 q.push_back(w);
614 }
615 }
616 }
617
618 let entries = self.assemble_entries(&order, &names)?;
620
621 let grpc_hub = self.grpc_hub.as_ref().map(|(name, _)| (*name).to_owned());
623
624 let mut grpc_services: Vec<(String, Arc<dyn contracts::GrpcServiceCapability>)> =
626 Vec::new();
627 for (name, caps) in &self.capabilities {
628 for cap in caps {
629 if let Capability::GrpcService(service) = cap {
630 grpc_services.push(((*name).to_owned(), service.clone()));
631 }
632 }
633 }
634
635 tracing::info!(
636 modules = ?entries.iter().map(|e| e.name).collect::<Vec<_>>(),
637 "Module dependency order resolved (topo)"
638 );
639
640 Ok(ModuleRegistry {
641 modules: entries,
642 grpc_hub,
643 grpc_services,
644 })
645 }
646}
647
648#[derive(Debug, Error)]
650pub enum RegistryError {
651 #[error("pre-init failed for module '{module}'")]
653 PreInit {
654 module: &'static str,
655 #[source]
656 source: anyhow::Error,
657 },
658 #[error("initialization failed for module '{module}'")]
659 Init {
660 module: &'static str,
661 #[source]
662 source: anyhow::Error,
663 },
664 #[error("post-init failed for module '{module}'")]
665 PostInit {
666 module: &'static str,
667 #[source]
668 source: anyhow::Error,
669 },
670 #[error("start failed for '{module}'")]
671 Start {
672 module: &'static str,
673 #[source]
674 source: anyhow::Error,
675 },
676
677 #[error("DB migration failed for module '{module}'")]
678 DbMigrate {
679 module: &'static str,
680 #[source]
681 source: anyhow::Error,
682 },
683 #[error("REST prepare failed for host module '{module}'")]
684 RestPrepare {
685 module: &'static str,
686 #[source]
687 source: anyhow::Error,
688 },
689 #[error("REST registration failed for module '{module}'")]
690 RestRegister {
691 module: &'static str,
692 #[source]
693 source: anyhow::Error,
694 },
695 #[error("REST finalize failed for host module '{module}'")]
696 RestFinalize {
697 module: &'static str,
698 #[source]
699 source: anyhow::Error,
700 },
701 #[error(
702 "REST phase requires an gateway host: modules with capability 'rest' found, but no module with capability 'rest_host'"
703 )]
704 RestRequiresHost,
705 #[error("multiple 'rest_host' modules detected; exactly one is allowed")]
706 MultipleRestHosts,
707 #[error("REST host module not found after validation")]
708 RestHostNotFoundAfterValidation,
709 #[error("REST host missing from entry")]
710 RestHostMissingFromEntry,
711
712 #[error("gRPC registration failed for module '{module}'")]
714 GrpcRegister {
715 module: String,
716 #[source]
717 source: anyhow::Error,
718 },
719 #[error(
720 "gRPC phase requires a hub: modules with capability 'grpc' found, but no module with capability 'grpc_hub'"
721 )]
722 GrpcRequiresHub,
723 #[error("multiple 'grpc_hub' modules detected; exactly one is allowed")]
724 MultipleGrpcHubs,
725
726 #[error("OoP spawn failed for module '{module}'")]
728 OopSpawn {
729 module: String,
730 #[source]
731 source: anyhow::Error,
732 },
733
734 #[error("unknown module '{0}'")]
736 UnknownModule(String),
737 #[error("module '{module}' depends on unknown '{depends_on}'")]
738 UnknownDependency { module: String, depends_on: String },
739 #[error("cyclic dependency detected: {}", path.join(" -> "))]
740 CycleDetected { path: Vec<&'static str> },
741 #[error("missing deps for '{0}'")]
742 MissingDeps(String),
743 #[error("core not found for '{0}'")]
744 CoreNotFound(String),
745 #[error("invalid registry configuration:\n{errors:#?}")]
746 InvalidRegistryConfiguration { errors: Vec<String> },
747}
748
749#[cfg(test)]
750#[cfg_attr(coverage_nightly, coverage(off))]
751mod tests {
752 use super::*;
753 use std::sync::Arc;
754
755 use crate::context::ModuleCtx;
757 use crate::contracts;
758
759 #[derive(Default)]
761 struct DummyCore;
762 #[async_trait::async_trait]
763 impl contracts::Module for DummyCore {
764 async fn init(&self, _ctx: &ModuleCtx) -> anyhow::Result<()> {
765 Ok(())
766 }
767 }
768
769 #[test]
772 fn topo_sort_happy_path() {
773 let mut b = RegistryBuilder::default();
774 b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
776 b.register_core_with_meta("core_b", &["core_a"], Arc::new(DummyCore));
777
778 let reg = b.build_topo_sorted().unwrap();
779 let order: Vec<_> = reg.modules().iter().map(|m| m.name).collect();
780 assert_eq!(order, vec!["core_a", "core_b"]);
781 }
782
783 #[test]
784 fn unknown_dependency_error() {
785 let mut b = RegistryBuilder::default();
786 b.register_core_with_meta("core_a", &["missing_dep"], Arc::new(DummyCore));
787
788 let err = b.build_topo_sorted().unwrap_err();
789 match err {
790 RegistryError::UnknownDependency { module, depends_on } => {
791 assert_eq!(module, "core_a");
792 assert_eq!(depends_on, "missing_dep");
793 }
794 other => panic!("unexpected error: {other:?}"),
795 }
796 }
797
798 #[test]
799 fn cyclic_dependency_detected() {
800 let mut b = RegistryBuilder::default();
801 b.register_core_with_meta("a", &["b"], Arc::new(DummyCore));
802 b.register_core_with_meta("b", &["a"], Arc::new(DummyCore));
803
804 let err = b.build_topo_sorted().unwrap_err();
805 match err {
806 RegistryError::CycleDetected { path } => {
807 assert!(path.contains(&"a"));
809 assert!(path.contains(&"b"));
810 assert!(path.len() >= 3); }
812 other => panic!("expected CycleDetected, got: {other:?}"),
813 }
814 }
815
816 #[test]
817 fn complex_cycle_detection_with_path() {
818 let mut b = RegistryBuilder::default();
819 b.register_core_with_meta("a", &["b"], Arc::new(DummyCore));
821 b.register_core_with_meta("b", &["c"], Arc::new(DummyCore));
822 b.register_core_with_meta("c", &["a"], Arc::new(DummyCore));
823 b.register_core_with_meta("d", &[], 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.contains(&"c"));
833 assert!(!path.contains(&"d")); assert!(path.len() >= 4); let error_msg = format!("{}", RegistryError::CycleDetected { path: path.clone() });
838 assert!(error_msg.contains("cyclic dependency detected"));
839 assert!(error_msg.contains("->"));
840 }
841 other => panic!("expected CycleDetected, got: {other:?}"),
842 }
843 }
844
845 #[test]
846 fn duplicate_core_reported_in_configuration_errors() {
847 let mut b = RegistryBuilder::default();
848 b.register_core_with_meta("a", &[], Arc::new(DummyCore));
849 b.register_core_with_meta("a", &[], Arc::new(DummyCore));
851
852 let err = b.build_topo_sorted().unwrap_err();
853 match err {
854 RegistryError::InvalidRegistryConfiguration { errors } => {
855 assert!(
856 errors.iter().any(|e| e.contains("already registered")),
857 "expected duplicate registration error, got {errors:?}"
858 );
859 }
860 other => panic!("unexpected error: {other:?}"),
861 }
862 }
863
864 #[test]
865 fn rest_capability_without_core_fails() {
866 let mut b = RegistryBuilder::default();
867 b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
868 b.register_rest_with_meta("unknown_module", Arc::new(DummyRest));
870
871 let err = b.build_topo_sorted().unwrap_err();
872 match err {
873 RegistryError::UnknownModule(name) => {
874 assert_eq!(name, "unknown_module");
875 }
876 other => panic!("expected UnknownModule, got: {other:?}"),
877 }
878 }
879
880 #[test]
881 fn db_capability_without_core_fails() {
882 let mut b = RegistryBuilder::default();
883 b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
884 b.register_db_with_meta("unknown_module", Arc::new(DummyDb));
886
887 let err = b.build_topo_sorted().unwrap_err();
888 match err {
889 RegistryError::UnknownModule(name) => {
890 assert_eq!(name, "unknown_module");
891 }
892 other => panic!("expected UnknownModule, got: {other:?}"),
893 }
894 }
895
896 #[test]
897 fn stateful_capability_without_core_fails() {
898 let mut b = RegistryBuilder::default();
899 b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
900 b.register_stateful_with_meta("unknown_module", Arc::new(DummyStateful));
902
903 let err = b.build_topo_sorted().unwrap_err();
904 match err {
905 RegistryError::UnknownModule(name) => {
906 assert_eq!(name, "unknown_module");
907 }
908 other => panic!("expected UnknownModule, got: {other:?}"),
909 }
910 }
911
912 #[test]
913 fn capability_query_works() {
914 let mut b = RegistryBuilder::default();
915 let module = Arc::new(DummyCore);
916 b.register_core_with_meta("test", &[], module);
917 b.register_db_with_meta("test", Arc::new(DummyDb));
918 b.register_rest_with_meta("test", Arc::new(DummyRest));
919
920 let reg = b.build_topo_sorted().unwrap();
921 let entry = ®.modules()[0];
922
923 assert!(entry.caps.has::<DatabaseCap>());
924 assert!(entry.caps.has::<RestApiCap>());
925 assert!(!entry.caps.has::<SystemCap>());
926
927 assert!(entry.caps.query::<DatabaseCap>().is_some());
928 assert!(entry.caps.query::<RestApiCap>().is_some());
929 assert!(entry.caps.query::<SystemCap>().is_none());
930 }
931
932 #[test]
933 fn rest_host_capability_without_core_fails() {
934 let mut b = RegistryBuilder::default();
935 b.register_core_with_meta("core_a", &[], Arc::new(DummyCore));
936 b.register_rest_host_with_meta("unknown_host", Arc::new(DummyRestHost));
938
939 let err = b.build_topo_sorted().unwrap_err();
940 match err {
941 RegistryError::UnknownModule(name) => {
942 assert_eq!(name, "unknown_host");
943 }
944 other => panic!("expected UnknownModule, got: {other:?}"),
945 }
946 }
947
948 #[test]
949 fn test_module_registry_builds() {
950 let registry = ModuleRegistry::discover_and_build();
951 assert!(registry.is_ok(), "Registry should build successfully");
952 }
953
954 #[derive(Default, Clone)]
956 struct DummyRest;
957 impl contracts::RestApiCapability for DummyRest {
958 fn register_rest(
959 &self,
960 _ctx: &crate::context::ModuleCtx,
961 _router: axum::Router,
962 _openapi: &dyn crate::api::OpenApiRegistry,
963 ) -> anyhow::Result<axum::Router> {
964 Ok(axum::Router::new())
965 }
966 }
967
968 #[derive(Default)]
969 struct DummyDb;
970 #[async_trait::async_trait]
971 impl contracts::DatabaseCapability for DummyDb {
972 async fn migrate(&self, _db: &modkit_db::DbHandle) -> anyhow::Result<()> {
973 Ok(())
974 }
975 }
976
977 #[derive(Default)]
978 struct DummyStateful;
979 #[async_trait::async_trait]
980 impl contracts::RunnableCapability for DummyStateful {
981 async fn start(&self, _cancel: tokio_util::sync::CancellationToken) -> anyhow::Result<()> {
982 Ok(())
983 }
984 async fn stop(&self, _cancel: tokio_util::sync::CancellationToken) -> anyhow::Result<()> {
985 Ok(())
986 }
987 }
988
989 #[derive(Default)]
990 struct DummyRestHost;
991 impl contracts::ApiGatewayCapability for DummyRestHost {
992 fn rest_prepare(
993 &self,
994 _ctx: &crate::context::ModuleCtx,
995 router: axum::Router,
996 ) -> anyhow::Result<axum::Router> {
997 Ok(router)
998 }
999 fn rest_finalize(
1000 &self,
1001 _ctx: &crate::context::ModuleCtx,
1002 router: axum::Router,
1003 ) -> anyhow::Result<axum::Router> {
1004 Ok(router)
1005 }
1006 fn as_registry(&self) -> &dyn crate::contracts::OpenApiRegistry {
1007 panic!("DummyRestHost::as_registry should not be called in tests")
1008 }
1009 }
1010}