1use crate::error::{BenchError, Result};
4use mockforge_core::openapi::spec::OpenApiSpec;
5use openapiv3::{OpenAPI, Operation, 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 get_operations(&self) -> Vec<ApiOperation> {
45 let mut operations = Vec::new();
46
47 for (path, path_item) in &self.spec.paths.paths {
48 if let ReferenceOr::Item(item) = path_item {
49 self.extract_operations_from_path(path, item, &mut operations);
50 }
51 }
52
53 operations
54 }
55
56 pub fn filter_operations(&self, filter: &str) -> Result<Vec<ApiOperation>> {
58 let all_ops = self.get_operations();
59
60 if filter.is_empty() {
61 return Ok(all_ops);
62 }
63
64 let filters: Vec<&str> = filter.split(',').map(|s| s.trim()).collect();
65 let mut filtered = Vec::new();
66
67 for filter_str in filters {
68 let parts: Vec<&str> = filter_str.splitn(2, ' ').collect();
69 if parts.len() != 2 {
70 return Err(BenchError::Other(format!(
71 "Invalid operation filter format: '{}'. Expected 'METHOD /path'",
72 filter_str
73 )));
74 }
75
76 let method = parts[0].to_uppercase();
77 let path_pattern = parts[1];
78
79 for op in &all_ops {
80 if op.method.to_uppercase() == method && Self::matches_path(&op.path, path_pattern)
81 {
82 filtered.push(op.clone());
83 }
84 }
85 }
86
87 if filtered.is_empty() {
88 return Err(BenchError::OperationNotFound(filter.to_string()));
89 }
90
91 Ok(filtered)
92 }
93
94 pub fn exclude_operations(
99 &self,
100 operations: Vec<ApiOperation>,
101 exclude: &str,
102 ) -> Result<Vec<ApiOperation>> {
103 if exclude.is_empty() {
104 return Ok(operations);
105 }
106
107 let exclusions: Vec<&str> = exclude.split(',').map(|s| s.trim()).collect();
108 let mut result = Vec::new();
109
110 for op in operations {
111 let mut should_exclude = false;
112
113 for exclude_str in &exclusions {
114 let parts: Vec<&str> = exclude_str.splitn(2, ' ').collect();
116
117 let (method, path_pattern) = if parts.len() == 2 {
118 (parts[0].to_uppercase(), Some(parts[1]))
119 } else {
120 (parts[0].to_uppercase(), None)
122 };
123
124 let method_matches = op.method.to_uppercase() == method;
125 let path_matches = path_pattern
126 .map(|p| Self::matches_path(&op.path, p))
127 .unwrap_or(true); if method_matches && path_matches {
130 should_exclude = true;
131 break;
132 }
133 }
134
135 if !should_exclude {
136 result.push(op);
137 }
138 }
139
140 Ok(result)
141 }
142
143 fn extract_operations_from_path(
145 &self,
146 path: &str,
147 path_item: &PathItem,
148 operations: &mut Vec<ApiOperation>,
149 ) {
150 let method_ops = vec![
151 ("get", &path_item.get),
152 ("post", &path_item.post),
153 ("put", &path_item.put),
154 ("delete", &path_item.delete),
155 ("patch", &path_item.patch),
156 ("head", &path_item.head),
157 ("options", &path_item.options),
158 ];
159
160 for (method, op_ref) in method_ops {
161 if let Some(op) = op_ref {
162 operations.push(ApiOperation {
163 method: method.to_string(),
164 path: path.to_string(),
165 operation: op.clone(),
166 operation_id: op.operation_id.clone(),
167 });
168 }
169 }
170 }
171
172 fn matches_path(path: &str, pattern: &str) -> bool {
174 if pattern == path {
175 return true;
176 }
177
178 if let Some(prefix) = pattern.strip_suffix('*') {
180 return path.starts_with(prefix);
181 }
182
183 false
184 }
185
186 pub fn get_base_url(&self) -> Option<String> {
188 self.spec.servers.first().map(|server| server.url.clone())
189 }
190
191 pub fn get_info(&self) -> (String, String) {
193 (self.spec.info.title.clone(), self.spec.info.version.clone())
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use openapiv3::Operation;
201
202 fn create_test_operation(method: &str, path: &str, operation_id: Option<&str>) -> ApiOperation {
204 ApiOperation {
205 method: method.to_string(),
206 path: path.to_string(),
207 operation: Operation::default(),
208 operation_id: operation_id.map(|s| s.to_string()),
209 }
210 }
211
212 #[test]
213 fn test_matches_path() {
214 assert!(SpecParser::matches_path("/users", "/users"));
215 assert!(SpecParser::matches_path("/users/123", "/users/*"));
216 assert!(!SpecParser::matches_path("/posts", "/users"));
217 }
218
219 #[test]
220 fn test_exclude_operations_by_method() {
221 let operations = vec![
223 create_test_operation("get", "/users", Some("getUsers")),
224 create_test_operation("post", "/users", Some("createUser")),
225 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
226 create_test_operation("get", "/posts", Some("getPosts")),
227 create_test_operation("delete", "/posts/{id}", Some("deletePost")),
228 ];
229
230 let spec = openapiv3::OpenAPI::default();
232 let parser = SpecParser { spec };
233 let result = parser.exclude_operations(operations.clone(), "DELETE").unwrap();
234
235 assert_eq!(result.len(), 3);
236 assert!(result.iter().all(|op| op.method.to_uppercase() != "DELETE"));
237 }
238
239 #[test]
240 fn test_exclude_operations_by_method_and_path() {
241 let operations = vec![
242 create_test_operation("get", "/users", Some("getUsers")),
243 create_test_operation("post", "/users", Some("createUser")),
244 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
245 create_test_operation("get", "/posts", Some("getPosts")),
246 create_test_operation("delete", "/posts/{id}", Some("deletePost")),
247 ];
248
249 let spec = openapiv3::OpenAPI::default();
250 let parser = SpecParser { spec };
251
252 let result = parser.exclude_operations(operations.clone(), "DELETE /users/{id}").unwrap();
254
255 assert_eq!(result.len(), 4);
256 assert!(result.iter().any(|op| op.operation_id == Some("deletePost".to_string())));
258 assert!(!result.iter().any(|op| op.operation_id == Some("deleteUser".to_string())));
260 }
261
262 #[test]
263 fn test_exclude_operations_multiple_methods() {
264 let operations = vec![
265 create_test_operation("get", "/users", Some("getUsers")),
266 create_test_operation("post", "/users", Some("createUser")),
267 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
268 create_test_operation("put", "/users/{id}", Some("updateUser")),
269 ];
270
271 let spec = openapiv3::OpenAPI::default();
272 let parser = SpecParser { spec };
273
274 let result = parser.exclude_operations(operations.clone(), "DELETE,POST").unwrap();
276
277 assert_eq!(result.len(), 2);
278 assert!(result.iter().all(|op| op.method.to_uppercase() != "DELETE"));
279 assert!(result.iter().all(|op| op.method.to_uppercase() != "POST"));
280 }
281
282 #[test]
283 fn test_exclude_operations_empty_string() {
284 let operations = vec![
285 create_test_operation("get", "/users", Some("getUsers")),
286 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
287 ];
288
289 let spec = openapiv3::OpenAPI::default();
290 let parser = SpecParser { spec };
291
292 let result = parser.exclude_operations(operations.clone(), "").unwrap();
294
295 assert_eq!(result.len(), 2);
296 }
297
298 #[test]
299 fn test_exclude_operations_with_wildcard() {
300 let operations = vec![
301 create_test_operation("get", "/users", Some("getUsers")),
302 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
303 create_test_operation("delete", "/posts/{id}", Some("deletePost")),
304 ];
305
306 let spec = openapiv3::OpenAPI::default();
307 let parser = SpecParser { spec };
308
309 let result = parser.exclude_operations(operations.clone(), "DELETE /users/*").unwrap();
311
312 assert_eq!(result.len(), 2);
313 assert!(result.iter().any(|op| op.operation_id == Some("deletePost".to_string())));
315 assert!(!result.iter().any(|op| op.operation_id == Some("deleteUser".to_string())));
317 }
318}