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 =
126 path_pattern.map(|p| Self::matches_path(&op.path, p)).unwrap_or(true); if method_matches && path_matches {
129 should_exclude = true;
130 break;
131 }
132 }
133
134 if !should_exclude {
135 result.push(op);
136 }
137 }
138
139 Ok(result)
140 }
141
142 fn extract_operations_from_path(
144 &self,
145 path: &str,
146 path_item: &PathItem,
147 operations: &mut Vec<ApiOperation>,
148 ) {
149 let method_ops = vec![
150 ("get", &path_item.get),
151 ("post", &path_item.post),
152 ("put", &path_item.put),
153 ("delete", &path_item.delete),
154 ("patch", &path_item.patch),
155 ("head", &path_item.head),
156 ("options", &path_item.options),
157 ];
158
159 for (method, op_ref) in method_ops {
160 if let Some(op) = op_ref {
161 operations.push(ApiOperation {
162 method: method.to_string(),
163 path: path.to_string(),
164 operation: op.clone(),
165 operation_id: op.operation_id.clone(),
166 });
167 }
168 }
169 }
170
171 fn matches_path(path: &str, pattern: &str) -> bool {
173 if pattern == path {
174 return true;
175 }
176
177 if let Some(prefix) = pattern.strip_suffix('*') {
179 return path.starts_with(prefix);
180 }
181
182 false
183 }
184
185 pub fn get_base_url(&self) -> Option<String> {
187 self.spec.servers.first().map(|server| server.url.clone())
188 }
189
190 pub fn get_info(&self) -> (String, String) {
192 (self.spec.info.title.clone(), self.spec.info.version.clone())
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use openapiv3::Operation;
200
201 fn create_test_operation(method: &str, path: &str, operation_id: Option<&str>) -> ApiOperation {
203 ApiOperation {
204 method: method.to_string(),
205 path: path.to_string(),
206 operation: Operation::default(),
207 operation_id: operation_id.map(|s| s.to_string()),
208 }
209 }
210
211 #[test]
212 fn test_matches_path() {
213 assert!(SpecParser::matches_path("/users", "/users"));
214 assert!(SpecParser::matches_path("/users/123", "/users/*"));
215 assert!(!SpecParser::matches_path("/posts", "/users"));
216 }
217
218 #[test]
219 fn test_exclude_operations_by_method() {
220 let operations = vec![
222 create_test_operation("get", "/users", Some("getUsers")),
223 create_test_operation("post", "/users", Some("createUser")),
224 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
225 create_test_operation("get", "/posts", Some("getPosts")),
226 create_test_operation("delete", "/posts/{id}", Some("deletePost")),
227 ];
228
229 let spec = openapiv3::OpenAPI::default();
231 let parser = SpecParser { spec };
232 let result = parser.exclude_operations(operations.clone(), "DELETE").unwrap();
233
234 assert_eq!(result.len(), 3);
235 assert!(result.iter().all(|op| op.method.to_uppercase() != "DELETE"));
236 }
237
238 #[test]
239 fn test_exclude_operations_by_method_and_path() {
240 let operations = vec![
241 create_test_operation("get", "/users", Some("getUsers")),
242 create_test_operation("post", "/users", Some("createUser")),
243 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
244 create_test_operation("get", "/posts", Some("getPosts")),
245 create_test_operation("delete", "/posts/{id}", Some("deletePost")),
246 ];
247
248 let spec = openapiv3::OpenAPI::default();
249 let parser = SpecParser { spec };
250
251 let result = parser.exclude_operations(operations.clone(), "DELETE /users/{id}").unwrap();
253
254 assert_eq!(result.len(), 4);
255 assert!(result.iter().any(|op| op.operation_id == Some("deletePost".to_string())));
257 assert!(!result.iter().any(|op| op.operation_id == Some("deleteUser".to_string())));
259 }
260
261 #[test]
262 fn test_exclude_operations_multiple_methods() {
263 let operations = vec![
264 create_test_operation("get", "/users", Some("getUsers")),
265 create_test_operation("post", "/users", Some("createUser")),
266 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
267 create_test_operation("put", "/users/{id}", Some("updateUser")),
268 ];
269
270 let spec = openapiv3::OpenAPI::default();
271 let parser = SpecParser { spec };
272
273 let result = parser.exclude_operations(operations.clone(), "DELETE,POST").unwrap();
275
276 assert_eq!(result.len(), 2);
277 assert!(result.iter().all(|op| op.method.to_uppercase() != "DELETE"));
278 assert!(result.iter().all(|op| op.method.to_uppercase() != "POST"));
279 }
280
281 #[test]
282 fn test_exclude_operations_empty_string() {
283 let operations = vec![
284 create_test_operation("get", "/users", Some("getUsers")),
285 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
286 ];
287
288 let spec = openapiv3::OpenAPI::default();
289 let parser = SpecParser { spec };
290
291 let result = parser.exclude_operations(operations.clone(), "").unwrap();
293
294 assert_eq!(result.len(), 2);
295 }
296
297 #[test]
298 fn test_exclude_operations_with_wildcard() {
299 let operations = vec![
300 create_test_operation("get", "/users", Some("getUsers")),
301 create_test_operation("delete", "/users/{id}", Some("deleteUser")),
302 create_test_operation("delete", "/posts/{id}", Some("deletePost")),
303 ];
304
305 let spec = openapiv3::OpenAPI::default();
306 let parser = SpecParser { spec };
307
308 let result = parser.exclude_operations(operations.clone(), "DELETE /users/*").unwrap();
310
311 assert_eq!(result.len(), 2);
312 assert!(result.iter().any(|op| op.operation_id == Some("deletePost".to_string())));
314 assert!(!result.iter().any(|op| op.operation_id == Some("deleteUser".to_string())));
316 }
317}