Skip to main content

mockforge_federation/
federation.rs

1//! Federation definition and management
2//!
3//! A federation is a collection of services that form a virtual system.
4
5use crate::service::{ServiceBoundary, ServiceRealityLevel};
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11/// Federation configuration
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct FederationConfig {
14    /// Federation name
15    pub name: String,
16    /// Federation description
17    #[serde(default)]
18    pub description: String,
19    /// Services in this federation
20    pub services: Vec<FederationService>,
21}
22
23/// Service definition in federation
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct FederationService {
26    /// Service name
27    pub name: String,
28    /// Workspace ID
29    pub workspace_id: String, // UUID as string for YAML
30    /// Base path
31    pub base_path: String,
32    /// Reality level
33    pub reality_level: String,
34    /// Service-specific config
35    #[serde(default)]
36    pub config: HashMap<String, serde_json::Value>,
37    /// Dependencies
38    #[serde(default)]
39    pub dependencies: Vec<String>,
40}
41
42/// Federation metadata (stored in database)
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Federation {
45    /// Federation ID
46    pub id: Uuid,
47    /// Federation name
48    pub name: String,
49    /// Federation description
50    pub description: String,
51    /// Organization ID
52    pub org_id: Uuid,
53    /// Service boundaries
54    pub services: Vec<ServiceBoundary>,
55    /// Created timestamp
56    pub created_at: DateTime<Utc>,
57    /// Updated timestamp
58    pub updated_at: DateTime<Utc>,
59}
60
61impl Federation {
62    /// Create a new federation from config
63    ///
64    /// # Errors
65    ///
66    /// Returns an error if any service has an invalid workspace ID or reality level.
67    pub fn from_config(org_id: Uuid, config: FederationConfig) -> Result<Self, String> {
68        let mut services = Vec::new();
69
70        for service_config in config.services {
71            let workspace_id = Uuid::parse_str(&service_config.workspace_id)
72                .map_err(|_| format!("Invalid workspace_id: {}", service_config.workspace_id))?;
73
74            let reality_level = ServiceRealityLevel::from_str(&service_config.reality_level)
75                .ok_or_else(|| {
76                    format!("Invalid reality_level: {}", service_config.reality_level)
77                })?;
78
79            let mut service = ServiceBoundary::new(
80                service_config.name.clone(),
81                workspace_id,
82                service_config.base_path.clone(),
83                reality_level,
84            );
85
86            service.config = service_config.config;
87            service.dependencies = service_config.dependencies;
88
89            services.push(service);
90        }
91
92        let now = Utc::now();
93
94        Ok(Self {
95            id: Uuid::new_v4(),
96            name: config.name,
97            description: config.description,
98            org_id,
99            services,
100            created_at: now,
101            updated_at: now,
102        })
103    }
104
105    /// Find service by path
106    #[must_use]
107    pub fn find_service_by_path(&self, path: &str) -> Option<&ServiceBoundary> {
108        // Find the longest matching base_path (most specific match)
109        self.services
110            .iter()
111            .filter(|s| s.matches_path(path))
112            .max_by_key(|s| s.base_path.len())
113    }
114
115    /// Get service by name
116    #[must_use]
117    pub fn get_service(&self, name: &str) -> Option<&ServiceBoundary> {
118        self.services.iter().find(|s| s.name == name)
119    }
120
121    /// Add a service to the federation
122    pub fn add_service(&mut self, service: ServiceBoundary) {
123        self.services.push(service);
124        self.updated_at = Utc::now();
125    }
126
127    /// Remove a service from the federation
128    pub fn remove_service(&mut self, name: &str) -> bool {
129        let len_before = self.services.len();
130        self.services.retain(|s| s.name != name);
131        let removed = self.services.len() < len_before;
132        if removed {
133            self.updated_at = Utc::now();
134        }
135        removed
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    fn create_test_federation() -> Federation {
144        Federation {
145            id: Uuid::new_v4(),
146            name: "test".to_string(),
147            description: "Test federation".to_string(),
148            org_id: Uuid::new_v4(),
149            services: vec![
150                ServiceBoundary::new(
151                    "auth".to_string(),
152                    Uuid::new_v4(),
153                    "/auth".to_string(),
154                    ServiceRealityLevel::Real,
155                ),
156                ServiceBoundary::new(
157                    "payments".to_string(),
158                    Uuid::new_v4(),
159                    "/payments".to_string(),
160                    ServiceRealityLevel::MockV3,
161                ),
162            ],
163            created_at: Utc::now(),
164            updated_at: Utc::now(),
165        }
166    }
167
168    #[test]
169    fn test_federation_from_config() {
170        let config = FederationConfig {
171            name: "test-federation".to_string(),
172            description: "Test".to_string(),
173            services: vec![FederationService {
174                name: "auth".to_string(),
175                workspace_id: Uuid::new_v4().to_string(),
176                base_path: "/auth".to_string(),
177                reality_level: "real".to_string(),
178                config: HashMap::new(),
179                dependencies: Vec::new(),
180            }],
181        };
182
183        let federation = Federation::from_config(Uuid::new_v4(), config).unwrap();
184        assert_eq!(federation.services.len(), 1);
185        assert_eq!(federation.services[0].name, "auth");
186    }
187
188    #[test]
189    fn test_federation_from_config_multiple_services() {
190        let config = FederationConfig {
191            name: "multi-service".to_string(),
192            description: "Multiple services".to_string(),
193            services: vec![
194                FederationService {
195                    name: "auth".to_string(),
196                    workspace_id: Uuid::new_v4().to_string(),
197                    base_path: "/auth".to_string(),
198                    reality_level: "real".to_string(),
199                    config: HashMap::new(),
200                    dependencies: Vec::new(),
201                },
202                FederationService {
203                    name: "payments".to_string(),
204                    workspace_id: Uuid::new_v4().to_string(),
205                    base_path: "/payments".to_string(),
206                    reality_level: "mock_v3".to_string(),
207                    config: HashMap::new(),
208                    dependencies: vec!["auth".to_string()],
209                },
210            ],
211        };
212
213        let federation = Federation::from_config(Uuid::new_v4(), config).unwrap();
214        assert_eq!(federation.services.len(), 2);
215        assert_eq!(federation.services[1].dependencies, vec!["auth".to_string()]);
216    }
217
218    #[test]
219    fn test_federation_from_config_invalid_workspace_id() {
220        let config = FederationConfig {
221            name: "test".to_string(),
222            description: String::new(),
223            services: vec![FederationService {
224                name: "auth".to_string(),
225                workspace_id: "invalid-uuid".to_string(),
226                base_path: "/auth".to_string(),
227                reality_level: "real".to_string(),
228                config: HashMap::new(),
229                dependencies: Vec::new(),
230            }],
231        };
232
233        let result = Federation::from_config(Uuid::new_v4(), config);
234        assert!(result.is_err());
235        assert!(result.unwrap_err().contains("Invalid workspace_id"));
236    }
237
238    #[test]
239    fn test_federation_from_config_invalid_reality_level() {
240        let config = FederationConfig {
241            name: "test".to_string(),
242            description: String::new(),
243            services: vec![FederationService {
244                name: "auth".to_string(),
245                workspace_id: Uuid::new_v4().to_string(),
246                base_path: "/auth".to_string(),
247                reality_level: "invalid_level".to_string(),
248                config: HashMap::new(),
249                dependencies: Vec::new(),
250            }],
251        };
252
253        let result = Federation::from_config(Uuid::new_v4(), config);
254        assert!(result.is_err());
255        assert!(result.unwrap_err().contains("Invalid reality_level"));
256    }
257
258    #[test]
259    fn test_find_service_by_path() {
260        let federation = create_test_federation();
261
262        assert!(federation.find_service_by_path("/auth").is_some());
263        assert!(federation.find_service_by_path("/payments").is_some());
264        assert!(federation.find_service_by_path("/unknown").is_none());
265    }
266
267    #[test]
268    fn test_find_service_by_path_nested() {
269        let federation = create_test_federation();
270
271        let service = federation.find_service_by_path("/auth/login").unwrap();
272        assert_eq!(service.name, "auth");
273
274        let service = federation.find_service_by_path("/payments/process").unwrap();
275        assert_eq!(service.name, "payments");
276    }
277
278    #[test]
279    fn test_find_service_by_path_longest_match() {
280        let mut federation = create_test_federation();
281        federation.services.push(ServiceBoundary::new(
282            "auth-admin".to_string(),
283            Uuid::new_v4(),
284            "/auth/admin".to_string(),
285            ServiceRealityLevel::MockV3,
286        ));
287
288        // Should match the longer /auth/admin path
289        let service = federation.find_service_by_path("/auth/admin/users").unwrap();
290        assert_eq!(service.name, "auth-admin");
291
292        // Should match /auth (shorter path)
293        let service = federation.find_service_by_path("/auth/login").unwrap();
294        assert_eq!(service.name, "auth");
295    }
296
297    #[test]
298    fn test_get_service() {
299        let federation = create_test_federation();
300
301        let service = federation.get_service("auth").unwrap();
302        assert_eq!(service.name, "auth");
303
304        let service = federation.get_service("payments").unwrap();
305        assert_eq!(service.name, "payments");
306
307        assert!(federation.get_service("nonexistent").is_none());
308    }
309
310    #[test]
311    fn test_add_service() {
312        let mut federation = create_test_federation();
313        let initial_count = federation.services.len();
314
315        federation.add_service(ServiceBoundary::new(
316            "inventory".to_string(),
317            Uuid::new_v4(),
318            "/inventory".to_string(),
319            ServiceRealityLevel::Blended,
320        ));
321
322        assert_eq!(federation.services.len(), initial_count + 1);
323        assert!(federation.get_service("inventory").is_some());
324    }
325
326    #[test]
327    fn test_add_service_updates_timestamp() {
328        let mut federation = create_test_federation();
329        let original_updated = federation.updated_at;
330
331        // Small delay to ensure timestamp changes
332        std::thread::sleep(std::time::Duration::from_millis(10));
333
334        federation.add_service(ServiceBoundary::new(
335            "new".to_string(),
336            Uuid::new_v4(),
337            "/new".to_string(),
338            ServiceRealityLevel::Real,
339        ));
340
341        assert!(federation.updated_at > original_updated);
342    }
343
344    #[test]
345    fn test_remove_service() {
346        let mut federation = create_test_federation();
347        let initial_count = federation.services.len();
348
349        assert!(federation.remove_service("auth"));
350        assert_eq!(federation.services.len(), initial_count - 1);
351        assert!(federation.get_service("auth").is_none());
352    }
353
354    #[test]
355    fn test_remove_service_not_found() {
356        let mut federation = create_test_federation();
357        let initial_count = federation.services.len();
358
359        assert!(!federation.remove_service("nonexistent"));
360        assert_eq!(federation.services.len(), initial_count);
361    }
362
363    #[test]
364    fn test_remove_service_updates_timestamp() {
365        let mut federation = create_test_federation();
366        let original_updated = federation.updated_at;
367
368        std::thread::sleep(std::time::Duration::from_millis(10));
369
370        federation.remove_service("auth");
371        assert!(federation.updated_at > original_updated);
372    }
373
374    // FederationConfig tests
375    #[test]
376    fn test_federation_config_serialize() {
377        let config = FederationConfig {
378            name: "test".to_string(),
379            description: "Test federation".to_string(),
380            services: vec![],
381        };
382
383        let json = serde_json::to_string(&config).unwrap();
384        assert!(json.contains("\"name\":\"test\""));
385        assert!(json.contains("\"description\":\"Test federation\""));
386    }
387
388    #[test]
389    fn test_federation_config_deserialize() {
390        let json = r#"{"name":"test","description":"","services":[]}"#;
391        let config: FederationConfig = serde_json::from_str(json).unwrap();
392        assert_eq!(config.name, "test");
393        assert!(config.services.is_empty());
394    }
395
396    // FederationService tests
397    #[test]
398    fn test_federation_service_debug() {
399        let service = FederationService {
400            name: "auth".to_string(),
401            workspace_id: Uuid::new_v4().to_string(),
402            base_path: "/auth".to_string(),
403            reality_level: "real".to_string(),
404            config: HashMap::new(),
405            dependencies: Vec::new(),
406        };
407
408        let debug = format!("{service:?}");
409        assert!(debug.contains("auth"));
410    }
411
412    // Federation serialization tests
413    #[test]
414    fn test_federation_serialize() {
415        let federation = create_test_federation();
416        let json = serde_json::to_string(&federation).unwrap();
417        assert!(json.contains("test"));
418        assert!(json.contains("auth"));
419        assert!(json.contains("payments"));
420    }
421
422    #[test]
423    fn test_federation_clone() {
424        let federation = create_test_federation();
425        let cloned = federation.clone();
426
427        assert_eq!(federation.id, cloned.id);
428        assert_eq!(federation.name, cloned.name);
429        assert_eq!(federation.services.len(), cloned.services.len());
430    }
431}