Skip to main content

mockforge_federation/
service.rs

1//! Service definitions and boundaries
2//!
3//! Services represent individual microservices in a federated system.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use uuid::Uuid;
8
9/// Service reality level
10///
11/// Controls how a service behaves in the federation:
12/// - `Real`: Use real upstream (no mocking)
13/// - `MockV3`: Use mock with reality level 3
14/// - `Blended`: Mix of mock and real data
15/// - `ChaosDriven`: Chaos testing mode
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum ServiceRealityLevel {
19    /// Use real upstream (no mocking)
20    Real,
21    /// Use mock with reality level 3
22    MockV3,
23    /// Mix of mock and real data
24    Blended,
25    /// Chaos testing mode
26    ChaosDriven,
27}
28
29impl ServiceRealityLevel {
30    /// Convert to string
31    #[must_use]
32    pub const fn as_str(&self) -> &'static str {
33        match self {
34            Self::Real => "real",
35            Self::MockV3 => "mock_v3",
36            Self::Blended => "blended",
37            Self::ChaosDriven => "chaos_driven",
38        }
39    }
40
41    /// Parse from string
42    #[must_use]
43    #[allow(clippy::should_implement_trait)]
44    pub fn from_str(s: &str) -> Option<Self> {
45        match s.to_lowercase().as_str() {
46            "real" => Some(Self::Real),
47            "mock_v3" | "mockv3" => Some(Self::MockV3),
48            "blended" => Some(Self::Blended),
49            "chaos_driven" | "chaosdriven" => Some(Self::ChaosDriven),
50            _ => None,
51        }
52    }
53}
54
55/// Service boundary definition
56///
57/// Defines a service in the federation, including its workspace mapping,
58/// base path, and reality level configuration.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ServiceBoundary {
61    /// Service name
62    pub name: String,
63    /// Workspace ID this service maps to
64    pub workspace_id: Uuid,
65    /// Base path for this service (e.g., "/auth", "/payments")
66    pub base_path: String,
67    /// Reality level for this service
68    pub reality_level: ServiceRealityLevel,
69    /// Service-specific configuration
70    #[serde(default)]
71    pub config: HashMap<String, serde_json::Value>,
72    /// Inter-service dependencies
73    #[serde(default)]
74    pub dependencies: Vec<String>,
75}
76
77impl ServiceBoundary {
78    /// Create a new service boundary
79    #[must_use]
80    pub fn new(
81        name: String,
82        workspace_id: Uuid,
83        base_path: String,
84        reality_level: ServiceRealityLevel,
85    ) -> Self {
86        Self {
87            name,
88            workspace_id,
89            base_path,
90            reality_level,
91            config: HashMap::new(),
92            dependencies: Vec::new(),
93        }
94    }
95
96    /// Check if a path matches this service
97    #[must_use]
98    pub fn matches_path(&self, path: &str) -> bool {
99        path.starts_with(&self.base_path)
100    }
101
102    /// Extract service-specific path from full path
103    #[must_use]
104    pub fn extract_service_path(&self, full_path: &str) -> Option<String> {
105        if full_path.starts_with(&self.base_path) {
106            let service_path = full_path.strip_prefix(&self.base_path)?;
107            Some(if service_path.is_empty() {
108                "/".to_string()
109            } else if !service_path.starts_with('/') {
110                format!("/{service_path}")
111            } else {
112                service_path.to_string()
113            })
114        } else {
115            None
116        }
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    // ServiceRealityLevel tests
125    #[test]
126    fn test_service_reality_level() {
127        assert_eq!(ServiceRealityLevel::Real.as_str(), "real");
128        assert_eq!(ServiceRealityLevel::from_str("real"), Some(ServiceRealityLevel::Real));
129        assert_eq!(ServiceRealityLevel::from_str("MOCK_V3"), Some(ServiceRealityLevel::MockV3));
130    }
131
132    #[test]
133    fn test_service_reality_level_as_str() {
134        assert_eq!(ServiceRealityLevel::Real.as_str(), "real");
135        assert_eq!(ServiceRealityLevel::MockV3.as_str(), "mock_v3");
136        assert_eq!(ServiceRealityLevel::Blended.as_str(), "blended");
137        assert_eq!(ServiceRealityLevel::ChaosDriven.as_str(), "chaos_driven");
138    }
139
140    #[test]
141    fn test_service_reality_level_from_str_all_variants() {
142        assert_eq!(ServiceRealityLevel::from_str("real"), Some(ServiceRealityLevel::Real));
143        assert_eq!(ServiceRealityLevel::from_str("mock_v3"), Some(ServiceRealityLevel::MockV3));
144        assert_eq!(ServiceRealityLevel::from_str("mockv3"), Some(ServiceRealityLevel::MockV3));
145        assert_eq!(ServiceRealityLevel::from_str("blended"), Some(ServiceRealityLevel::Blended));
146        assert_eq!(
147            ServiceRealityLevel::from_str("chaos_driven"),
148            Some(ServiceRealityLevel::ChaosDriven)
149        );
150        assert_eq!(
151            ServiceRealityLevel::from_str("chaosdriven"),
152            Some(ServiceRealityLevel::ChaosDriven)
153        );
154    }
155
156    #[test]
157    fn test_service_reality_level_from_str_case_insensitive() {
158        assert_eq!(ServiceRealityLevel::from_str("REAL"), Some(ServiceRealityLevel::Real));
159        assert_eq!(ServiceRealityLevel::from_str("Real"), Some(ServiceRealityLevel::Real));
160        assert_eq!(ServiceRealityLevel::from_str("BLENDED"), Some(ServiceRealityLevel::Blended));
161    }
162
163    #[test]
164    fn test_service_reality_level_from_str_invalid() {
165        assert_eq!(ServiceRealityLevel::from_str("invalid"), None);
166        assert_eq!(ServiceRealityLevel::from_str(""), None);
167        assert_eq!(ServiceRealityLevel::from_str("unknown"), None);
168    }
169
170    #[test]
171    fn test_service_reality_level_clone() {
172        let level = ServiceRealityLevel::MockV3;
173        let cloned = level;
174        assert_eq!(level, cloned);
175    }
176
177    #[test]
178    fn test_service_reality_level_debug() {
179        let level = ServiceRealityLevel::ChaosDriven;
180        let debug = format!("{level:?}");
181        assert!(debug.contains("ChaosDriven"));
182    }
183
184    #[test]
185    fn test_service_reality_level_serialize() {
186        let level = ServiceRealityLevel::MockV3;
187        let json = serde_json::to_string(&level).unwrap();
188        assert!(json.contains("mock_v3"));
189    }
190
191    #[test]
192    fn test_service_reality_level_deserialize() {
193        let json = "\"blended\"";
194        let level: ServiceRealityLevel = serde_json::from_str(json).unwrap();
195        assert_eq!(level, ServiceRealityLevel::Blended);
196    }
197
198    // ServiceBoundary tests
199    #[test]
200    fn test_service_boundary_path_matching() {
201        let service = ServiceBoundary::new(
202            "auth".to_string(),
203            Uuid::new_v4(),
204            "/auth".to_string(),
205            ServiceRealityLevel::Real,
206        );
207
208        assert!(service.matches_path("/auth"));
209        assert!(service.matches_path("/auth/login"));
210        assert!(service.matches_path("/auth/users/123"));
211        assert!(!service.matches_path("/payments"));
212    }
213
214    #[test]
215    fn test_extract_service_path() {
216        let service = ServiceBoundary::new(
217            "auth".to_string(),
218            Uuid::new_v4(),
219            "/auth".to_string(),
220            ServiceRealityLevel::Real,
221        );
222
223        assert_eq!(service.extract_service_path("/auth"), Some("/".to_string()));
224        assert_eq!(service.extract_service_path("/auth/login"), Some("/login".to_string()));
225        assert_eq!(service.extract_service_path("/auth/users/123"), Some("/users/123".to_string()));
226        assert_eq!(service.extract_service_path("/payments"), None);
227    }
228
229    #[test]
230    fn test_service_boundary_new() {
231        let workspace_id = Uuid::new_v4();
232        let service = ServiceBoundary::new(
233            "payments".to_string(),
234            workspace_id,
235            "/payments".to_string(),
236            ServiceRealityLevel::MockV3,
237        );
238
239        assert_eq!(service.name, "payments");
240        assert_eq!(service.workspace_id, workspace_id);
241        assert_eq!(service.base_path, "/payments");
242        assert_eq!(service.reality_level, ServiceRealityLevel::MockV3);
243        assert!(service.config.is_empty());
244        assert!(service.dependencies.is_empty());
245    }
246
247    #[test]
248    fn test_service_boundary_with_config() {
249        let mut config = HashMap::new();
250        config.insert("timeout".to_string(), serde_json::json!(5000));
251
252        let service = ServiceBoundary {
253            name: "api".to_string(),
254            workspace_id: Uuid::new_v4(),
255            base_path: "/api".to_string(),
256            reality_level: ServiceRealityLevel::Blended,
257            config,
258            dependencies: vec!["auth".to_string()],
259        };
260
261        assert_eq!(service.config.len(), 1);
262        assert_eq!(service.config["timeout"], serde_json::json!(5000));
263        assert_eq!(service.dependencies, vec!["auth".to_string()]);
264    }
265
266    #[test]
267    fn test_service_boundary_clone() {
268        let service = ServiceBoundary::new(
269            "test".to_string(),
270            Uuid::new_v4(),
271            "/test".to_string(),
272            ServiceRealityLevel::Real,
273        );
274
275        let cloned = service.clone();
276        assert_eq!(service.name, cloned.name);
277        assert_eq!(service.workspace_id, cloned.workspace_id);
278        assert_eq!(service.base_path, cloned.base_path);
279    }
280
281    #[test]
282    fn test_service_boundary_debug() {
283        let service = ServiceBoundary::new(
284            "test".to_string(),
285            Uuid::new_v4(),
286            "/test".to_string(),
287            ServiceRealityLevel::Real,
288        );
289
290        let debug = format!("{service:?}");
291        assert!(debug.contains("test"));
292        assert!(debug.contains("ServiceBoundary"));
293    }
294
295    #[test]
296    fn test_service_boundary_serialize() {
297        let service = ServiceBoundary::new(
298            "api".to_string(),
299            Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(),
300            "/api".to_string(),
301            ServiceRealityLevel::MockV3,
302        );
303
304        let json = serde_json::to_string(&service).unwrap();
305        assert!(json.contains("\"name\":\"api\""));
306        assert!(json.contains("/api"));
307        assert!(json.contains("mock_v3"));
308    }
309
310    #[test]
311    fn test_extract_service_path_without_leading_slash() {
312        let service = ServiceBoundary::new(
313            "api".to_string(),
314            Uuid::new_v4(),
315            "/api".to_string(),
316            ServiceRealityLevel::Real,
317        );
318
319        // Path that doesn't start with / after stripping base_path
320        assert_eq!(service.extract_service_path("/api"), Some("/".to_string()));
321    }
322
323    #[test]
324    fn test_matches_path_empty_base() {
325        let service = ServiceBoundary::new(
326            "root".to_string(),
327            Uuid::new_v4(),
328            String::new(),
329            ServiceRealityLevel::Real,
330        );
331
332        assert!(service.matches_path(""));
333        assert!(service.matches_path("/anything"));
334    }
335
336    #[test]
337    fn test_matches_path_with_nested_paths() {
338        let service = ServiceBoundary::new(
339            "nested".to_string(),
340            Uuid::new_v4(),
341            "/api/v1".to_string(),
342            ServiceRealityLevel::Real,
343        );
344
345        assert!(service.matches_path("/api/v1"));
346        assert!(service.matches_path("/api/v1/users"));
347        assert!(!service.matches_path("/api/v2"));
348    }
349}