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