1use std::collections::HashMap;
4use std::fs::File;
5use std::path::Path;
6
7use openapiv3::{OpenAPI, ReferenceOr, Schema, StatusCode};
8
9use crate::{Error, Result};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum HttpMethod {
14 Get,
15 Post,
16 Put,
17 Delete,
18 Patch,
19 Head,
20 Options,
21}
22
23impl std::fmt::Display for HttpMethod {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 match self {
26 HttpMethod::Get => write!(f, "GET"),
27 HttpMethod::Post => write!(f, "POST"),
28 HttpMethod::Put => write!(f, "PUT"),
29 HttpMethod::Delete => write!(f, "DELETE"),
30 HttpMethod::Patch => write!(f, "PATCH"),
31 HttpMethod::Head => write!(f, "HEAD"),
32 HttpMethod::Options => write!(f, "OPTIONS"),
33 }
34 }
35}
36
37impl HttpMethod {
38 pub fn as_str(&self) -> &'static str {
39 match self {
40 HttpMethod::Get => "get",
41 HttpMethod::Post => "post",
42 HttpMethod::Put => "put",
43 HttpMethod::Delete => "delete",
44 HttpMethod::Patch => "patch",
45 HttpMethod::Head => "head",
46 HttpMethod::Options => "options",
47 }
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum ParamLocation {
54 Path,
55 Query,
56 Header,
57 Cookie,
58}
59
60#[derive(Debug, Clone)]
62pub struct OperationParam {
63 pub name: String,
64 pub location: ParamLocation,
65 pub required: bool,
66 pub schema: Option<ReferenceOr<Schema>>,
67 pub description: Option<String>,
68}
69
70#[derive(Debug, Clone)]
72pub struct OperationResponse {
73 pub status_code: ResponseStatus,
74 pub description: String,
75 pub schema: Option<ReferenceOr<Schema>>,
76 pub content_type: Option<String>,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Hash)]
81pub enum ResponseStatus {
82 Code(u16),
83 Default,
84}
85
86impl ResponseStatus {
87 pub fn is_success(&self) -> bool {
88 match self {
89 ResponseStatus::Code(code) => (200..300).contains(code),
90 ResponseStatus::Default => false,
91 }
92 }
93
94 pub fn is_error(&self) -> bool {
95 match self {
96 ResponseStatus::Code(code) => *code >= 400,
97 ResponseStatus::Default => true,
98 }
99 }
100}
101
102#[derive(Debug, Clone)]
104pub struct Operation {
105 pub operation_id: Option<String>,
106 pub method: HttpMethod,
107 pub path: String,
108 pub summary: Option<String>,
109 pub description: Option<String>,
110 pub parameters: Vec<OperationParam>,
111 pub request_body: Option<RequestBody>,
112 pub responses: Vec<OperationResponse>,
113 pub tags: Vec<String>,
114}
115
116#[derive(Debug, Clone)]
118pub struct RequestBody {
119 pub required: bool,
120 pub description: Option<String>,
121 pub content_type: String,
122 pub schema: Option<ReferenceOr<Schema>>,
123}
124
125pub struct ParsedSpec {
127 pub info: SpecInfo,
128 operations: Vec<Operation>,
129 operation_map: HashMap<(HttpMethod, String), usize>,
130 pub components: Option<openapiv3::Components>,
131}
132
133#[derive(Debug, Clone)]
135pub struct SpecInfo {
136 pub title: String,
137 pub version: String,
138 pub description: Option<String>,
139}
140
141impl ParsedSpec {
142 pub fn from_openapi(spec: OpenAPI) -> Result<Self> {
144 let info = SpecInfo {
145 title: spec.info.title.clone(),
146 version: spec.info.version.clone(),
147 description: spec.info.description.clone(),
148 };
149
150 let mut operations = Vec::new();
151 let mut operation_map = HashMap::new();
152
153 for (path, path_item) in &spec.paths.paths {
154 let item = match path_item {
155 ReferenceOr::Reference { .. } => {
156 return Err(Error::Unsupported(
157 "external path references not supported".to_string(),
158 ));
159 }
160 ReferenceOr::Item(item) => item,
161 };
162
163 let method_ops = [
165 (HttpMethod::Get, &item.get),
166 (HttpMethod::Post, &item.post),
167 (HttpMethod::Put, &item.put),
168 (HttpMethod::Delete, &item.delete),
169 (HttpMethod::Patch, &item.patch),
170 (HttpMethod::Head, &item.head),
171 (HttpMethod::Options, &item.options),
172 ];
173
174 for (method, op_opt) in method_ops {
175 if let Some(op) = op_opt {
176 let operation = parse_operation(method, path, op, &item.parameters, &spec)?;
177 let idx = operations.len();
178 operation_map.insert((method, path.clone()), idx);
179 operations.push(operation);
180 }
181 }
182 }
183
184 Ok(Self {
185 info,
186 operations,
187 operation_map,
188 components: spec.components,
189 })
190 }
191
192 pub fn get_operation(&self, method: HttpMethod, path: &str) -> Option<&Operation> {
194 self.operation_map
195 .get(&(method, path.to_string()))
196 .map(|&idx| &self.operations[idx])
197 }
198
199 pub fn operations(&self) -> impl Iterator<Item = &Operation> {
201 self.operations.iter()
202 }
203}
204
205pub fn load_spec(path: &Path) -> Result<OpenAPI> {
207 let file =
208 File::open(path).map_err(|e| Error::ParseError(format!("failed to open file: {}", e)))?;
209
210 if let Ok(spec) = serde_json::from_reader::<_, OpenAPI>(&file) {
212 return Ok(spec);
213 }
214
215 let file =
216 File::open(path).map_err(|e| Error::ParseError(format!("failed to open file: {}", e)))?;
217
218 yaml_serde::from_reader(file)
219 .map_err(|e| Error::ParseError(format!("failed to parse spec: {}", e)))
220}
221
222fn parse_operation(
223 method: HttpMethod,
224 path: &str,
225 op: &openapiv3::Operation,
226 path_params: &[ReferenceOr<openapiv3::Parameter>],
227 spec: &OpenAPI,
228) -> Result<Operation> {
229 let mut parameters = Vec::new();
230
231 for param_ref in path_params {
233 if let Some(param) = resolve_parameter(param_ref, spec)? {
234 parameters.push(param);
235 }
236 }
237
238 for param_ref in &op.parameters {
240 if let Some(param) = resolve_parameter(param_ref, spec)? {
241 parameters.retain(|p| !(p.name == param.name && p.location == param.location));
243 parameters.push(param);
244 }
245 }
246
247 let request_body = if let Some(body_ref) = &op.request_body {
249 parse_request_body(body_ref, spec)?
250 } else {
251 None
252 };
253
254 let mut responses = Vec::new();
256
257 if let Some(default) = &op.responses.default
258 && let Some(resp) = parse_response(ResponseStatus::Default, default, spec)?
259 {
260 responses.push(resp);
261 }
262
263 for (code, resp_ref) in &op.responses.responses {
264 let status = match code {
265 StatusCode::Code(c) => ResponseStatus::Code(*c),
266 StatusCode::Range(_) => continue, };
268 if let Some(resp) = parse_response(status, resp_ref, spec)? {
269 responses.push(resp);
270 }
271 }
272
273 Ok(Operation {
274 operation_id: op.operation_id.clone(),
275 method,
276 path: path.to_string(),
277 summary: op.summary.clone(),
278 description: op.description.clone(),
279 parameters,
280 request_body,
281 responses,
282 tags: op.tags.clone(),
283 })
284}
285
286fn resolve_parameter(
287 param_ref: &ReferenceOr<openapiv3::Parameter>,
288 spec: &OpenAPI,
289) -> Result<Option<OperationParam>> {
290 let param = match param_ref {
291 ReferenceOr::Reference { reference } => {
292 let name = reference
294 .strip_prefix("#/components/parameters/")
295 .ok_or_else(|| {
296 Error::ParseError(format!("invalid parameter reference: {}", reference))
297 })?;
298 spec.components
299 .as_ref()
300 .and_then(|c| c.parameters.get(name))
301 .and_then(|p| match p {
302 ReferenceOr::Item(item) => Some(item),
303 _ => None,
304 })
305 .ok_or_else(|| Error::ParseError(format!("parameter not found: {}", name)))?
306 }
307 ReferenceOr::Item(item) => item,
308 };
309
310 let (location, data) = match param {
311 openapiv3::Parameter::Path { parameter_data, .. } => (ParamLocation::Path, parameter_data),
312 openapiv3::Parameter::Query { parameter_data, .. } => {
313 (ParamLocation::Query, parameter_data)
314 }
315 openapiv3::Parameter::Header { parameter_data, .. } => {
316 (ParamLocation::Header, parameter_data)
317 }
318 openapiv3::Parameter::Cookie { parameter_data, .. } => {
319 (ParamLocation::Cookie, parameter_data)
320 }
321 };
322
323 let schema = match &data.format {
324 openapiv3::ParameterSchemaOrContent::Schema(s) => Some(s.clone()),
325 openapiv3::ParameterSchemaOrContent::Content(_) => None,
326 };
327
328 Ok(Some(OperationParam {
329 name: data.name.clone(),
330 location,
331 required: data.required,
332 schema,
333 description: data.description.clone(),
334 }))
335}
336
337fn parse_request_body(
338 body_ref: &ReferenceOr<openapiv3::RequestBody>,
339 spec: &OpenAPI,
340) -> Result<Option<RequestBody>> {
341 let body = match body_ref {
342 ReferenceOr::Reference { reference } => {
343 let name = reference
344 .strip_prefix("#/components/requestBodies/")
345 .ok_or_else(|| {
346 Error::ParseError(format!("invalid request body reference: {}", reference))
347 })?;
348 spec.components
349 .as_ref()
350 .and_then(|c| c.request_bodies.get(name))
351 .and_then(|b| match b {
352 ReferenceOr::Item(item) => Some(item),
353 _ => None,
354 })
355 .ok_or_else(|| Error::ParseError(format!("request body not found: {}", name)))?
356 }
357 ReferenceOr::Item(item) => item,
358 };
359
360 let (content_type, media) = body
362 .content
363 .iter()
364 .find(|(ct, _)| ct.starts_with("application/json"))
365 .or_else(|| body.content.first())
366 .ok_or_else(|| Error::ParseError("request body has no content".to_string()))?;
367
368 Ok(Some(RequestBody {
369 required: body.required,
370 description: body.description.clone(),
371 content_type: content_type.clone(),
372 schema: media.schema.clone(),
373 }))
374}
375
376fn parse_response(
377 status: ResponseStatus,
378 resp_ref: &ReferenceOr<openapiv3::Response>,
379 spec: &OpenAPI,
380) -> Result<Option<OperationResponse>> {
381 let resp = match resp_ref {
382 ReferenceOr::Reference { reference } => {
383 let name = reference
384 .strip_prefix("#/components/responses/")
385 .ok_or_else(|| {
386 Error::ParseError(format!("invalid response reference: {}", reference))
387 })?;
388 spec.components
389 .as_ref()
390 .and_then(|c| c.responses.get(name))
391 .and_then(|r| match r {
392 ReferenceOr::Item(item) => Some(item),
393 _ => None,
394 })
395 .ok_or_else(|| Error::ParseError(format!("response not found: {}", name)))?
396 }
397 ReferenceOr::Item(item) => item,
398 };
399
400 let (content_type, schema) = if let Some((ct, media)) = resp
402 .content
403 .iter()
404 .find(|(ct, _)| ct.starts_with("application/json"))
405 .or_else(|| resp.content.first())
406 {
407 (Some(ct.clone()), media.schema.clone())
408 } else {
409 (None, None)
410 };
411
412 Ok(Some(OperationResponse {
413 status_code: status,
414 description: resp.description.clone(),
415 schema,
416 content_type,
417 }))
418}