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 #[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 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 #[must_use]
122 pub fn find_service_by_path(&self, path: &str) -> Option<&ServiceBoundary> {
123 self.services
125 .iter()
126 .filter(|s| s.matches_path(path))
127 .max_by_key(|s| s.base_path.len())
128 }
129
130 #[must_use]
132 pub fn get_service(&self, name: &str) -> Option<&ServiceBoundary> {
133 self.services.iter().find(|s| s.name == name)
134 }
135
136 pub fn add_service(&mut self, service: ServiceBoundary) {
138 self.services.push(service);
139 self.updated_at = Utc::now();
140 }
141
142 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 let service = federation.find_service_by_path("/auth/admin/users").unwrap();
305 assert_eq!(service.name, "auth-admin");
306
307 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 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 #[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 #[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 #[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}