mockforge_bench/
spec_parser.rs1use 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 fn extract_operations_from_path(
96 &self,
97 path: &str,
98 path_item: &PathItem,
99 operations: &mut Vec<ApiOperation>,
100 ) {
101 let method_ops = vec![
102 ("get", &path_item.get),
103 ("post", &path_item.post),
104 ("put", &path_item.put),
105 ("delete", &path_item.delete),
106 ("patch", &path_item.patch),
107 ("head", &path_item.head),
108 ("options", &path_item.options),
109 ];
110
111 for (method, op_ref) in method_ops {
112 if let Some(op) = op_ref {
113 operations.push(ApiOperation {
114 method: method.to_string(),
115 path: path.to_string(),
116 operation: op.clone(),
117 operation_id: op.operation_id.clone(),
118 });
119 }
120 }
121 }
122
123 fn matches_path(path: &str, pattern: &str) -> bool {
125 if pattern == path {
126 return true;
127 }
128
129 if let Some(prefix) = pattern.strip_suffix('*') {
131 return path.starts_with(prefix);
132 }
133
134 false
135 }
136
137 pub fn get_base_url(&self) -> Option<String> {
139 self.spec.servers.first().map(|server| server.url.clone())
140 }
141
142 pub fn get_info(&self) -> (String, String) {
144 (self.spec.info.title.clone(), self.spec.info.version.clone())
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn test_matches_path() {
154 assert!(SpecParser::matches_path("/users", "/users"));
155 assert!(SpecParser::matches_path("/users/123", "/users/*"));
156 assert!(!SpecParser::matches_path("/posts", "/users"));
157 }
158}