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 spec(&self) -> &OpenAPI {
50 &self.spec
51 }
52
53 pub fn get_operations(&self) -> Vec<ApiOperation> {
55 let mut operations = Vec::new();
56
57 for (path, path_item) in &self.spec.paths.paths {
58 if let ReferenceOr::Item(item) = path_item {
59 self.extract_operations_from_path(path, item, &mut operations);
60 }
61 }
62
63 operations
64 }
65
66 pub fn filter_operations(&self, filter: &str) -> Result<Vec<ApiOperation>> {
68 let all_ops = self.get_operations();
69
70 if filter.is_empty() {
71 return Ok(all_ops);
72 }
73
74 let filters: Vec<&str> = filter.split(',').map(|s| s.trim()).collect();
75 let mut filtered = Vec::new();
76
77 for filter_str in filters {
78 let parts: Vec<&str> = filter_str.splitn(2, ' ').collect();
79 if parts.len() != 2 {
80 return Err(BenchError::Other(format!(
81 "Invalid operation filter format: '{}'. Expected 'METHOD /path'",
82 filter_str
83 )));
84 }
85
86 let method = parts[0].to_uppercase();
87 let path_pattern = parts[1];
88
89 for op in &all_ops {
90 if op.method.to_uppercase() == method && Self::matches_path(&op.path, path_pattern)
91 {
92 filtered.push(op.clone());
93 }
94 }
95 }
96
97 if filtered.is_empty() {
98 return Err(BenchError::OperationNotFound(filter.to_string()));
99 }
100
101 Ok(filtered)
102 }
103
104 pub fn exclude_operations(
109 &self,
110 operations: Vec<ApiOperation>,
111 exclude: &str,
112 ) -> Result<Vec<ApiOperation>> {
113 if exclude.is_empty() {
114 return Ok(operations);
115 }
116
117 let exclusions: Vec<&str> = exclude.split(',').map(|s| s.trim()).collect();
118 let mut result = Vec::new();
119
120 for op in operations {
121 let mut should_exclude = false;
122
123 for exclude_str in &exclusions {
124 let parts: Vec<&str> = exclude_str.splitn(2, ' ').collect();
126
127 let (method, path_pattern) = if parts.len() == 2 {
128 (parts[0].to_uppercase(), Some(parts[1]))
129 } else {
130 (parts[0].to_uppercase(), None)
132 };
133
134 let method_matches = op.method.to_uppercase() == method;
135 let path_matches =
136 path_pattern.map(|p| Self::matches_path(&op.path, p)).unwrap_or(true); if method_matches && path_matches {
139 should_exclude = true;
140 break;
141 }
142 }
143
144 if !should_exclude {
145 result.push(op);
146 }
147 }
148
149 Ok(result)
150 }
151
152 fn extract_operations_from_path(
154 &self,
155 path: &str,
156 path_item: &PathItem,
157 operations: &mut Vec<ApiOperation>,
158 ) {
159 let path_level_params: Vec<ReferenceOr<Parameter>> = path_item
161 .parameters
162 .iter()
163 .filter_map(|p| self.resolve_parameter(p).map(ReferenceOr::Item))
164 .collect();
165
166 let method_ops = vec![
167 ("get", &path_item.get),
168 ("post", &path_item.post),
169 ("put", &path_item.put),
170 ("delete", &path_item.delete),
171 ("patch", &path_item.patch),
172 ("head", &path_item.head),
173 ("options", &path_item.options),
174 ];
175
176 for (method, op_ref) in method_ops {
177 if let Some(op) = op_ref {
178 let mut resolved_op = op.clone();
180 let mut resolved_params: Vec<ReferenceOr<Parameter>> = Vec::new();
181
182 resolved_params.extend(path_level_params.clone());
184
185 for param_ref in &op.parameters {
187 if let Some(resolved) = self.resolve_parameter(param_ref) {
188 resolved_params.push(ReferenceOr::Item(resolved));
189 }
190 }
191
192 resolved_op.parameters = resolved_params;
193
194 operations.push(ApiOperation {
195 method: method.to_string(),
196 path: path.to_string(),
197 operation: resolved_op,
198 operation_id: op.operation_id.clone(),
199 });
200 }
201 }
202 }
203
204 fn resolve_parameter(&self, param_ref: &ReferenceOr<Parameter>) -> Option<Parameter> {
206 match param_ref {
207 ReferenceOr::Item(param) => Some(param.clone()),
208 ReferenceOr::Reference { reference } => {
209 let parts: Vec<&str> = reference.split('/').collect();
211 if parts.len() >= 4 && parts[1] == "components" && parts[2] == "parameters" {
212 let param_name = parts[3];
213 if let Some(components) = &self.spec.components {
214 if let ReferenceOr::Item(param) = components.parameters.get(param_name)? {
215 return Some(param.clone());
216 }
217 }
218 }
219 None
220 }
221 }
222 }
223
224 fn matches_path(path: &str, pattern: &str) -> bool {
226 if pattern == path {
227 return true;
228 }
229
230 if let Some(prefix) = pattern.strip_suffix('*') {
232 return path.starts_with(prefix);
233 }
234
235 false
236 }
237
238 pub fn get_base_url(&self) -> Option<String> {
240 self.spec.servers.first().map(|server| server.url.clone())
241 }
242
243 pub fn get_base_path(&self) -> Option<String> {
253 let server_url = self.spec.servers.first().map(|s| &s.url)?;
254
255 if server_url.starts_with('/') {
257 let path = server_url.trim_end_matches('/');
258 if !path.is_empty() && path != "/" {
259 return Some(path.to_string());
260 }
261 return None;
262 }
263
264 if let Ok(parsed) = url::Url::parse(server_url) {
266 let path = parsed.path().trim_end_matches('/');
267 if !path.is_empty() && path != "/" {
268 return Some(path.to_string());
269 }
270 }
271
272 None
273 }
274
275 pub fn get_info(&self) -> (String, String) {
277 (self.spec.info.title.clone(), self.spec.info.version.clone())
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use openapiv3::Operation;
285
286 fn create_test_operation(method: &str, path: &str, operation_id: Option<&str>) -> ApiOperation {
288 ApiOperation {
289 method: method.to_string(),
290 path: path.to_string(),
291 operation: Operation::default(),
292 operation_id: operation_id.map(|s| s.to_string()),
293 }
294 }
295
296 #[test]
297 fn test_matches_path() {
298 assert!(SpecParser::matches_path("/users", "/users"));
299 assert!(SpecParser::matches_path("/users/123", "/users/*"));
300 assert!(!SpecParser::matches_path("/posts", "/users"));
301 }
302
303 #[test]
304 fn test_exclude_operations_by_method() {
305 let operations = vec![
307 create_test_operation("get", "/users", Some("getUsers")),
308 create_test_operation("post", "/users", Some("createUser")),
309 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
310 create_test_operation("get", "/posts", Some("getPosts")),
311 create_test_operation("delete", "/posts/{id}", Some("deletePost")),
312 ];
313
314 let spec = openapiv3::OpenAPI::default();
316 let parser = SpecParser { spec };
317 let result = parser.exclude_operations(operations.clone(), "DELETE").unwrap();
318
319 assert_eq!(result.len(), 3);
320 assert!(result.iter().all(|op| op.method.to_uppercase() != "DELETE"));
321 }
322
323 #[test]
324 fn test_exclude_operations_by_method_and_path() {
325 let operations = vec![
326 create_test_operation("get", "/users", Some("getUsers")),
327 create_test_operation("post", "/users", Some("createUser")),
328 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
329 create_test_operation("get", "/posts", Some("getPosts")),
330 create_test_operation("delete", "/posts/{id}", Some("deletePost")),
331 ];
332
333 let spec = openapiv3::OpenAPI::default();
334 let parser = SpecParser { spec };
335
336 let result = parser.exclude_operations(operations.clone(), "DELETE /users/{id}").unwrap();
338
339 assert_eq!(result.len(), 4);
340 assert!(result.iter().any(|op| op.operation_id == Some("deletePost".to_string())));
342 assert!(!result.iter().any(|op| op.operation_id == Some("deleteUser".to_string())));
344 }
345
346 #[test]
347 fn test_exclude_operations_multiple_methods() {
348 let operations = vec![
349 create_test_operation("get", "/users", Some("getUsers")),
350 create_test_operation("post", "/users", Some("createUser")),
351 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
352 create_test_operation("put", "/users/{id}", Some("updateUser")),
353 ];
354
355 let spec = openapiv3::OpenAPI::default();
356 let parser = SpecParser { spec };
357
358 let result = parser.exclude_operations(operations.clone(), "DELETE,POST").unwrap();
360
361 assert_eq!(result.len(), 2);
362 assert!(result.iter().all(|op| op.method.to_uppercase() != "DELETE"));
363 assert!(result.iter().all(|op| op.method.to_uppercase() != "POST"));
364 }
365
366 #[test]
367 fn test_exclude_operations_empty_string() {
368 let operations = vec![
369 create_test_operation("get", "/users", Some("getUsers")),
370 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
371 ];
372
373 let spec = openapiv3::OpenAPI::default();
374 let parser = SpecParser { spec };
375
376 let result = parser.exclude_operations(operations.clone(), "").unwrap();
378
379 assert_eq!(result.len(), 2);
380 }
381
382 #[test]
383 fn test_exclude_operations_with_wildcard() {
384 let operations = vec![
385 create_test_operation("get", "/users", Some("getUsers")),
386 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
387 create_test_operation("delete", "/posts/{id}", Some("deletePost")),
388 ];
389
390 let spec = openapiv3::OpenAPI::default();
391 let parser = SpecParser { spec };
392
393 let result = parser.exclude_operations(operations.clone(), "DELETE /users/*").unwrap();
395
396 assert_eq!(result.len(), 2);
397 assert!(result.iter().any(|op| op.operation_id == Some("deletePost".to_string())));
399 assert!(!result.iter().any(|op| op.operation_id == Some("deleteUser".to_string())));
401 }
402
403 #[test]
404 fn test_get_base_path_with_full_url() {
405 let mut spec = openapiv3::OpenAPI::default();
406 spec.servers.push(openapiv3::Server {
407 url: "https://api.example.com/api/v1".to_string(),
408 description: None,
409 variables: Default::default(),
410 extensions: Default::default(),
411 });
412
413 let parser = SpecParser { spec };
414 let base_path = parser.get_base_path();
415
416 assert_eq!(base_path, Some("/api/v1".to_string()));
417 }
418
419 #[test]
420 fn test_get_base_path_with_relative_path() {
421 let mut spec = openapiv3::OpenAPI::default();
422 spec.servers.push(openapiv3::Server {
423 url: "/api/v2".to_string(),
424 description: None,
425 variables: Default::default(),
426 extensions: Default::default(),
427 });
428
429 let parser = SpecParser { spec };
430 let base_path = parser.get_base_path();
431
432 assert_eq!(base_path, Some("/api/v2".to_string()));
433 }
434
435 #[test]
436 fn test_get_base_path_no_path_in_url() {
437 let mut spec = openapiv3::OpenAPI::default();
438 spec.servers.push(openapiv3::Server {
439 url: "https://api.example.com".to_string(),
440 description: None,
441 variables: Default::default(),
442 extensions: Default::default(),
443 });
444
445 let parser = SpecParser { spec };
446 let base_path = parser.get_base_path();
447
448 assert_eq!(base_path, None);
449 }
450
451 #[test]
452 fn test_get_base_path_no_servers() {
453 let spec = openapiv3::OpenAPI::default();
454 let parser = SpecParser { spec };
455 let base_path = parser.get_base_path();
456
457 assert_eq!(base_path, None);
458 }
459
460 #[test]
461 fn test_get_base_path_trailing_slash_removed() {
462 let mut spec = openapiv3::OpenAPI::default();
463 spec.servers.push(openapiv3::Server {
464 url: "https://api.example.com/api/v1/".to_string(),
465 description: None,
466 variables: Default::default(),
467 extensions: Default::default(),
468 });
469
470 let parser = SpecParser { spec };
471 let base_path = parser.get_base_path();
472
473 assert_eq!(base_path, Some("/api/v1".to_string()));
475 }
476}