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 raw_name(&self) -> &str {
207 self.operation_id.as_deref().unwrap_or(&self.path)
208 }
209
210 pub fn name(&self) -> String {
214 self.raw_name().to_upper_camel_case()
215 }
216
217 pub fn has_error_responses(&self) -> bool {
219 self.responses.iter().any(|r| r.status_code.is_error())
220 }
221}
222
223#[derive(Debug, Clone)]
225pub struct RequestBody {
226 pub required: bool,
227 pub description: Option<String>,
228 pub content_type: String,
229 pub schema: Option<ReferenceOr<Schema>>,
230}
231
232pub struct ParsedSpec {
234 pub info: SpecInfo,
235 operations: Vec<Operation>,
236 operation_map: HashMap<(HttpMethod, String), usize>,
237 pub components: Option<openapiv3::Components>,
238}
239
240#[derive(Debug, Clone)]
242pub struct SpecInfo {
243 pub title: String,
244 pub version: String,
245 pub description: Option<String>,
246}
247
248impl ParsedSpec {
249 pub fn from_openapi(spec: OpenAPI) -> Result<Self> {
251 let info = SpecInfo {
252 title: spec.info.title.clone(),
253 version: spec.info.version.clone(),
254 description: spec.info.description.clone(),
255 };
256
257 let mut operations = Vec::new();
258 let mut operation_map = HashMap::new();
259
260 for (path, path_item) in &spec.paths.paths {
261 let item = match path_item {
262 ReferenceOr::Reference { .. } => {
263 return Err(Error::Unsupported(
264 "external path references not supported".to_string(),
265 ));
266 }
267 ReferenceOr::Item(item) => item,
268 };
269
270 let method_ops = [
272 (HttpMethod::Get, &item.get),
273 (HttpMethod::Post, &item.post),
274 (HttpMethod::Put, &item.put),
275 (HttpMethod::Delete, &item.delete),
276 (HttpMethod::Patch, &item.patch),
277 (HttpMethod::Head, &item.head),
278 (HttpMethod::Options, &item.options),
279 ];
280
281 for (method, op_opt) in method_ops {
282 if let Some(op) = op_opt {
283 let operation = parse_operation(method, path, op, &item.parameters, &spec)?;
284 let idx = operations.len();
285 operation_map.insert((method, path.clone()), idx);
286 operations.push(operation);
287 }
288 }
289 }
290
291 Ok(Self {
292 info,
293 operations,
294 operation_map,
295 components: spec.components,
296 })
297 }
298
299 pub fn get_operation(&self, method: HttpMethod, path: &str) -> Option<&Operation> {
301 self.operation_map
302 .get(&(method, path.to_string()))
303 .map(|&idx| &self.operations[idx])
304 }
305
306 pub fn operations(&self) -> impl Iterator<Item = &Operation> {
308 self.operations.iter()
309 }
310
311 pub fn schema_names(&self) -> Vec<String> {
313 self.components
314 .as_ref()
315 .map(|c| c.schemas.keys().cloned().collect())
316 .unwrap_or_default()
317 }
318}
319
320pub fn load_spec(path: &Path) -> Result<OpenAPI> {
322 let file =
323 File::open(path).map_err(|e| Error::ParseError(format!("failed to open file: {}", e)))?;
324
325 if let Ok(spec) = serde_json::from_reader::<_, OpenAPI>(&file) {
327 return Ok(spec);
328 }
329
330 let file =
331 File::open(path).map_err(|e| Error::ParseError(format!("failed to open file: {}", e)))?;
332
333 yaml_serde::from_reader(file)
334 .map_err(|e| Error::ParseError(format!("failed to parse spec: {}", e)))
335}
336
337fn parse_operation(
338 method: HttpMethod,
339 path: &str,
340 op: &openapiv3::Operation,
341 path_params: &[ReferenceOr<openapiv3::Parameter>],
342 spec: &OpenAPI,
343) -> Result<Operation> {
344 let mut parameters = Vec::new();
345
346 for param_ref in path_params {
348 if let Some(param) = resolve_parameter(param_ref, spec)? {
349 parameters.push(param);
350 }
351 }
352
353 for param_ref in &op.parameters {
355 if let Some(param) = resolve_parameter(param_ref, spec)? {
356 parameters.retain(|p| !(p.name == param.name && p.location == param.location));
358 parameters.push(param);
359 }
360 }
361
362 let request_body = if let Some(body_ref) = &op.request_body {
364 parse_request_body(body_ref, spec)?
365 } else {
366 None
367 };
368
369 let mut responses = Vec::new();
371
372 if let Some(default) = &op.responses.default
373 && let Some(resp) = parse_response(ResponseStatus::Default, default, spec)?
374 {
375 responses.push(resp);
376 }
377
378 for (code, resp_ref) in &op.responses.responses {
379 let status = match code {
380 StatusCode::Code(c) => ResponseStatus::Code(*c),
381 StatusCode::Range(_) => continue, };
383 if let Some(resp) = parse_response(status, resp_ref, spec)? {
384 responses.push(resp);
385 }
386 }
387
388 Ok(Operation {
389 operation_id: op.operation_id.clone(),
390 method,
391 path: path.to_string(),
392 summary: op.summary.clone(),
393 description: op.description.clone(),
394 parameters,
395 request_body,
396 responses,
397 tags: op.tags.clone(),
398 })
399}
400
401fn resolve_parameter(
402 param_ref: &ReferenceOr<openapiv3::Parameter>,
403 spec: &OpenAPI,
404) -> Result<Option<OperationParam>> {
405 let param = resolve_ref(param_ref, spec)?;
406
407 let (location, data) = match param {
408 openapiv3::Parameter::Path { parameter_data, .. } => (ParamLocation::Path, parameter_data),
409 openapiv3::Parameter::Query { parameter_data, .. } => {
410 (ParamLocation::Query, parameter_data)
411 }
412 openapiv3::Parameter::Header { parameter_data, .. } => {
413 (ParamLocation::Header, parameter_data)
414 }
415 openapiv3::Parameter::Cookie { parameter_data, .. } => {
416 (ParamLocation::Cookie, parameter_data)
417 }
418 };
419
420 let schema = match &data.format {
421 openapiv3::ParameterSchemaOrContent::Schema(s) => Some(s.clone()),
422 openapiv3::ParameterSchemaOrContent::Content(_) => None,
423 };
424
425 Ok(Some(OperationParam {
426 name: data.name.clone(),
427 location,
428 required: data.required,
429 schema,
430 description: data.description.clone(),
431 }))
432}
433
434fn parse_request_body(
435 body_ref: &ReferenceOr<openapiv3::RequestBody>,
436 spec: &OpenAPI,
437) -> Result<Option<RequestBody>> {
438 let body = resolve_ref(body_ref, spec)?;
439
440 let (content_type, media) = body
442 .content
443 .iter()
444 .find(|(ct, _)| ct.starts_with("application/json"))
445 .or_else(|| body.content.first())
446 .ok_or_else(|| Error::ParseError("request body has no content".to_string()))?;
447
448 Ok(Some(RequestBody {
449 required: body.required,
450 description: body.description.clone(),
451 content_type: content_type.clone(),
452 schema: media.schema.clone(),
453 }))
454}
455
456fn parse_response(
457 status: ResponseStatus,
458 resp_ref: &ReferenceOr<openapiv3::Response>,
459 spec: &OpenAPI,
460) -> Result<Option<OperationResponse>> {
461 let resp = resolve_ref(resp_ref, spec)?;
462
463 let (content_type, schema) = if let Some((ct, media)) = resp
465 .content
466 .iter()
467 .find(|(ct, _)| ct.starts_with("application/json"))
468 .or_else(|| resp.content.first())
469 {
470 (Some(ct.clone()), media.schema.clone())
471 } else {
472 (None, None)
473 };
474
475 Ok(Some(OperationResponse {
476 status_code: status,
477 description: resp.description.clone(),
478 schema,
479 content_type,
480 }))
481}