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 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#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct ServiceBoundary {
60 pub name: String,
62 pub workspace_id: Uuid,
64 pub base_path: String,
66 pub reality_level: ServiceRealityLevel,
68 #[serde(default)]
70 pub config: HashMap<String, serde_json::Value>,
71 #[serde(default)]
73 pub dependencies: Vec<String>,
74}
75
76impl ServiceBoundary {
77 #[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 #[must_use]
97 pub fn matches_path(&self, path: &str) -> bool {
98 path.starts_with(&self.base_path)
99 }
100
101 #[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 #[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 #[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 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}