Skip to main content

module_orchestrator/domain/
service.rs

1use std::collections::HashSet;
2use std::sync::Arc;
3
4use modkit::registry::ModuleRegistry;
5use modkit::runtime::ModuleManager;
6use modkit_macros::domain_model;
7
8use super::model::{DeploymentMode, InstanceInfo, ModuleInfo};
9
10/// Lightweight compiled-module metadata (owned data, no trait objects).
11#[domain_model]
12struct CompiledModule {
13    name: String,
14    capabilities: Vec<String>,
15    deps: Vec<String>,
16}
17
18/// Service that assembles module information from catalog and runtime data.
19#[domain_model]
20pub struct ModulesService {
21    /// Compiled modules snapshot (built once at init, immutable after).
22    compiled: Vec<CompiledModule>,
23    /// Runtime module manager for live instance queries.
24    module_manager: Arc<ModuleManager>,
25}
26
27impl ModulesService {
28    /// Build from a live `ModuleRegistry` and a `ModuleManager`.
29    ///
30    /// Extracts module metadata (names, deps, capability labels) from the registry
31    /// and drops the registry afterwards — no trait objects are kept.
32    #[must_use]
33    pub fn new(registry: &ModuleRegistry, module_manager: Arc<ModuleManager>) -> Self {
34        let compiled: Vec<CompiledModule> = registry
35            .modules()
36            .iter()
37            .map(|entry| CompiledModule {
38                name: entry.name().to_owned(),
39                capabilities: entry
40                    .caps()
41                    .labels()
42                    .iter()
43                    .map(|s| (*s).to_owned())
44                    .collect(),
45                deps: entry.deps().iter().map(|d| (*d).to_owned()).collect(),
46            })
47            .collect();
48
49        Self {
50            compiled,
51            module_manager,
52        }
53    }
54
55    /// List all registered modules, merging compile-time catalog data with runtime instances.
56    #[must_use]
57    pub fn list_modules(&self) -> Vec<ModuleInfo> {
58        let mut modules = Vec::new();
59        let mut seen_names = HashSet::new();
60
61        // 1. Emit all compiled-in modules from the catalog.
62        for cm in &self.compiled {
63            seen_names.insert(cm.name.clone());
64
65            let instances = self.get_module_instances(&cm.name);
66
67            modules.push(ModuleInfo {
68                name: cm.name.clone(),
69                capabilities: cm.capabilities.clone(),
70                dependencies: cm.deps.clone(),
71                deployment_mode: DeploymentMode::CompiledIn,
72                instances,
73            });
74        }
75
76        // 2. Add any dynamically registered modules from ModuleManager
77        //    that are not in the compiled catalog (external / out-of-process).
78        for instance in self.module_manager.all_instances() {
79            if seen_names.contains(&instance.module) {
80                continue;
81            }
82            seen_names.insert(instance.module.clone());
83
84            let instances = self.get_module_instances(&instance.module);
85
86            modules.push(ModuleInfo {
87                name: instance.module.clone(),
88                capabilities: vec![],
89                dependencies: vec![],
90                deployment_mode: DeploymentMode::OutOfProcess,
91                instances,
92            });
93        }
94
95        // Sort by name for deterministic output
96        modules.sort_by(|a, b| a.name.cmp(&b.name));
97
98        modules
99    }
100
101    fn get_module_instances(&self, module_name: &str) -> Vec<InstanceInfo> {
102        self.module_manager
103            .instances_of(module_name)
104            .into_iter()
105            .map(|inst| {
106                let grpc_services = inst
107                    .grpc_services
108                    .iter()
109                    .map(|(name, ep)| (name.clone(), ep.uri.clone()))
110                    .collect();
111
112                InstanceInfo {
113                    instance_id: inst.instance_id,
114                    version: inst.version.clone(),
115                    state: inst.state(),
116                    grpc_services,
117                }
118            })
119            .collect()
120    }
121}
122
123#[cfg(test)]
124#[cfg_attr(coverage_nightly, coverage(off))]
125mod tests {
126    use super::*;
127    use modkit::registry::RegistryBuilder;
128    use modkit::runtime::{Endpoint, InstanceState, ModuleInstance, ModuleManager};
129    use uuid::Uuid;
130
131    // ---- Test helpers ----
132
133    // (name, deps, has_rest, has_system)
134    type ModuleSpec = (&'static str, &'static [&'static str], bool, bool);
135
136    #[domain_model]
137    #[derive(Default)]
138    struct DummyCore;
139    #[async_trait::async_trait]
140    impl modkit::Module for DummyCore {
141        async fn init(&self, _ctx: &modkit::context::ModuleCtx) -> anyhow::Result<()> {
142            Ok(())
143        }
144    }
145
146    #[domain_model]
147    #[derive(Default, Clone)]
148    struct DummyRest;
149    impl modkit::contracts::RestApiCapability for DummyRest {
150        fn register_rest(
151            &self,
152            _ctx: &modkit::context::ModuleCtx,
153            _router: axum::Router,
154            _openapi: &dyn modkit::api::OpenApiRegistry,
155        ) -> anyhow::Result<axum::Router> {
156            Ok(axum::Router::new())
157        }
158    }
159
160    #[domain_model]
161    #[derive(Default)]
162    struct DummySystem;
163    #[async_trait::async_trait]
164    impl modkit::contracts::SystemCapability for DummySystem {}
165
166    fn build_registry(modules: &[ModuleSpec]) -> ModuleRegistry {
167        let mut b = RegistryBuilder::default();
168        for &(name, deps, has_rest, has_system) in modules {
169            b.register_core_with_meta(name, deps, Arc::new(DummyCore));
170            if has_rest {
171                b.register_rest_with_meta(name, Arc::new(DummyRest));
172            }
173            if has_system {
174                b.register_system_with_meta(name, Arc::new(DummySystem));
175            }
176        }
177        b.build_topo_sorted().unwrap()
178    }
179
180    // ---- Tests ----
181
182    #[test]
183    fn list_compiled_in_modules_from_registry() {
184        let registry = build_registry(&[
185            ("api_gateway", &[], true, true),
186            ("nodes_registry", &["api_gateway"], true, false),
187        ]);
188        let manager = Arc::new(ModuleManager::new());
189        let svc = ModulesService::new(&registry, manager);
190        let modules = svc.list_modules();
191
192        assert_eq!(modules.len(), 2);
193        // Sorted by name
194        assert_eq!(modules[0].name, "api_gateway");
195        assert_eq!(modules[0].deployment_mode, DeploymentMode::CompiledIn);
196        assert!(modules[0].capabilities.contains(&"rest".to_owned()));
197        assert!(modules[0].capabilities.contains(&"system".to_owned()));
198        assert!(modules[0].instances.is_empty());
199
200        assert_eq!(modules[1].name, "nodes_registry");
201        assert_eq!(modules[1].dependencies, vec!["api_gateway"]);
202    }
203
204    #[test]
205    fn dynamic_external_instances_appear_as_out_of_process() {
206        let registry = build_registry(&[]);
207        let manager = Arc::new(ModuleManager::new());
208
209        let instance = Arc::new(
210            ModuleInstance::new("external_svc", Uuid::new_v4())
211                .with_version("2.0.0")
212                .with_grpc_service("ext.Service", Endpoint::http("127.0.0.1", 9001)),
213        );
214        manager.register_instance(instance);
215
216        let svc = ModulesService::new(&registry, manager);
217        let modules = svc.list_modules();
218
219        assert_eq!(modules.len(), 1);
220        assert_eq!(modules[0].name, "external_svc");
221        assert_eq!(modules[0].deployment_mode, DeploymentMode::OutOfProcess);
222        assert_eq!(modules[0].instances.len(), 1);
223        assert_eq!(modules[0].instances[0].version, Some("2.0.0".to_owned()));
224        assert!(
225            modules[0].instances[0]
226                .grpc_services
227                .contains_key("ext.Service")
228        );
229    }
230
231    #[test]
232    fn compiled_in_modules_show_instances_from_manager() {
233        let registry = build_registry(&[("grpc_hub", &[], false, true)]);
234        let manager = Arc::new(ModuleManager::new());
235
236        let instance =
237            Arc::new(ModuleInstance::new("grpc_hub", Uuid::new_v4()).with_version("0.1.0"));
238        manager.register_instance(instance);
239
240        let svc = ModulesService::new(&registry, manager);
241        let modules = svc.list_modules();
242
243        assert_eq!(modules.len(), 1);
244        assert_eq!(modules[0].name, "grpc_hub");
245        assert_eq!(modules[0].deployment_mode, DeploymentMode::CompiledIn);
246        assert_eq!(modules[0].instances.len(), 1);
247    }
248
249    #[test]
250    fn instance_state_maps_correctly() {
251        let registry = build_registry(&[]);
252        let manager = Arc::new(ModuleManager::new());
253
254        let instance = Arc::new(ModuleInstance::new("svc", Uuid::new_v4()));
255        // Default state is Registered
256        manager.register_instance(instance);
257
258        let svc = ModulesService::new(&registry, manager);
259        let modules = svc.list_modules();
260
261        assert_eq!(modules[0].instances[0].state, InstanceState::Registered);
262    }
263
264    #[test]
265    fn result_is_sorted_by_name() {
266        let registry =
267            build_registry(&[("zebra", &[], false, false), ("alpha", &[], false, false)]);
268        let manager = Arc::new(ModuleManager::new());
269
270        let svc = ModulesService::new(&registry, manager);
271        let modules = svc.list_modules();
272
273        assert_eq!(modules[0].name, "alpha");
274        assert_eq!(modules[1].name, "zebra");
275    }
276}