1use crate::error::OpenApiError;
2use crate::server::ToolMetadata;
3use crate::tool_generator::ToolGenerator;
4use serde_json::Value;
5
6#[derive(Debug, Clone)]
7pub struct OpenApiSpec {
8 pub raw: Value,
9 pub info: OpenApiInfo,
10 pub operations: Vec<OpenApiOperation>,
11}
12
13#[derive(Debug, Clone)]
14pub struct OpenApiInfo {
15 pub title: String,
16 pub version: String,
17 pub description: Option<String>,
18}
19
20#[derive(Debug, Clone)]
21pub struct OpenApiOperation {
22 pub operation_id: String,
23 pub method: String,
24 pub path: String,
25 pub summary: Option<String>,
26 pub description: Option<String>,
27 pub parameters: Vec<OpenApiParameter>,
28}
29
30#[derive(Debug, Clone, PartialEq)]
31pub enum ParameterLocation {
32 Query,
33 Path,
34 Header,
35 Cookie,
36 FormData,
37}
38
39impl std::fmt::Display for ParameterLocation {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 match self {
42 ParameterLocation::Query => write!(f, "query"),
43 ParameterLocation::Path => write!(f, "path"),
44 ParameterLocation::Header => write!(f, "header"),
45 ParameterLocation::Cookie => write!(f, "cookie"),
46 ParameterLocation::FormData => write!(f, "formData"),
47 }
48 }
49}
50
51impl std::convert::TryFrom<&str> for ParameterLocation {
52 type Error = crate::error::OpenApiError;
53
54 fn try_from(s: &str) -> Result<Self, Self::Error> {
55 match s {
56 "query" => Ok(ParameterLocation::Query),
57 "path" => Ok(ParameterLocation::Path),
58 "header" => Ok(ParameterLocation::Header),
59 "cookie" => Ok(ParameterLocation::Cookie),
60 "formData" => Ok(ParameterLocation::FormData),
61 _ => Err(crate::error::OpenApiError::InvalidParameterLocation(
62 s.to_string(),
63 )),
64 }
65 }
66}
67
68#[derive(Debug, Clone)]
69pub struct OpenApiParameter {
70 pub name: String,
71 pub location: ParameterLocation,
72 pub required: bool,
73 pub param_type: String,
74 pub description: Option<String>,
75 pub schema: Value,
76}
77
78impl OpenApiSpec {
79 pub async fn from_url(url: &str) -> Result<Self, OpenApiError> {
81 let client = reqwest::Client::new();
82 let response = client.get(url).send().await?;
83 let text = response.text().await?;
84 let json_value: Value = serde_json::from_str(&text)?;
85
86 Self::from_value(json_value)
87 }
88
89 pub async fn from_file(path: &str) -> Result<Self, OpenApiError> {
91 let content = tokio::fs::read_to_string(path).await?;
92 let json_value: Value = serde_json::from_str(&content)?;
93
94 Self::from_value(json_value)
95 }
96
97 pub fn from_value(json_value: Value) -> Result<Self, OpenApiError> {
99 let info_obj = json_value
101 .get("info")
102 .ok_or_else(|| OpenApiError::Spec("Missing 'info' section".to_string()))?;
103
104 let info = OpenApiInfo {
105 title: info_obj
106 .get("title")
107 .and_then(|v| v.as_str())
108 .unwrap_or("Unknown API")
109 .to_string(),
110 version: info_obj
111 .get("version")
112 .and_then(|v| v.as_str())
113 .unwrap_or("1.0.0")
114 .to_string(),
115 description: info_obj
116 .get("description")
117 .and_then(|v| v.as_str())
118 .map(|s| s.to_string()),
119 };
120
121 let mut operations = Vec::new();
123
124 let paths = json_value
125 .get("paths")
126 .ok_or_else(|| OpenApiError::Spec("Missing 'paths' section".to_string()))?
127 .as_object()
128 .ok_or_else(|| OpenApiError::Spec("'paths' is not an object".to_string()))?;
129
130 for (path, path_obj) in paths {
131 let path_obj = path_obj
132 .as_object()
133 .ok_or_else(|| OpenApiError::Spec(format!("Path '{path}' is not an object")))?;
134
135 for (method, operation_obj) in path_obj {
136 if ![
138 "get", "post", "put", "delete", "patch", "head", "options", "trace",
139 ]
140 .contains(&method.as_str())
141 {
142 continue;
143 }
144
145 let operation =
146 Self::parse_operation(method.clone(), path.clone(), operation_obj.clone())?;
147 operations.push(operation);
148 }
149 }
150
151 Ok(OpenApiSpec {
152 raw: json_value,
153 info,
154 operations,
155 })
156 }
157
158 fn parse_operation(
160 method: String,
161 path: String,
162 operation_value: Value,
163 ) -> Result<OpenApiOperation, OpenApiError> {
164 let operation_id = operation_value
165 .get("operationId")
166 .and_then(|v| v.as_str())
167 .unwrap_or(&format!(
168 "{}_{}",
169 method,
170 path.replace('/', "_").replace(['{', '}'], "")
171 ))
172 .to_string();
173
174 let summary = operation_value
175 .get("summary")
176 .and_then(|v| v.as_str())
177 .map(|s| s.to_string());
178
179 let description = operation_value
180 .get("description")
181 .and_then(|v| v.as_str())
182 .map(|s| s.to_string());
183
184 let mut parameters = Vec::new();
185
186 if let Some(params_array) = operation_value.get("parameters").and_then(|v| v.as_array()) {
187 for param in params_array {
188 if let Ok(parameter) = Self::parse_parameter(param.clone()) {
189 parameters.push(parameter);
190 }
191 }
192 }
193
194 Ok(OpenApiOperation {
195 operation_id,
196 method,
197 path,
198 summary,
199 description,
200 parameters,
201 })
202 }
203
204 fn parse_parameter(param_value: Value) -> Result<OpenApiParameter, OpenApiError> {
206 let name = param_value
207 .get("name")
208 .and_then(|v| v.as_str())
209 .ok_or_else(|| OpenApiError::Spec("Parameter missing 'name'".to_string()))?
210 .to_string();
211
212 let location_str = param_value
213 .get("in")
214 .and_then(|v| v.as_str())
215 .ok_or_else(|| OpenApiError::Spec("Parameter missing 'in'".to_string()))?;
216 let location = ParameterLocation::try_from(location_str)?;
217
218 let required = param_value
219 .get("required")
220 .and_then(|v| v.as_bool())
221 .unwrap_or(false);
222
223 let description = param_value
224 .get("description")
225 .and_then(|v| v.as_str())
226 .map(|s| s.to_string());
227
228 let schema = param_value
229 .get("schema")
230 .cloned()
231 .unwrap_or_else(|| serde_json::json!({"type": "string"}));
232
233 let param_type = schema
234 .get("type")
235 .and_then(|v| v.as_str())
236 .unwrap_or("string")
237 .to_string();
238
239 Ok(OpenApiParameter {
240 name,
241 location,
242 required,
243 param_type,
244 description,
245 schema,
246 })
247 }
248
249 pub fn to_tool_metadata(&self) -> Result<Vec<ToolMetadata>, OpenApiError> {
251 let mut tools = Vec::new();
252
253 for operation in &self.operations {
254 let tool_metadata = ToolGenerator::generate_tool_metadata(operation)?;
255 tools.push(tool_metadata);
256 }
257
258 Ok(tools)
259 }
260
261 pub fn get_operation(&self, operation_id: &str) -> Option<&OpenApiOperation> {
263 self.operations
264 .iter()
265 .find(|op| op.operation_id == operation_id)
266 }
267
268 pub fn get_operation_ids(&self) -> Vec<String> {
270 self.operations
271 .iter()
272 .map(|op| op.operation_id.clone())
273 .collect()
274 }
275}