Skip to main content

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}