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