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