grapsus_config/namespace.rs
1//! Namespace and service configuration for hierarchical organization.
2//!
3//! This module provides configuration types for organizing Grapsus resources
4//! into logical groups using namespaces and services.
5//!
6//! # Hierarchy
7//!
8//! ```text
9//! Config (root)
10//! ├── Global resources (listeners, routes, upstreams, agents, filters)
11//! └── namespaces[]
12//! ├── Namespace-level resources
13//! └── services[]
14//! └── Service-level resources
15//! ```
16//!
17//! # Scoping Rules
18//!
19//! - **Global resources**: Visible everywhere in the configuration
20//! - **Namespace resources**: Visible within the namespace and its services
21//! - **Service resources**: Local to the specific service
22//! - **Exports**: Namespace resources can be exported to make them globally visible
23//!
24//! Resolution follows "most specific wins": Service → Namespace → Exported → Global
25
26use serde::{Deserialize, Serialize};
27use std::collections::HashMap;
28
29use grapsus_common::limits::Limits;
30
31use crate::{AgentConfig, FilterConfig, ListenerConfig, RouteConfig, UpstreamConfig};
32
33// ============================================================================
34// Namespace Configuration
35// ============================================================================
36
37/// Configuration for a namespace - a logical grouping of related resources.
38///
39/// Namespaces provide domain-driven boundaries within the configuration,
40/// allowing operators to organize resources by team, service domain, or
41/// any other logical grouping.
42///
43/// # Example KDL
44///
45/// ```kdl
46/// namespace "api" {
47/// limits {
48/// max-body-size 10485760
49/// }
50///
51/// upstreams {
52/// upstream "backend" { ... }
53/// }
54///
55/// routes {
56/// route "users" {
57/// upstream "backend" // Resolves to api:backend
58/// }
59/// }
60///
61/// service "payments" {
62/// // Service-specific configuration
63/// }
64///
65/// exports {
66/// upstreams "backend" // Make globally visible
67/// }
68/// }
69/// ```
70#[derive(Debug, Clone, Serialize, Deserialize, Default)]
71pub struct NamespaceConfig {
72 /// Unique namespace identifier.
73 ///
74 /// Must not contain the `:` character as it's reserved for
75 /// qualified ID syntax (e.g., `namespace:resource`).
76 pub id: String,
77
78 /// Namespace-level limits.
79 ///
80 /// These limits override global limits and are overridden by
81 /// service-level limits. If not specified, global limits apply.
82 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub limits: Option<Limits>,
84
85 /// Namespace-level listeners.
86 ///
87 /// Listeners at the namespace level are shared across all
88 /// services within the namespace.
89 #[serde(default, skip_serializing_if = "Vec::is_empty")]
90 pub listeners: Vec<ListenerConfig>,
91
92 /// Namespace-level upstreams.
93 ///
94 /// These upstreams are visible to all routes within the namespace
95 /// and its services. They can be referenced without qualification
96 /// from within the namespace.
97 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
98 pub upstreams: HashMap<String, UpstreamConfig>,
99
100 /// Namespace-level routes.
101 ///
102 /// Routes defined at the namespace level can reference namespace
103 /// upstreams without qualification.
104 #[serde(default, skip_serializing_if = "Vec::is_empty")]
105 pub routes: Vec<RouteConfig>,
106
107 /// Namespace-level agents.
108 ///
109 /// Agents at this level are visible to all filters within the
110 /// namespace and its services.
111 #[serde(default, skip_serializing_if = "Vec::is_empty")]
112 pub agents: Vec<AgentConfig>,
113
114 /// Namespace-level filters.
115 ///
116 /// Filters at this level can be referenced by routes within
117 /// the namespace and its services.
118 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
119 pub filters: HashMap<String, FilterConfig>,
120
121 /// Services within this namespace.
122 ///
123 /// Services provide more granular grouping within a namespace,
124 /// typically representing individual microservices or API groups.
125 #[serde(default, skip_serializing_if = "Vec::is_empty")]
126 pub services: Vec<ServiceConfig>,
127
128 /// Resources exported from this namespace.
129 ///
130 /// Exported resources become globally visible and can be
131 /// referenced from any scope without qualification.
132 #[serde(default, skip_serializing_if = "ExportConfig::is_empty")]
133 pub exports: ExportConfig,
134}
135
136impl NamespaceConfig {
137 /// Create a new namespace with the given ID.
138 pub fn new(id: impl Into<String>) -> Self {
139 Self {
140 id: id.into(),
141 ..Default::default()
142 }
143 }
144
145 /// Returns true if this namespace contains no resources.
146 pub fn is_empty(&self) -> bool {
147 self.listeners.is_empty()
148 && self.upstreams.is_empty()
149 && self.routes.is_empty()
150 && self.agents.is_empty()
151 && self.filters.is_empty()
152 && self.services.is_empty()
153 && self.limits.is_none()
154 }
155
156 /// Get a service by ID within this namespace.
157 pub fn get_service(&self, id: &str) -> Option<&ServiceConfig> {
158 self.services.iter().find(|s| s.id == id)
159 }
160
161 /// Get a mutable service by ID within this namespace.
162 pub fn get_service_mut(&mut self, id: &str) -> Option<&mut ServiceConfig> {
163 self.services.iter_mut().find(|s| s.id == id)
164 }
165}
166
167// ============================================================================
168// Service Configuration
169// ============================================================================
170
171/// Configuration for a service within a namespace.
172///
173/// Services represent individual microservices, API groups, or logical
174/// components that need their own listener, routes, and backend configuration.
175///
176/// # Example KDL
177///
178/// ```kdl
179/// service "payments" {
180/// listener {
181/// address "0.0.0.0:8443"
182/// protocol "https"
183/// tls { ... }
184/// }
185///
186/// upstreams {
187/// upstream "payments-backend" { ... }
188/// }
189///
190/// routes {
191/// route "checkout" {
192/// upstream "payments-backend" // Service-local
193/// }
194/// }
195/// }
196/// ```
197#[derive(Debug, Clone, Serialize, Deserialize, Default)]
198pub struct ServiceConfig {
199 /// Unique service identifier within the namespace.
200 ///
201 /// Must not contain the `:` character as it's reserved for
202 /// qualified ID syntax (e.g., `namespace:service:resource`).
203 pub id: String,
204
205 /// Service-specific listener.
206 ///
207 /// Unlike namespace listeners (which are collections), a service
208 /// typically has a single dedicated listener for its traffic.
209 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub listener: Option<ListenerConfig>,
211
212 /// Service-local upstreams.
213 ///
214 /// These upstreams are only visible within this service.
215 /// They shadow any namespace or global upstreams with the same name.
216 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
217 pub upstreams: HashMap<String, UpstreamConfig>,
218
219 /// Service-local routes.
220 ///
221 /// Routes can reference service-local, namespace, or global upstreams.
222 /// Resolution follows "most specific wins".
223 #[serde(default, skip_serializing_if = "Vec::is_empty")]
224 pub routes: Vec<RouteConfig>,
225
226 /// Service-local agents.
227 ///
228 /// These agents are only visible within this service.
229 #[serde(default, skip_serializing_if = "Vec::is_empty")]
230 pub agents: Vec<AgentConfig>,
231
232 /// Service-local filters.
233 ///
234 /// These filters can only be referenced by routes within this service.
235 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
236 pub filters: HashMap<String, FilterConfig>,
237
238 /// Service-level limits.
239 ///
240 /// These limits override both global and namespace limits.
241 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub limits: Option<Limits>,
243}
244
245impl ServiceConfig {
246 /// Create a new service with the given ID.
247 pub fn new(id: impl Into<String>) -> Self {
248 Self {
249 id: id.into(),
250 ..Default::default()
251 }
252 }
253
254 /// Returns true if this service contains no resources.
255 pub fn is_empty(&self) -> bool {
256 self.listener.is_none()
257 && self.upstreams.is_empty()
258 && self.routes.is_empty()
259 && self.agents.is_empty()
260 && self.filters.is_empty()
261 && self.limits.is_none()
262 }
263}
264
265// ============================================================================
266// Export Configuration
267// ============================================================================
268
269/// Configuration for exporting namespace resources globally.
270///
271/// Exported resources become visible from any scope in the configuration,
272/// allowing namespaces to share common resources (like shared auth upstreams
273/// or common filters) with other parts of the system.
274///
275/// # Example KDL
276///
277/// ```kdl
278/// exports {
279/// upstreams "shared-auth" "shared-cache"
280/// agents "global-waf"
281/// filters "rate-limiter"
282/// }
283/// ```
284#[derive(Debug, Clone, Serialize, Deserialize, Default)]
285pub struct ExportConfig {
286 /// Upstream IDs to export globally.
287 ///
288 /// These upstreams become visible from any scope and can be
289 /// referenced without namespace qualification.
290 #[serde(default, skip_serializing_if = "Vec::is_empty")]
291 pub upstreams: Vec<String>,
292
293 /// Agent IDs to export globally.
294 #[serde(default, skip_serializing_if = "Vec::is_empty")]
295 pub agents: Vec<String>,
296
297 /// Filter IDs to export globally.
298 #[serde(default, skip_serializing_if = "Vec::is_empty")]
299 pub filters: Vec<String>,
300}
301
302impl ExportConfig {
303 /// Returns true if no resources are exported.
304 pub fn is_empty(&self) -> bool {
305 self.upstreams.is_empty() && self.agents.is_empty() && self.filters.is_empty()
306 }
307
308 /// Returns the total number of exported resources.
309 pub fn len(&self) -> usize {
310 self.upstreams.len() + self.agents.len() + self.filters.len()
311 }
312}
313
314// ============================================================================
315// Tests
316// ============================================================================
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321 use crate::{ConnectionPoolConfig, HttpVersionConfig, UpstreamTarget, UpstreamTimeouts};
322 use grapsus_common::types::LoadBalancingAlgorithm;
323
324 /// Create a minimal upstream config for testing
325 fn test_upstream() -> UpstreamConfig {
326 UpstreamConfig {
327 id: "test-upstream".to_string(),
328 targets: vec![UpstreamTarget {
329 address: "127.0.0.1:8080".to_string(),
330 weight: 1,
331 max_requests: None,
332 metadata: HashMap::new(),
333 }],
334 load_balancing: LoadBalancingAlgorithm::RoundRobin,
335 sticky_session: None,
336 health_check: None,
337 connection_pool: ConnectionPoolConfig::default(),
338 timeouts: UpstreamTimeouts::default(),
339 tls: None,
340 http_version: HttpVersionConfig::default(),
341 }
342 }
343
344 #[test]
345 fn test_namespace_new() {
346 let ns = NamespaceConfig::new("api");
347 assert_eq!(ns.id, "api");
348 assert!(ns.is_empty());
349 }
350
351 #[test]
352 fn test_namespace_is_empty() {
353 let mut ns = NamespaceConfig::new("api");
354 assert!(ns.is_empty());
355
356 ns.upstreams.insert("backend".to_string(), test_upstream());
357 assert!(!ns.is_empty());
358 }
359
360 #[test]
361 fn test_service_new() {
362 let svc = ServiceConfig::new("payments");
363 assert_eq!(svc.id, "payments");
364 assert!(svc.is_empty());
365 }
366
367 #[test]
368 fn test_service_is_empty() {
369 let mut svc = ServiceConfig::new("payments");
370 assert!(svc.is_empty());
371
372 svc.upstreams.insert("backend".to_string(), test_upstream());
373 assert!(!svc.is_empty());
374 }
375
376 #[test]
377 fn test_export_config_is_empty() {
378 let exports = ExportConfig::default();
379 assert!(exports.is_empty());
380 assert_eq!(exports.len(), 0);
381 }
382
383 #[test]
384 fn test_export_config_len() {
385 let exports = ExportConfig {
386 upstreams: vec!["a".to_string(), "b".to_string()],
387 agents: vec!["c".to_string()],
388 filters: vec![],
389 };
390 assert!(!exports.is_empty());
391 assert_eq!(exports.len(), 3);
392 }
393
394 #[test]
395 fn test_namespace_get_service() {
396 let mut ns = NamespaceConfig::new("api");
397 ns.services.push(ServiceConfig::new("payments"));
398 ns.services.push(ServiceConfig::new("users"));
399
400 assert!(ns.get_service("payments").is_some());
401 assert!(ns.get_service("users").is_some());
402 assert!(ns.get_service("orders").is_none());
403 }
404
405 #[test]
406 fn test_namespace_serialization() {
407 let ns = NamespaceConfig {
408 id: "api".to_string(),
409 limits: None,
410 listeners: vec![],
411 upstreams: HashMap::new(),
412 routes: vec![],
413 agents: vec![],
414 filters: HashMap::new(),
415 services: vec![ServiceConfig::new("payments")],
416 exports: ExportConfig::default(),
417 };
418
419 let json = serde_json::to_string(&ns).unwrap();
420 let parsed: NamespaceConfig = serde_json::from_str(&json).unwrap();
421 assert_eq!(parsed.id, "api");
422 assert_eq!(parsed.services.len(), 1);
423 assert_eq!(parsed.services[0].id, "payments");
424 }
425}