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, Eq)]
34pub struct DiscoveredGateway {
35    pub path: String,
36    pub path_shape: String,
37    pub module_name: Option<String>,
38    pub events: Vec<String>,
39}
40
41/// Snapshot of a resolved microservice message pattern.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct DiscoveredMessagePattern {
44    pub pattern: String,
45    pub kind: MessagePatternKind,
46    pub module_name: Option<String>,
47}
48
49/// Read-only discovery snapshot for a built Boot application.
50#[derive(Debug, Clone, PartialEq)]
51pub struct DiscoveryService {
52    modules: Vec<DiscoveredModule>,
53    routes: Vec<DiscoveredRoute>,
54    gateways: Vec<DiscoveredGateway>,
55    message_patterns: Vec<DiscoveredMessagePattern>,
56}
57
58impl DiscoveryService {
59    pub fn from_app(app: &BootApplication) -> Result<Self> {
60        Ok(Self {
61            modules: discover_modules(app)?,
62            routes: discover_routes(app),
63            gateways: discover_gateways(app),
64            message_patterns: discover_message_patterns(app),
65        })
66    }
67
68    pub fn modules(&self) -> &[DiscoveredModule] {
69        &self.modules
70    }
71
72    pub fn routes(&self) -> &[DiscoveredRoute] {
73        &self.routes
74    }
75
76    pub fn gateways(&self) -> &[DiscoveredGateway] {
77        &self.gateways
78    }
79
80    pub fn message_patterns(&self) -> &[DiscoveredMessagePattern] {
81        &self.message_patterns
82    }
83
84    pub fn module(&self, name: &str) -> Option<&DiscoveredModule> {
85        self.modules.iter().find(|module| module.name == name)
86    }
87
88    pub fn modules_with_provider(&self, token: &ProviderToken) -> Vec<&DiscoveredModule> {
89        self.modules
90            .iter()
91            .filter(|module| module.provider_tokens.contains(token))
92            .collect()
93    }
94
95    pub fn routes_for_module(&self, module_name: &str) -> Vec<&DiscoveredRoute> {
96        self.routes
97            .iter()
98            .filter(|route| route.module_name.as_deref() == Some(module_name))
99            .collect()
100    }
101
102    pub fn routes_for_controller(&self, controller_prefix: &str) -> Vec<&DiscoveredRoute> {
103        self.routes
104            .iter()
105            .filter(|route| route.controller_prefix.as_deref() == Some(controller_prefix))
106            .collect()
107    }
108
109    pub fn gateways_for_module(&self, module_name: &str) -> Vec<&DiscoveredGateway> {
110        self.gateways
111            .iter()
112            .filter(|gateway| gateway.module_name.as_deref() == Some(module_name))
113            .collect()
114    }
115
116    pub fn message_patterns_for_module(&self, module_name: &str) -> Vec<&DiscoveredMessagePattern> {
117        self.message_patterns
118            .iter()
119            .filter(|pattern| pattern.module_name.as_deref() == Some(module_name))
120            .collect()
121    }
122
123    pub fn reflector(&self) -> Reflector {
124        Reflector::new(self.clone())
125    }
126}
127
128/// Metadata lookup helper over a discovery snapshot.
129#[derive(Debug, Clone, PartialEq)]
130pub struct Reflector {
131    discovery: DiscoveryService,
132}
133
134impl Reflector {
135    pub fn new(discovery: DiscoveryService) -> Self {
136        Self { discovery }
137    }
138
139    pub fn from_app(app: &BootApplication) -> Result<Self> {
140        Ok(Self::new(DiscoveryService::from_app(app)?))
141    }
142
143    pub fn discovery(&self) -> &DiscoveryService {
144        &self.discovery
145    }
146
147    pub fn route(&self, method: HttpMethod, path: &str) -> Option<&DiscoveredRoute> {
148        self.discovery
149            .routes
150            .iter()
151            .find(|route| route.method == method && route.path == path)
152    }
153
154    pub fn openapi(&self, method: HttpMethod, path: &str) -> Option<&OpenApiRouteMetadata> {
155        self.route(method, path).map(|route| &route.openapi)
156    }
157
158    pub fn metadata(&self, method: HttpMethod, path: &str) -> Option<&BTreeMap<String, Value>> {
159        self.route(method, path).map(|route| &route.metadata)
160    }
161
162    pub fn metadata_value(&self, method: HttpMethod, path: &str, key: &str) -> Option<&Value> {
163        self.metadata(method, path)
164            .and_then(|metadata| metadata.get(key))
165    }
166
167    pub fn metadata_as<T>(&self, method: HttpMethod, path: &str, key: &str) -> Result<Option<T>>
168    where
169        T: DeserializeOwned,
170    {
171        let Some(value) = self.metadata_value(method, path, key) else {
172            return Ok(None);
173        };
174
175        serde_json::from_value(value.clone())
176            .map(Some)
177            .map_err(|error| {
178                BootError::Internal(format!(
179                    "failed to deserialize route metadata `{key}`: {error}"
180                ))
181            })
182    }
183
184    pub fn operation_id(&self, method: HttpMethod, path: &str) -> Option<&str> {
185        self.openapi(method, path)
186            .and_then(|metadata| metadata.operation_id.as_deref())
187    }
188
189    pub fn routes_with_tag(&self, tag: &str) -> Vec<&DiscoveredRoute> {
190        self.discovery
191            .routes
192            .iter()
193            .filter(|route| route.openapi.tags.iter().any(|value| value == tag))
194            .collect()
195    }
196
197    pub fn routes_with_metadata(&self, key: &str) -> Vec<&DiscoveredRoute> {
198        self.discovery
199            .routes
200            .iter()
201            .filter(|route| route.metadata.contains_key(key))
202            .collect()
203    }
204
205    pub fn routes_with_metadata_value(&self, key: &str, value: &Value) -> Vec<&DiscoveredRoute> {
206        self.discovery
207            .routes
208            .iter()
209            .filter(|route| route.metadata.get(key) == Some(value))
210            .collect()
211    }
212
213    pub fn routes_for_module(&self, module_name: &str) -> Vec<&DiscoveredRoute> {
214        self.discovery.routes_for_module(module_name)
215    }
216
217    pub fn routes_for_controller(&self, controller_prefix: &str) -> Vec<&DiscoveredRoute> {
218        self.discovery.routes_for_controller(controller_prefix)
219    }
220}
221
222fn discover_modules(app: &BootApplication) -> Result<Vec<DiscoveredModule>> {
223    app.module_instances
224        .iter()
225        .map(|instance| {
226            Ok(DiscoveredModule {
227                name: instance.module.name().to_string(),
228                provider_tokens: instance.module_ref.local_tokens()?,
229            })
230        })
231        .collect()
232}
233
234fn discover_routes(app: &BootApplication) -> Vec<DiscoveredRoute> {
235    app.routes()
236        .iter()
237        .map(|route| DiscoveredRoute {
238            method: route.method(),
239            path: route.path().to_string(),
240            path_shape: route.path_shape(),
241            path_params: route
242                .path_param_names()
243                .into_iter()
244                .map(str::to_string)
245                .collect(),
246            module_name: route.module_name().map(str::to_string),
247            controller_prefix: route.controller_prefix().map(str::to_string),
248            openapi: route.openapi().clone(),
249            versioning: route.versioning().clone(),
250            serialization: route.serialization().clone(),
251            metadata: route.metadata().clone(),
252            validation_enabled: route.validation_enabled(),
253        })
254        .collect()
255}
256
257fn discover_gateways(app: &BootApplication) -> Vec<DiscoveredGateway> {
258    app.gateways()
259        .iter()
260        .map(|gateway| DiscoveredGateway {
261            path: gateway.path().to_string(),
262            path_shape: gateway.path_shape(),
263            module_name: gateway.module_name().map(str::to_string),
264            events: gateway.events().into_iter().map(str::to_string).collect(),
265        })
266        .collect()
267}
268
269fn discover_message_patterns(app: &BootApplication) -> Vec<DiscoveredMessagePattern> {
270    app.message_patterns()
271        .iter()
272        .map(|pattern| DiscoveredMessagePattern {
273            pattern: pattern.pattern().to_string(),
274            kind: pattern.kind(),
275            module_name: pattern.module_name().map(str::to_string),
276        })
277        .collect()
278}