Skip to main content

a3s_boot/
discovery.rs

1use crate::{
2    BootApplication, BootError, HttpMethod, MessagePatternKind, OpenApiRouteMetadata,
3    ProviderToken, Result, RouteVersioning, SerializationOptions,
4};
5use serde::de::DeserializeOwned;
6use serde_json::Value;
7use std::collections::BTreeMap;
8
9/// Snapshot of a registered module and its local providers.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct DiscoveredModule {
12    pub name: String,
13    pub provider_tokens: Vec<ProviderToken>,
14}
15
16/// Snapshot of a resolved HTTP route.
17#[derive(Debug, Clone, PartialEq)]
18pub struct DiscoveredRoute {
19    pub method: HttpMethod,
20    pub path: String,
21    pub path_shape: String,
22    pub path_params: Vec<String>,
23    pub module_name: Option<String>,
24    pub controller_prefix: Option<String>,
25    pub openapi: OpenApiRouteMetadata,
26    pub versioning: RouteVersioning,
27    pub serialization: SerializationOptions,
28    pub metadata: BTreeMap<String, Value>,
29    pub validation_enabled: bool,
30}
31
32/// Snapshot of a resolved WebSocket gateway.
33#[derive(Debug, Clone, PartialEq)]
34pub struct DiscoveredGateway {
35    pub path: String,
36    pub path_shape: String,
37    pub namespace: Option<String>,
38    pub module_name: Option<String>,
39    pub events: Vec<String>,
40    pub metadata: BTreeMap<String, Value>,
41    pub event_metadata: BTreeMap<String, BTreeMap<String, Value>>,
42}
43
44/// Snapshot of a resolved microservice message pattern.
45#[derive(Debug, Clone, PartialEq)]
46pub struct DiscoveredMessagePattern {
47    pub pattern: String,
48    pub kind: MessagePatternKind,
49    pub module_name: Option<String>,
50    pub metadata: BTreeMap<String, Value>,
51}
52
53/// Read-only module graph snapshot for diagnostics and Nest-style devtools.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct ApplicationGraph {
56    pub modules: Vec<ApplicationGraphModule>,
57}
58
59impl ApplicationGraph {
60    pub fn module(&self, name: &str) -> Option<&ApplicationGraphModule> {
61        self.modules.iter().find(|module| module.name == name)
62    }
63
64    pub fn imports_of(&self, name: &str) -> Vec<&ApplicationGraphModule> {
65        let Some(module) = self.module(name) else {
66            return Vec::new();
67        };
68
69        module
70            .imports
71            .iter()
72            .filter_map(|import| self.module(import))
73            .collect()
74    }
75
76    pub fn dependents_of(&self, name: &str) -> Vec<&ApplicationGraphModule> {
77        self.modules
78            .iter()
79            .filter(|module| module.imports.iter().any(|import| import == name))
80            .collect()
81    }
82}
83
84/// One module node in an [`ApplicationGraph`].
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct ApplicationGraphModule {
87    pub name: String,
88    pub imports: Vec<String>,
89    pub provider_tokens: Vec<ProviderToken>,
90    pub export_tokens: Vec<ProviderToken>,
91    pub is_global: bool,
92    pub route_prefix: Option<String>,
93    pub route_count: usize,
94    pub gateway_count: usize,
95    pub message_pattern_count: usize,
96}
97
98/// Read-only discovery snapshot for a built Boot application.
99#[derive(Debug, Clone, PartialEq)]
100pub struct DiscoveryService {
101    modules: Vec<DiscoveredModule>,
102    routes: Vec<DiscoveredRoute>,
103    gateways: Vec<DiscoveredGateway>,
104    message_patterns: Vec<DiscoveredMessagePattern>,
105    graph: ApplicationGraph,
106}
107
108impl DiscoveryService {
109    pub fn from_app(app: &BootApplication) -> Result<Self> {
110        let modules = discover_modules(app)?;
111        let routes = discover_routes(app);
112        let gateways = discover_gateways(app);
113        let message_patterns = discover_message_patterns(app);
114        let graph = discover_application_graph(app, &routes, &gateways, &message_patterns)?;
115
116        Ok(Self {
117            modules,
118            routes,
119            gateways,
120            message_patterns,
121            graph,
122        })
123    }
124
125    pub fn modules(&self) -> &[DiscoveredModule] {
126        &self.modules
127    }
128
129    pub fn routes(&self) -> &[DiscoveredRoute] {
130        &self.routes
131    }
132
133    pub fn gateways(&self) -> &[DiscoveredGateway] {
134        &self.gateways
135    }
136
137    pub fn message_patterns(&self) -> &[DiscoveredMessagePattern] {
138        &self.message_patterns
139    }
140
141    pub fn graph(&self) -> &ApplicationGraph {
142        &self.graph
143    }
144
145    pub fn module(&self, name: &str) -> Option<&DiscoveredModule> {
146        self.modules.iter().find(|module| module.name == name)
147    }
148
149    pub fn modules_with_provider(&self, token: &ProviderToken) -> Vec<&DiscoveredModule> {
150        self.modules
151            .iter()
152            .filter(|module| module.provider_tokens.contains(token))
153            .collect()
154    }
155
156    pub fn routes_for_module(&self, module_name: &str) -> Vec<&DiscoveredRoute> {
157        self.routes
158            .iter()
159            .filter(|route| route.module_name.as_deref() == Some(module_name))
160            .collect()
161    }
162
163    pub fn routes_for_controller(&self, controller_prefix: &str) -> Vec<&DiscoveredRoute> {
164        self.routes
165            .iter()
166            .filter(|route| route.controller_prefix.as_deref() == Some(controller_prefix))
167            .collect()
168    }
169
170    pub fn gateways_for_module(&self, module_name: &str) -> Vec<&DiscoveredGateway> {
171        self.gateways
172            .iter()
173            .filter(|gateway| gateway.module_name.as_deref() == Some(module_name))
174            .collect()
175    }
176
177    pub fn message_patterns_for_module(&self, module_name: &str) -> Vec<&DiscoveredMessagePattern> {
178        self.message_patterns
179            .iter()
180            .filter(|pattern| pattern.module_name.as_deref() == Some(module_name))
181            .collect()
182    }
183
184    pub fn gateway(&self, path: &str) -> Option<&DiscoveredGateway> {
185        self.gateways.iter().find(|gateway| gateway.path == path)
186    }
187
188    pub fn message_pattern(&self, pattern: &str) -> Option<&DiscoveredMessagePattern> {
189        self.message_patterns
190            .iter()
191            .find(|message_pattern| message_pattern.pattern == pattern)
192    }
193
194    pub fn reflector(&self) -> Reflector {
195        Reflector::new(self.clone())
196    }
197}
198
199/// Metadata lookup helper over a discovery snapshot.
200#[derive(Debug, Clone, PartialEq)]
201pub struct Reflector {
202    discovery: DiscoveryService,
203}
204
205impl Reflector {
206    pub fn new(discovery: DiscoveryService) -> Self {
207        Self { discovery }
208    }
209
210    pub fn from_app(app: &BootApplication) -> Result<Self> {
211        Ok(Self::new(DiscoveryService::from_app(app)?))
212    }
213
214    pub fn discovery(&self) -> &DiscoveryService {
215        &self.discovery
216    }
217
218    pub fn route(&self, method: HttpMethod, path: &str) -> Option<&DiscoveredRoute> {
219        self.discovery
220            .routes
221            .iter()
222            .find(|route| route.method == method && route.path == path)
223    }
224
225    pub fn openapi(&self, method: HttpMethod, path: &str) -> Option<&OpenApiRouteMetadata> {
226        self.route(method, path).map(|route| &route.openapi)
227    }
228
229    pub fn metadata(&self, method: HttpMethod, path: &str) -> Option<&BTreeMap<String, Value>> {
230        self.route(method, path).map(|route| &route.metadata)
231    }
232
233    pub fn metadata_value(&self, method: HttpMethod, path: &str, key: &str) -> Option<&Value> {
234        self.metadata(method, path)
235            .and_then(|metadata| metadata.get(key))
236    }
237
238    pub fn metadata_as<T>(&self, method: HttpMethod, path: &str, key: &str) -> Result<Option<T>>
239    where
240        T: DeserializeOwned,
241    {
242        let Some(value) = self.metadata_value(method, path, key) else {
243            return Ok(None);
244        };
245
246        serde_json::from_value(value.clone())
247            .map(Some)
248            .map_err(|error| {
249                BootError::Internal(format!(
250                    "failed to deserialize route metadata `{key}`: {error}"
251                ))
252            })
253    }
254
255    pub fn operation_id(&self, method: HttpMethod, path: &str) -> Option<&str> {
256        self.openapi(method, path)
257            .and_then(|metadata| metadata.operation_id.as_deref())
258    }
259
260    pub fn routes_with_tag(&self, tag: &str) -> Vec<&DiscoveredRoute> {
261        self.discovery
262            .routes
263            .iter()
264            .filter(|route| route.openapi.tags.iter().any(|value| value == tag))
265            .collect()
266    }
267
268    pub fn routes_with_metadata(&self, key: &str) -> Vec<&DiscoveredRoute> {
269        self.discovery
270            .routes
271            .iter()
272            .filter(|route| route.metadata.contains_key(key))
273            .collect()
274    }
275
276    pub fn routes_with_metadata_value(&self, key: &str, value: &Value) -> Vec<&DiscoveredRoute> {
277        self.discovery
278            .routes
279            .iter()
280            .filter(|route| route.metadata.get(key) == Some(value))
281            .collect()
282    }
283
284    pub fn gateway(&self, path: &str) -> Option<&DiscoveredGateway> {
285        self.discovery.gateway(path)
286    }
287
288    pub fn gateway_metadata(&self, path: &str) -> Option<&BTreeMap<String, Value>> {
289        self.gateway(path).map(|gateway| &gateway.metadata)
290    }
291
292    pub fn gateway_metadata_value(&self, path: &str, key: &str) -> Option<&Value> {
293        self.gateway_metadata(path)
294            .and_then(|metadata| metadata.get(key))
295    }
296
297    pub fn gateway_metadata_as<T>(&self, path: &str, key: &str) -> Result<Option<T>>
298    where
299        T: DeserializeOwned,
300    {
301        let Some(value) = self.gateway_metadata_value(path, key) else {
302            return Ok(None);
303        };
304
305        serde_json::from_value(value.clone())
306            .map(Some)
307            .map_err(|error| {
308                BootError::Internal(format!(
309                    "failed to deserialize websocket gateway metadata `{key}`: {error}"
310                ))
311            })
312    }
313
314    pub fn gateway_event_metadata(
315        &self,
316        path: &str,
317        event: &str,
318    ) -> Option<&BTreeMap<String, Value>> {
319        self.gateway(path)
320            .and_then(|gateway| gateway.event_metadata.get(event))
321    }
322
323    pub fn gateway_event_metadata_value(
324        &self,
325        path: &str,
326        event: &str,
327        key: &str,
328    ) -> Option<&Value> {
329        self.gateway_event_metadata(path, event)
330            .and_then(|metadata| metadata.get(key))
331    }
332
333    pub fn gateway_event_metadata_as<T>(
334        &self,
335        path: &str,
336        event: &str,
337        key: &str,
338    ) -> Result<Option<T>>
339    where
340        T: DeserializeOwned,
341    {
342        let Some(value) = self.gateway_event_metadata_value(path, event, key) else {
343            return Ok(None);
344        };
345
346        serde_json::from_value(value.clone())
347            .map(Some)
348            .map_err(|error| {
349                BootError::Internal(format!(
350                    "failed to deserialize websocket event metadata `{key}`: {error}"
351                ))
352            })
353    }
354
355    pub fn message_pattern(&self, pattern: &str) -> Option<&DiscoveredMessagePattern> {
356        self.discovery.message_pattern(pattern)
357    }
358
359    pub fn message_pattern_metadata(&self, pattern: &str) -> Option<&BTreeMap<String, Value>> {
360        self.message_pattern(pattern)
361            .map(|message_pattern| &message_pattern.metadata)
362    }
363
364    pub fn message_pattern_metadata_value(&self, pattern: &str, key: &str) -> Option<&Value> {
365        self.message_pattern_metadata(pattern)
366            .and_then(|metadata| metadata.get(key))
367    }
368
369    pub fn message_pattern_metadata_as<T>(&self, pattern: &str, key: &str) -> Result<Option<T>>
370    where
371        T: DeserializeOwned,
372    {
373        let Some(value) = self.message_pattern_metadata_value(pattern, key) else {
374            return Ok(None);
375        };
376
377        serde_json::from_value(value.clone())
378            .map(Some)
379            .map_err(|error| {
380                BootError::Internal(format!(
381                    "failed to deserialize message pattern metadata `{key}`: {error}"
382                ))
383            })
384    }
385
386    pub fn routes_for_module(&self, module_name: &str) -> Vec<&DiscoveredRoute> {
387        self.discovery.routes_for_module(module_name)
388    }
389
390    pub fn routes_for_controller(&self, controller_prefix: &str) -> Vec<&DiscoveredRoute> {
391        self.discovery.routes_for_controller(controller_prefix)
392    }
393}
394
395fn discover_modules(app: &BootApplication) -> Result<Vec<DiscoveredModule>> {
396    app.module_instances
397        .iter()
398        .map(|instance| {
399            Ok(DiscoveredModule {
400                name: instance.module.name().to_string(),
401                provider_tokens: instance.module_ref.local_tokens()?,
402            })
403        })
404        .collect()
405}
406
407fn discover_application_graph(
408    app: &BootApplication,
409    routes: &[DiscoveredRoute],
410    gateways: &[DiscoveredGateway],
411    message_patterns: &[DiscoveredMessagePattern],
412) -> Result<ApplicationGraph> {
413    let mut modules = Vec::new();
414
415    for instance in &app.module_instances {
416        let name = instance.module.name().to_string();
417        let route_count = routes
418            .iter()
419            .filter(|route| route.module_name.as_deref() == Some(name.as_str()))
420            .count();
421        let gateway_count = gateways
422            .iter()
423            .filter(|gateway| gateway.module_name.as_deref() == Some(name.as_str()))
424            .count();
425        let message_pattern_count = message_patterns
426            .iter()
427            .filter(|pattern| pattern.module_name.as_deref() == Some(name.as_str()))
428            .count();
429
430        modules.push(ApplicationGraphModule {
431            name,
432            imports: instance.imports.clone(),
433            provider_tokens: instance.module_ref.local_tokens()?,
434            export_tokens: instance.exports.clone(),
435            is_global: instance.is_global,
436            route_prefix: instance.route_prefix.clone(),
437            route_count,
438            gateway_count,
439            message_pattern_count,
440        });
441    }
442
443    Ok(ApplicationGraph { modules })
444}
445
446fn discover_routes(app: &BootApplication) -> Vec<DiscoveredRoute> {
447    app.routes()
448        .iter()
449        .map(|route| DiscoveredRoute {
450            method: route.method(),
451            path: route.path().to_string(),
452            path_shape: route.path_shape(),
453            path_params: route
454                .path_param_names()
455                .into_iter()
456                .map(str::to_string)
457                .collect(),
458            module_name: route.module_name().map(str::to_string),
459            controller_prefix: route.controller_prefix().map(str::to_string),
460            openapi: route.openapi().clone(),
461            versioning: route.versioning().clone(),
462            serialization: route.serialization().clone(),
463            metadata: route.metadata().clone(),
464            validation_enabled: route.validation_enabled(),
465        })
466        .collect()
467}
468
469fn discover_gateways(app: &BootApplication) -> Vec<DiscoveredGateway> {
470    app.gateways()
471        .iter()
472        .map(|gateway| DiscoveredGateway {
473            path: gateway.path().to_string(),
474            path_shape: gateway.path_shape(),
475            namespace: gateway.namespace().map(str::to_string),
476            module_name: gateway.module_name().map(str::to_string),
477            events: gateway.events().into_iter().map(str::to_string).collect(),
478            metadata: gateway.metadata().clone(),
479            event_metadata: gateway
480                .events()
481                .into_iter()
482                .filter_map(|event| {
483                    gateway
484                        .event_metadata(event)
485                        .map(|metadata| (event.to_string(), metadata.clone()))
486                })
487                .collect(),
488        })
489        .collect()
490}
491
492fn discover_message_patterns(app: &BootApplication) -> Vec<DiscoveredMessagePattern> {
493    app.message_patterns()
494        .iter()
495        .map(|pattern| DiscoveredMessagePattern {
496            pattern: pattern.pattern().to_string(),
497            kind: pattern.kind(),
498            module_name: pattern.module_name().map(str::to_string),
499            metadata: pattern.metadata().clone(),
500        })
501        .collect()
502}