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