1use crate::service::{ServiceBoundary, ServiceRealityLevel};
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct FederationConfig {
14 pub name: String,
16 #[serde(default)]
18 pub description: String,
19 pub services: Vec<FederationService>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct FederationService {
26 pub name: String,
28 pub workspace_id: String, pub base_path: String,
32 pub reality_level: String,
34 #[serde(default)]
36 pub config: HashMap<String, serde_json::Value>,
37 #[serde(default)]
39 pub dependencies: Vec<String>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Federation {
45 pub id: Uuid,
47 pub name: String,
49 pub description: String,
51 pub org_id: Uuid,
53 pub services: Vec<ServiceBoundary>,
55 pub created_at: DateTime<Utc>,
57 pub updated_at: DateTime<Utc>,
59}
60
61impl Federation {
62 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 #[must_use]
107 pub fn find_service_by_path(&self, path: &str) -> Option<&ServiceBoundary> {
108 self.services
110 .iter()
111 .filter(|s| s.matches_path(path))
112 .max_by_key(|s| s.base_path.len())
113 }
114
115 #[must_use]
117 pub fn get_service(&self, name: &str) -> Option<&ServiceBoundary> {
118 self.services.iter().find(|s| s.name == name)
119 }
120
121 pub fn add_service(&mut self, service: ServiceBoundary) {
123 self.services.push(service);
124 self.updated_at = Utc::now();
125 }
126
127 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 let service = federation.find_service_by_path("/auth/admin/users").unwrap();
290 assert_eq!(service.name, "auth-admin");
291
292 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 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 #[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 #[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 #[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}