1use crate::error::{BenchError, Result};
4use mockforge_core::openapi::spec::OpenApiSpec;
5use openapiv3::{OpenAPI, Operation, Parameter, PathItem, ReferenceOr};
6use std::path::Path;
7
8#[derive(Debug, Clone)]
10pub struct ApiOperation {
11 pub method: String,
12 pub path: String,
13 pub operation: Operation,
14 pub operation_id: Option<String>,
15}
16
17impl ApiOperation {
18 pub fn display_name(&self) -> String {
20 self.operation_id
21 .clone()
22 .unwrap_or_else(|| format!("{} {}", self.method.to_uppercase(), self.path))
23 }
24}
25
26pub struct SpecParser {
28 spec: OpenAPI,
29}
30
31impl SpecParser {
32 pub async fn from_file(path: &Path) -> Result<Self> {
34 let spec = OpenApiSpec::from_file(path)
35 .await
36 .map_err(|e| BenchError::SpecParseError(e.to_string()))?;
37
38 Ok(Self {
39 spec: spec.spec.clone(),
40 })
41 }
42
43 pub fn from_spec(spec: OpenApiSpec) -> Self {
45 Self { spec: spec.spec }
46 }
47
48 pub fn get_operations(&self) -> Vec<ApiOperation> {
50 let mut operations = Vec::new();
51
52 for (path, path_item) in &self.spec.paths.paths {
53 if let ReferenceOr::Item(item) = path_item {
54 self.extract_operations_from_path(path, item, &mut operations);
55 }
56 }
57
58 operations
59 }
60
61 pub fn filter_operations(&self, filter: &str) -> Result<Vec<ApiOperation>> {
63 let all_ops = self.get_operations();
64
65 if filter.is_empty() {
66 return Ok(all_ops);
67 }
68
69 let filters: Vec<&str> = filter.split(',').map(|s| s.trim()).collect();
70 let mut filtered = Vec::new();
71
72 for filter_str in filters {
73 let parts: Vec<&str> = filter_str.splitn(2, ' ').collect();
74 if parts.len() != 2 {
75 return Err(BenchError::Other(format!(
76 "Invalid operation filter format: '{}'. Expected 'METHOD /path'",
77 filter_str
78 )));
79 }
80
81 let method = parts[0].to_uppercase();
82 let path_pattern = parts[1];
83
84 for op in &all_ops {
85 if op.method.to_uppercase() == method && Self::matches_path(&op.path, path_pattern)
86 {
87 filtered.push(op.clone());
88 }
89 }
90 }
91
92 if filtered.is_empty() {
93 return Err(BenchError::OperationNotFound(filter.to_string()));
94 }
95
96 Ok(filtered)
97 }
98
99 pub fn exclude_operations(
104 &self,
105 operations: Vec<ApiOperation>,
106 exclude: &str,
107 ) -> Result<Vec<ApiOperation>> {
108 if exclude.is_empty() {
109 return Ok(operations);
110 }
111
112 let exclusions: Vec<&str> = exclude.split(',').map(|s| s.trim()).collect();
113 let mut result = Vec::new();
114
115 for op in operations {
116 let mut should_exclude = false;
117
118 for exclude_str in &exclusions {
119 let parts: Vec<&str> = exclude_str.splitn(2, ' ').collect();
121
122 let (method, path_pattern) = if parts.len() == 2 {
123 (parts[0].to_uppercase(), Some(parts[1]))
124 } else {
125 (parts[0].to_uppercase(), None)
127 };
128
129 let method_matches = op.method.to_uppercase() == method;
130 let path_matches =
131 path_pattern.map(|p| Self::matches_path(&op.path, p)).unwrap_or(true); if method_matches && path_matches {
134 should_exclude = true;
135 break;
136 }
137 }
138
139 if !should_exclude {
140 result.push(op);
141 }
142 }
143
144 Ok(result)
145 }
146
147 fn extract_operations_from_path(
149 &self,
150 path: &str,
151 path_item: &PathItem,
152 operations: &mut Vec<ApiOperation>,
153 ) {
154 let path_level_params: Vec<ReferenceOr<Parameter>> = path_item
156 .parameters
157 .iter()
158 .filter_map(|p| self.resolve_parameter(p).map(ReferenceOr::Item))
159 .collect();
160
161 let method_ops = vec![
162 ("get", &path_item.get),
163 ("post", &path_item.post),
164 ("put", &path_item.put),
165 ("delete", &path_item.delete),
166 ("patch", &path_item.patch),
167 ("head", &path_item.head),
168 ("options", &path_item.options),
169 ];
170
171 for (method, op_ref) in method_ops {
172 if let Some(op) = op_ref {
173 let mut resolved_op = op.clone();
175 let mut resolved_params: Vec<ReferenceOr<Parameter>> = Vec::new();
176
177 resolved_params.extend(path_level_params.clone());
179
180 for param_ref in &op.parameters {
182 if let Some(resolved) = self.resolve_parameter(param_ref) {
183 resolved_params.push(ReferenceOr::Item(resolved));
184 }
185 }
186
187 resolved_op.parameters = resolved_params;
188
189 operations.push(ApiOperation {
190 method: method.to_string(),
191 path: path.to_string(),
192 operation: resolved_op,
193 operation_id: op.operation_id.clone(),
194 });
195 }
196 }
197 }
198
199 fn resolve_parameter(&self, param_ref: &ReferenceOr<Parameter>) -> Option<Parameter> {
201 match param_ref {
202 ReferenceOr::Item(param) => Some(param.clone()),
203 ReferenceOr::Reference { reference } => {
204 let parts: Vec<&str> = reference.split('/').collect();
206 if parts.len() >= 4 && parts[1] == "components" && parts[2] == "parameters" {
207 let param_name = parts[3];
208 if let Some(components) = &self.spec.components {
209 if let ReferenceOr::Item(param) = components.parameters.get(param_name)? {
210 return Some(param.clone());
211 }
212 }
213 }
214 None
215 }
216 }
217 }
218
219 fn matches_path(path: &str, pattern: &str) -> bool {
221 if pattern == path {
222 return true;
223 }
224
225 if let Some(prefix) = pattern.strip_suffix('*') {
227 return path.starts_with(prefix);
228 }
229
230 false
231 }
232
233 pub fn get_base_url(&self) -> Option<String> {
235 self.spec.servers.first().map(|server| server.url.clone())
236 }
237
238 pub fn get_base_path(&self) -> Option<String> {
248 let server_url = self.spec.servers.first().map(|s| &s.url)?;
249
250 if server_url.starts_with('/') {
252 let path = server_url.trim_end_matches('/');
253 if !path.is_empty() && path != "/" {
254 return Some(path.to_string());
255 }
256 return None;
257 }
258
259 if let Ok(parsed) = url::Url::parse(server_url) {
261 let path = parsed.path().trim_end_matches('/');
262 if !path.is_empty() && path != "/" {
263 return Some(path.to_string());
264 }
265 }
266
267 None
268 }
269
270 pub fn get_info(&self) -> (String, String) {
272 (self.spec.info.title.clone(), self.spec.info.version.clone())
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use openapiv3::Operation;
280
281 fn create_test_operation(method: &str, path: &str, operation_id: Option<&str>) -> ApiOperation {
283 ApiOperation {
284 method: method.to_string(),
285 path: path.to_string(),
286 operation: Operation::default(),
287 operation_id: operation_id.map(|s| s.to_string()),
288 }
289 }
290
291 #[test]
292 fn test_matches_path() {
293 assert!(SpecParser::matches_path("/users", "/users"));
294 assert!(SpecParser::matches_path("/users/123", "/users/*"));
295 assert!(!SpecParser::matches_path("/posts", "/users"));
296 }
297
298 #[test]
299 fn test_exclude_operations_by_method() {
300 let operations = vec![
302 create_test_operation("get", "/users", Some("getUsers")),
303 create_test_operation("post", "/users", Some("createUser")),
304 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
305 create_test_operation("get", "/posts", Some("getPosts")),
306 create_test_operation("delete", "/posts/{id}", Some("deletePost")),
307 ];
308
309 let spec = openapiv3::OpenAPI::default();
311 let parser = SpecParser { spec };
312 let result = parser.exclude_operations(operations.clone(), "DELETE").unwrap();
313
314 assert_eq!(result.len(), 3);
315 assert!(result.iter().all(|op| op.method.to_uppercase() != "DELETE"));
316 }
317
318 #[test]
319 fn test_exclude_operations_by_method_and_path() {
320 let operations = vec![
321 create_test_operation("get", "/users", Some("getUsers")),
322 create_test_operation("post", "/users", Some("createUser")),
323 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
324 create_test_operation("get", "/posts", Some("getPosts")),
325 create_test_operation("delete", "/posts/{id}", Some("deletePost")),
326 ];
327
328 let spec = openapiv3::OpenAPI::default();
329 let parser = SpecParser { spec };
330
331 let result = parser.exclude_operations(operations.clone(), "DELETE /users/{id}").unwrap();
333
334 assert_eq!(result.len(), 4);
335 assert!(result.iter().any(|op| op.operation_id == Some("deletePost".to_string())));
337 assert!(!result.iter().any(|op| op.operation_id == Some("deleteUser".to_string())));
339 }
340
341 #[test]
342 fn test_exclude_operations_multiple_methods() {
343 let operations = vec![
344 create_test_operation("get", "/users", Some("getUsers")),
345 create_test_operation("post", "/users", Some("createUser")),
346 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
347 create_test_operation("put", "/users/{id}", Some("updateUser")),
348 ];
349
350 let spec = openapiv3::OpenAPI::default();
351 let parser = SpecParser { spec };
352
353 let result = parser.exclude_operations(operations.clone(), "DELETE,POST").unwrap();
355
356 assert_eq!(result.len(), 2);
357 assert!(result.iter().all(|op| op.method.to_uppercase() != "DELETE"));
358 assert!(result.iter().all(|op| op.method.to_uppercase() != "POST"));
359 }
360
361 #[test]
362 fn test_exclude_operations_empty_string() {
363 let operations = vec![
364 create_test_operation("get", "/users", Some("getUsers")),
365 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
366 ];
367
368 let spec = openapiv3::OpenAPI::default();
369 let parser = SpecParser { spec };
370
371 let result = parser.exclude_operations(operations.clone(), "").unwrap();
373
374 assert_eq!(result.len(), 2);
375 }
376
377 #[test]
378 fn test_exclude_operations_with_wildcard() {
379 let operations = vec![
380 create_test_operation("get", "/users", Some("getUsers")),
381 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
382 create_test_operation("delete", "/posts/{id}", Some("deletePost")),
383 ];
384
385 let spec = openapiv3::OpenAPI::default();
386 let parser = SpecParser { spec };
387
388 let result = parser.exclude_operations(operations.clone(), "DELETE /users/*").unwrap();
390
391 assert_eq!(result.len(), 2);
392 assert!(result.iter().any(|op| op.operation_id == Some("deletePost".to_string())));
394 assert!(!result.iter().any(|op| op.operation_id == Some("deleteUser".to_string())));
396 }
397
398 #[test]
399 fn test_get_base_path_with_full_url() {
400 let mut spec = openapiv3::OpenAPI::default();
401 spec.servers.push(openapiv3::Server {
402 url: "https://api.example.com/api/v1".to_string(),
403 description: None,
404 variables: Default::default(),
405 extensions: Default::default(),
406 });
407
408 let parser = SpecParser { spec };
409 let base_path = parser.get_base_path();
410
411 assert_eq!(base_path, Some("/api/v1".to_string()));
412 }
413
414 #[test]
415 fn test_get_base_path_with_relative_path() {
416 let mut spec = openapiv3::OpenAPI::default();
417 spec.servers.push(openapiv3::Server {
418 url: "/api/v2".to_string(),
419 description: None,
420 variables: Default::default(),
421 extensions: Default::default(),
422 });
423
424 let parser = SpecParser { spec };
425 let base_path = parser.get_base_path();
426
427 assert_eq!(base_path, Some("/api/v2".to_string()));
428 }
429
430 #[test]
431 fn test_get_base_path_no_path_in_url() {
432 let mut spec = openapiv3::OpenAPI::default();
433 spec.servers.push(openapiv3::Server {
434 url: "https://api.example.com".to_string(),
435 description: None,
436 variables: Default::default(),
437 extensions: Default::default(),
438 });
439
440 let parser = SpecParser { spec };
441 let base_path = parser.get_base_path();
442
443 assert_eq!(base_path, None);
444 }
445
446 #[test]
447 fn test_get_base_path_no_servers() {
448 let spec = openapiv3::OpenAPI::default();
449 let parser = SpecParser { spec };
450 let base_path = parser.get_base_path();
451
452 assert_eq!(base_path, None);
453 }
454
455 #[test]
456 fn test_get_base_path_trailing_slash_removed() {
457 let mut spec = openapiv3::OpenAPI::default();
458 spec.servers.push(openapiv3::Server {
459 url: "https://api.example.com/api/v1/".to_string(),
460 description: None,
461 variables: Default::default(),
462 extensions: Default::default(),
463 });
464
465 let parser = SpecParser { spec };
466 let base_path = parser.get_base_path();
467
468 assert_eq!(base_path, Some("/api/v1".to_string()));
470 }
471}