1use std::collections::HashMap;
17use grapsus_common::ids::{QualifiedId, Scope};
18use grapsus_common::limits::Limits;
19
20use crate::{AgentConfig, Config, FilterConfig, ListenerConfig, RouteConfig, UpstreamConfig};
21
22#[derive(Debug, Clone)]
31pub struct FlattenedConfig {
32 pub upstreams: HashMap<QualifiedId, UpstreamConfig>,
34
35 pub routes: Vec<(QualifiedId, RouteConfig)>,
37
38 pub agents: HashMap<QualifiedId, AgentConfig>,
40
41 pub filters: HashMap<QualifiedId, FilterConfig>,
43
44 pub listeners: Vec<(QualifiedId, ListenerConfig)>,
46
47 pub scope_limits: HashMap<Scope, Limits>,
49
50 pub exported_upstreams: HashMap<String, QualifiedId>,
52
53 pub exported_agents: HashMap<String, QualifiedId>,
55
56 pub exported_filters: HashMap<String, QualifiedId>,
58}
59
60impl FlattenedConfig {
61 pub fn new() -> Self {
63 Self {
64 upstreams: HashMap::new(),
65 routes: Vec::new(),
66 agents: HashMap::new(),
67 filters: HashMap::new(),
68 listeners: Vec::new(),
69 scope_limits: HashMap::new(),
70 exported_upstreams: HashMap::new(),
71 exported_agents: HashMap::new(),
72 exported_filters: HashMap::new(),
73 }
74 }
75
76 pub fn get_upstream(&self, qid: &QualifiedId) -> Option<&UpstreamConfig> {
78 self.upstreams.get(qid)
79 }
80
81 pub fn get_upstream_by_canonical(&self, canonical: &str) -> Option<&UpstreamConfig> {
83 self.upstreams.get(&QualifiedId::parse(canonical))
84 }
85
86 pub fn get_agent(&self, qid: &QualifiedId) -> Option<&AgentConfig> {
88 self.agents.get(qid)
89 }
90
91 pub fn get_filter(&self, qid: &QualifiedId) -> Option<&FilterConfig> {
93 self.filters.get(qid)
94 }
95
96 pub fn get_limits(&self, scope: &Scope) -> Option<&Limits> {
101 self.scope_limits.get(scope)
102 }
103
104 pub fn get_effective_limits(&self, scope: &Scope) -> Option<&Limits> {
108 for s in scope.chain() {
109 if let Some(limits) = self.scope_limits.get(&s) {
110 return Some(limits);
111 }
112 }
113 None
114 }
115
116 pub fn routes_in_scope<'a>(
118 &'a self,
119 scope: &'a Scope,
120 ) -> impl Iterator<Item = &'a (QualifiedId, RouteConfig)> {
121 self.routes
122 .iter()
123 .filter(move |(qid, _)| &qid.scope == scope)
124 }
125
126 pub fn listeners_in_scope<'a>(
128 &'a self,
129 scope: &'a Scope,
130 ) -> impl Iterator<Item = &'a (QualifiedId, ListenerConfig)> {
131 self.listeners
132 .iter()
133 .filter(move |(qid, _)| &qid.scope == scope)
134 }
135
136 pub fn is_upstream_exported(&self, name: &str) -> bool {
138 self.exported_upstreams.contains_key(name)
139 }
140
141 pub fn get_exported_upstream_qid(&self, name: &str) -> Option<&QualifiedId> {
143 self.exported_upstreams.get(name)
144 }
145}
146
147impl Default for FlattenedConfig {
148 fn default() -> Self {
149 Self::new()
150 }
151}
152
153impl Config {
158 pub fn flatten(&self) -> FlattenedConfig {
163 let mut flat = FlattenedConfig::new();
164
165 flat.scope_limits.insert(Scope::Global, self.limits.clone());
167
168 self.flatten_global(&mut flat);
170
171 for ns in &self.namespaces {
173 self.flatten_namespace(ns, &mut flat);
174 }
175
176 flat
177 }
178
179 fn flatten_global(&self, flat: &mut FlattenedConfig) {
180 for (id, upstream) in &self.upstreams {
182 flat.upstreams
183 .insert(QualifiedId::global(id), upstream.clone());
184 }
185
186 for route in &self.routes {
188 flat.routes
189 .push((QualifiedId::global(&route.id), route.clone()));
190 }
191
192 for agent in &self.agents {
194 flat.agents
195 .insert(QualifiedId::global(&agent.id), agent.clone());
196 }
197
198 for (id, filter) in &self.filters {
200 flat.filters.insert(QualifiedId::global(id), filter.clone());
201 }
202
203 for listener in &self.listeners {
205 flat.listeners
206 .push((QualifiedId::global(&listener.id), listener.clone()));
207 }
208 }
209
210 fn flatten_namespace(&self, ns: &crate::NamespaceConfig, flat: &mut FlattenedConfig) {
211 let ns_scope = Scope::Namespace(ns.id.clone());
212
213 if let Some(ref limits) = ns.limits {
215 flat.scope_limits.insert(ns_scope.clone(), limits.clone());
216 }
217
218 for (id, upstream) in &ns.upstreams {
220 let qid = QualifiedId::namespaced(&ns.id, id);
221 flat.upstreams.insert(qid.clone(), upstream.clone());
222
223 if ns.exports.upstreams.contains(id) {
225 flat.exported_upstreams.insert(id.clone(), qid);
226 }
227 }
228
229 for route in &ns.routes {
231 flat.routes
232 .push((QualifiedId::namespaced(&ns.id, &route.id), route.clone()));
233 }
234
235 for agent in &ns.agents {
237 let qid = QualifiedId::namespaced(&ns.id, &agent.id);
238 flat.agents.insert(qid.clone(), agent.clone());
239
240 if ns.exports.agents.contains(&agent.id) {
242 flat.exported_agents.insert(agent.id.clone(), qid);
243 }
244 }
245
246 for (id, filter) in &ns.filters {
248 let qid = QualifiedId::namespaced(&ns.id, id);
249 flat.filters.insert(qid.clone(), filter.clone());
250
251 if ns.exports.filters.contains(id) {
253 flat.exported_filters.insert(id.clone(), qid);
254 }
255 }
256
257 for listener in &ns.listeners {
259 flat.listeners.push((
260 QualifiedId::namespaced(&ns.id, &listener.id),
261 listener.clone(),
262 ));
263 }
264
265 for svc in &ns.services {
267 self.flatten_service(&ns.id, svc, flat);
268 }
269 }
270
271 fn flatten_service(&self, ns_id: &str, svc: &crate::ServiceConfig, flat: &mut FlattenedConfig) {
272 let svc_scope = Scope::Service {
273 namespace: ns_id.to_string(),
274 service: svc.id.clone(),
275 };
276
277 if let Some(ref limits) = svc.limits {
279 flat.scope_limits.insert(svc_scope.clone(), limits.clone());
280 }
281
282 for (id, upstream) in &svc.upstreams {
284 flat.upstreams.insert(
285 QualifiedId::in_service(ns_id, &svc.id, id),
286 upstream.clone(),
287 );
288 }
289
290 for route in &svc.routes {
292 flat.routes.push((
293 QualifiedId::in_service(ns_id, &svc.id, &route.id),
294 route.clone(),
295 ));
296 }
297
298 for agent in &svc.agents {
300 flat.agents.insert(
301 QualifiedId::in_service(ns_id, &svc.id, &agent.id),
302 agent.clone(),
303 );
304 }
305
306 for (id, filter) in &svc.filters {
308 flat.filters
309 .insert(QualifiedId::in_service(ns_id, &svc.id, id), filter.clone());
310 }
311
312 if let Some(ref listener) = svc.listener {
314 flat.listeners.push((
315 QualifiedId::in_service(ns_id, &svc.id, &listener.id),
316 listener.clone(),
317 ));
318 }
319 }
320}
321
322#[cfg(test)]
327mod tests {
328 use super::*;
329 use crate::{
330 namespace::{ExportConfig, NamespaceConfig, ServiceConfig},
331 ConnectionPoolConfig, HttpVersionConfig, UpstreamTarget, UpstreamTimeouts,
332 };
333 use grapsus_common::types::LoadBalancingAlgorithm;
334
335 fn test_upstream(id: &str) -> UpstreamConfig {
336 UpstreamConfig {
337 id: id.to_string(),
338 targets: vec![UpstreamTarget {
339 address: "127.0.0.1:8080".to_string(),
340 weight: 1,
341 max_requests: None,
342 metadata: HashMap::new(),
343 }],
344 load_balancing: LoadBalancingAlgorithm::RoundRobin,
345 sticky_session: None,
346 health_check: None,
347 connection_pool: ConnectionPoolConfig::default(),
348 timeouts: UpstreamTimeouts::default(),
349 tls: None,
350 http_version: HttpVersionConfig::default(),
351 }
352 }
353
354 fn test_config() -> Config {
355 let mut config = Config::default_for_testing();
356
357 config.upstreams.insert(
359 "global-backend".to_string(),
360 test_upstream("global-backend"),
361 );
362
363 let mut ns = NamespaceConfig::new("api");
365 ns.upstreams
366 .insert("ns-backend".to_string(), test_upstream("ns-backend"));
367 ns.upstreams.insert(
368 "shared-backend".to_string(),
369 test_upstream("shared-backend"),
370 );
371 ns.exports = ExportConfig {
372 upstreams: vec!["shared-backend".to_string()],
373 agents: vec![],
374 filters: vec![],
375 };
376
377 let mut svc = ServiceConfig::new("payments");
379 svc.upstreams
380 .insert("svc-backend".to_string(), test_upstream("svc-backend"));
381 ns.services.push(svc);
382
383 config.namespaces.push(ns);
384 config
385 }
386
387 #[test]
388 fn test_flatten_global_upstreams() {
389 let config = test_config();
390 let flat = config.flatten();
391
392 let qid = QualifiedId::global("global-backend");
394 assert!(flat.upstreams.contains_key(&qid));
395 assert_eq!(flat.get_upstream(&qid).unwrap().id, "global-backend");
396 }
397
398 #[test]
399 fn test_flatten_namespace_upstreams() {
400 let config = test_config();
401 let flat = config.flatten();
402
403 let qid = QualifiedId::namespaced("api", "ns-backend");
405 assert!(flat.upstreams.contains_key(&qid));
406 assert_eq!(flat.get_upstream(&qid).unwrap().id, "ns-backend");
407 }
408
409 #[test]
410 fn test_flatten_service_upstreams() {
411 let config = test_config();
412 let flat = config.flatten();
413
414 let qid = QualifiedId::in_service("api", "payments", "svc-backend");
416 assert!(flat.upstreams.contains_key(&qid));
417 assert_eq!(flat.get_upstream(&qid).unwrap().id, "svc-backend");
418 }
419
420 #[test]
421 fn test_flatten_exported_upstreams() {
422 let config = test_config();
423 let flat = config.flatten();
424
425 assert!(flat.is_upstream_exported("shared-backend"));
427 assert!(!flat.is_upstream_exported("ns-backend"));
428
429 let exported_qid = flat.get_exported_upstream_qid("shared-backend").unwrap();
430 assert_eq!(exported_qid.canonical(), "api:shared-backend");
431 }
432
433 #[test]
434 fn test_get_upstream_by_canonical() {
435 let config = test_config();
436 let flat = config.flatten();
437
438 let upstream = flat.get_upstream_by_canonical("api:ns-backend").unwrap();
440 assert_eq!(upstream.id, "ns-backend");
441
442 let service_upstream = flat
443 .get_upstream_by_canonical("api:payments:svc-backend")
444 .unwrap();
445 assert_eq!(service_upstream.id, "svc-backend");
446 }
447
448 #[test]
449 fn test_flatten_scope_limits() {
450 let mut config = test_config();
451
452 let ns = config.namespaces.get_mut(0).unwrap();
454 ns.limits = Some(Limits::for_testing());
455
456 let flat = config.flatten();
457
458 assert!(flat.scope_limits.contains_key(&Scope::Global));
460
461 assert!(flat
463 .scope_limits
464 .contains_key(&Scope::Namespace("api".to_string())));
465 }
466
467 #[test]
468 fn test_get_effective_limits() {
469 let mut config = test_config();
470
471 let ns = config.namespaces.get_mut(0).unwrap();
473 ns.limits = Some(Limits::for_testing());
474
475 let flat = config.flatten();
476
477 let svc_scope = Scope::Service {
479 namespace: "api".to_string(),
480 service: "payments".to_string(),
481 };
482 let limits = flat.get_effective_limits(&svc_scope);
483 assert!(limits.is_some());
484 }
485
486 #[test]
487 fn test_routes_in_scope() {
488 let config = test_config();
489 let flat = config.flatten();
490
491 let global_routes: Vec<_> = flat.routes_in_scope(&Scope::Global).collect();
493 assert!(!global_routes.is_empty());
494 }
495
496 #[test]
497 fn test_flatten_preserves_route_order() {
498 let config = test_config();
499 let flat = config.flatten();
500
501 let route_ids: Vec<_> = flat.routes.iter().map(|(qid, _)| qid.canonical()).collect();
503 assert!(!route_ids.is_empty());
504 }
505}