1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use uuid::Uuid;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum ServiceRealityLevel {
19 Real,
21 MockV3,
23 Blended,
25 ChaosDriven,
27}
28
29impl ServiceRealityLevel {
30 #[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 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ServiceBoundary {
61 pub name: String,
63 pub workspace_id: Uuid,
65 pub base_path: String,
67 pub reality_level: ServiceRealityLevel,
69 #[serde(default)]
71 pub config: HashMap<String, serde_json::Value>,
72 #[serde(default)]
74 pub dependencies: Vec<String>,
75}
76
77impl ServiceBoundary {
78 #[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 #[must_use]
98 pub fn matches_path(&self, path: &str) -> bool {
99 path.starts_with(&self.base_path)
100 }
101
102 #[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 #[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 #[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 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}