1use std::collections::BTreeMap;
6
7use openapiv3::{
8 OpenAPI, Operation, Parameter, ParameterSchemaOrContent, PathItem, ReferenceOr, RequestBody,
9 Response, Schema, SchemaKind, StatusCode, Type, VariantOrUnknownOrEmpty,
10};
11
12use super::openapi_types::*;
13
14#[derive(Debug, Clone)]
16#[non_exhaustive]
17pub enum OpenApiError {
18 ParseError(String),
20 InvalidSpec(String),
22}
23
24impl std::fmt::Display for OpenApiError {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 match self {
27 Self::ParseError(msg) => write!(f, "Parse error: {}", msg),
28 Self::InvalidSpec(msg) => write!(f, "Invalid spec: {}", msg),
29 }
30 }
31}
32
33impl std::error::Error for OpenApiError {}
34
35pub fn parse_openapi(content: &str) -> Result<OpenApiSpec, OpenApiError> {
37 let spec: OpenAPI = match serde_yaml::from_str(content) {
39 Ok(s) => s,
40 Err(yaml_err) => match serde_json::from_str(content) {
41 Ok(s) => s,
42 Err(json_err) => {
43 let msg = if content.trim_start().starts_with(['{', '[']) {
46 format!("JSON: {json_err}")
47 } else {
48 format!("YAML: {yaml_err}")
49 };
50 return Err(OpenApiError::ParseError(msg));
51 }
52 },
53 };
54
55 Ok(transform_spec(&spec))
56}
57
58fn transform_spec(spec: &OpenAPI) -> OpenApiSpec {
60 let info = ApiInfo {
61 title: spec.info.title.clone(),
62 version: spec.info.version.clone(),
63 description: spec.info.description.clone(),
64 };
65
66 let servers = spec
67 .servers
68 .iter()
69 .map(|s| ApiServer {
70 url: s.url.clone(),
71 description: s.description.clone(),
72 })
73 .collect();
74
75 let tags: Vec<ApiTag> = spec
76 .tags
77 .iter()
78 .map(|t| ApiTag {
79 name: t.name.clone(),
80 description: t.description.clone(),
81 })
82 .collect();
83
84 let mut operations = Vec::new();
86 for (path, item) in &spec.paths.paths {
87 if let ReferenceOr::Item(path_item) = item {
88 extract_operations(path, path_item, spec, &mut operations);
89 }
90 }
91
92 let mut schemas = BTreeMap::new();
94 if let Some(components) = &spec.components {
95 for (name, schema_ref) in &components.schemas {
96 if let ReferenceOr::Item(schema) = schema_ref {
97 let mut seen = vec![name.clone()];
100 schemas.insert(name.clone(), transform_schema(schema, spec, &mut seen));
101 }
102 }
103 }
104
105 OpenApiSpec {
106 info,
107 servers,
108 operations,
109 tags,
110 schemas,
111 }
112}
113
114fn extract_operations(
116 path: &str,
117 item: &PathItem,
118 spec: &OpenAPI,
119 operations: &mut Vec<ApiOperation>,
120) {
121 let methods = [
122 (HttpMethod::Get, &item.get),
123 (HttpMethod::Post, &item.post),
124 (HttpMethod::Put, &item.put),
125 (HttpMethod::Delete, &item.delete),
126 (HttpMethod::Patch, &item.patch),
127 (HttpMethod::Head, &item.head),
128 (HttpMethod::Options, &item.options),
129 ];
130
131 for (method, op_option) in methods {
132 if let Some(op) = op_option {
133 operations.push(transform_operation(
134 path,
135 method,
136 op,
137 &item.parameters,
138 spec,
139 ));
140 }
141 }
142}
143
144fn transform_operation(
146 path: &str,
147 method: HttpMethod,
148 op: &Operation,
149 path_params: &[ReferenceOr<Parameter>],
150 spec: &OpenAPI,
151) -> ApiOperation {
152 let mut parameters: Vec<ApiParameter> = path_params
154 .iter()
155 .filter_map(|p| transform_parameter(p, spec))
156 .collect();
157
158 for param in &op.parameters {
159 if let Some(p) = transform_parameter(param, spec) {
160 if !parameters.iter().any(|existing| existing.name == p.name) {
162 parameters.push(p);
163 }
164 }
165 }
166
167 let request_body = op
168 .request_body
169 .as_ref()
170 .and_then(|rb| transform_request_body(rb, spec));
171
172 let responses = op
173 .responses
174 .responses
175 .iter()
176 .map(|(code, resp)| transform_response(code, resp, spec))
177 .collect();
178
179 ApiOperation {
180 operation_id: op.operation_id.clone(),
181 method,
182 path: path.to_string(),
183 summary: op.summary.clone(),
184 description: op.description.clone(),
185 tags: op.tags.clone(),
186 parameters,
187 request_body,
188 responses,
189 deprecated: op.deprecated,
190 }
191}
192
193fn transform_parameter(param_ref: &ReferenceOr<Parameter>, spec: &OpenAPI) -> Option<ApiParameter> {
195 let param = resolve_parameter(param_ref, spec)?;
196
197 let location = match ¶m.parameter_data_ref().format {
198 openapiv3::ParameterSchemaOrContent::Schema(_) => {
199 match param {
201 Parameter::Query { .. } => ParameterLocation::Query,
202 Parameter::Header { .. } => ParameterLocation::Header,
203 Parameter::Path { .. } => ParameterLocation::Path,
204 Parameter::Cookie { .. } => ParameterLocation::Cookie,
205 }
206 }
207 _ => return None,
208 };
209
210 let data = param.parameter_data_ref();
211 let schema = match &data.format {
212 ParameterSchemaOrContent::Schema(s) => {
213 Some(resolve_and_transform(s, spec, &mut Vec::new()))
214 }
215 _ => None,
216 };
217
218 Some(ApiParameter {
219 name: data.name.clone(),
220 location,
221 description: data.description.clone(),
222 required: data.required,
223 deprecated: data.deprecated.unwrap_or(false),
224 schema,
225 example: data.example.as_ref().map(format_json_value),
226 })
227}
228
229fn resolve_parameter<'a>(
231 param_ref: &'a ReferenceOr<Parameter>,
232 spec: &'a OpenAPI,
233) -> Option<&'a Parameter> {
234 match param_ref {
235 ReferenceOr::Item(param) => Some(param),
236 ReferenceOr::Reference { reference } => {
237 let name = reference.strip_prefix("#/components/parameters/")?;
238 spec.components
239 .as_ref()?
240 .parameters
241 .get(name)
242 .and_then(|p| match p {
243 ReferenceOr::Item(param) => Some(param),
244 _ => None,
245 })
246 }
247 }
248}
249
250fn transform_request_body(
252 rb_ref: &ReferenceOr<RequestBody>,
253 spec: &OpenAPI,
254) -> Option<ApiRequestBody> {
255 let rb = resolve_request_body(rb_ref, spec)?;
256
257 let content = rb
258 .content
259 .iter()
260 .map(|(media_type, media)| MediaTypeContent {
261 media_type: media_type.clone(),
262 schema: media
263 .schema
264 .as_ref()
265 .map(|s| resolve_and_transform(s, spec, &mut Vec::new())),
266 example: media.example.as_ref().map(format_json_value),
267 })
268 .collect();
269
270 Some(ApiRequestBody {
271 description: rb.description.clone(),
272 required: rb.required,
273 content,
274 })
275}
276
277fn resolve_request_body<'a>(
279 rb_ref: &'a ReferenceOr<RequestBody>,
280 spec: &'a OpenAPI,
281) -> Option<&'a RequestBody> {
282 match rb_ref {
283 ReferenceOr::Item(rb) => Some(rb),
284 ReferenceOr::Reference { reference } => {
285 let name = reference.strip_prefix("#/components/requestBodies/")?;
286 spec.components
287 .as_ref()?
288 .request_bodies
289 .get(name)
290 .and_then(|r| match r {
291 ReferenceOr::Item(rb) => Some(rb),
292 _ => None,
293 })
294 }
295 }
296}
297
298fn transform_response(
300 status_code: &StatusCode,
301 resp_ref: &ReferenceOr<Response>,
302 spec: &OpenAPI,
303) -> ApiResponse {
304 let status_str = match status_code {
305 StatusCode::Code(code) => code.to_string(),
306 StatusCode::Range(range) => format!("{}XX", range),
307 };
308
309 let resp = resolve_response(resp_ref, spec);
310
311 let (description, content) = if let Some(r) = resp {
312 let content = r
313 .content
314 .iter()
315 .map(|(media_type, media)| MediaTypeContent {
316 media_type: media_type.clone(),
317 schema: media
318 .schema
319 .as_ref()
320 .map(|s| resolve_and_transform(s, spec, &mut Vec::new())),
321 example: media.example.as_ref().map(format_json_value),
322 })
323 .collect();
324 (r.description.clone(), content)
325 } else {
326 (String::new(), Vec::new())
327 };
328
329 ApiResponse {
330 status_code: status_str,
331 description,
332 content,
333 }
334}
335
336fn resolve_response<'a>(
338 resp_ref: &'a ReferenceOr<Response>,
339 spec: &'a OpenAPI,
340) -> Option<&'a Response> {
341 match resp_ref {
342 ReferenceOr::Item(resp) => Some(resp),
343 ReferenceOr::Reference { reference } => {
344 let name = reference.strip_prefix("#/components/responses/")?;
345 spec.components
346 .as_ref()?
347 .responses
348 .get(name)
349 .and_then(|r| match r {
350 ReferenceOr::Item(resp) => Some(resp),
351 _ => None,
352 })
353 }
354 }
355}
356
357trait AsSchema {
359 fn as_schema(&self) -> &Schema;
360}
361
362impl AsSchema for Schema {
363 fn as_schema(&self) -> &Schema {
364 self
365 }
366}
367
368impl AsSchema for Box<Schema> {
369 fn as_schema(&self) -> &Schema {
370 self
371 }
372}
373
374fn resolve_and_transform<S: AsSchema>(
381 schema_ref: &ReferenceOr<S>,
382 spec: &OpenAPI,
383 seen: &mut Vec<String>,
384) -> SchemaDefinition {
385 match schema_ref {
386 ReferenceOr::Item(schema) => transform_schema(schema.as_schema(), spec, seen),
387 ReferenceOr::Reference { reference } => {
388 let ref_name = reference
390 .strip_prefix("#/components/schemas/")
391 .map(|s| s.to_string());
392
393 if let Some(name) = &ref_name
395 && seen.contains(name)
396 {
397 return SchemaDefinition {
398 ref_name: ref_name.clone(),
399 ..Default::default()
400 };
401 }
402
403 let resolved = ref_name.as_ref().and_then(|name| {
405 spec.components
406 .as_ref()?
407 .schemas
408 .get(name)
409 .and_then(|s| match s {
410 ReferenceOr::Item(schema) => Some(schema),
411 _ => None,
412 })
413 });
414
415 if let Some(schema) = resolved {
416 if let Some(name) = &ref_name {
417 seen.push(name.clone());
418 }
419 let mut def = transform_schema(schema, spec, seen);
420 if ref_name.is_some() {
421 seen.pop();
422 }
423 def.ref_name = ref_name;
424 def
425 } else {
426 SchemaDefinition {
427 ref_name,
428 ..Default::default()
429 }
430 }
431 }
432 }
433}
434
435fn extract_format<T: std::fmt::Debug>(format: &VariantOrUnknownOrEmpty<T>) -> Option<String> {
437 match format {
438 VariantOrUnknownOrEmpty::Item(f) => Some(format!("{:?}", f).to_lowercase()),
439 VariantOrUnknownOrEmpty::Unknown(s) => Some(s.clone()),
440 VariantOrUnknownOrEmpty::Empty => None,
441 }
442}
443
444fn transform_schema(schema: &Schema, spec: &OpenAPI, seen: &mut Vec<String>) -> SchemaDefinition {
446 let mut def = SchemaDefinition {
447 description: schema.schema_data.description.clone(),
448 example: schema.schema_data.example.as_ref().map(format_json_value),
449 default: schema.schema_data.default.as_ref().map(format_json_value),
450 nullable: schema.schema_data.nullable,
451 ..Default::default()
452 };
453
454 match &schema.schema_kind {
455 SchemaKind::Type(t) => match t {
456 Type::String(s) => {
457 def.schema_type = SchemaType::String;
458 def.format = extract_format(&s.format);
459 def.enum_values = s.enumeration.iter().filter_map(|v| v.clone()).collect();
460 }
461 Type::Number(n) => {
462 def.schema_type = SchemaType::Number;
463 def.format = extract_format(&n.format);
464 }
465 Type::Integer(i) => {
466 def.schema_type = SchemaType::Integer;
467 def.format = extract_format(&i.format);
468 }
469 Type::Boolean(_) => {
470 def.schema_type = SchemaType::Boolean;
471 }
472 Type::Array(a) => {
473 def.schema_type = SchemaType::Array;
474 if let Some(items) = &a.items {
475 def.items = Some(Box::new(resolve_and_transform(items, spec, seen)));
476 }
477 }
478 Type::Object(o) => {
479 def.schema_type = SchemaType::Object;
480 def.required = o.required.clone();
481 for (name, prop) in &o.properties {
482 let prop_schema = resolve_and_transform(prop, spec, seen);
483 def.properties.insert(name.clone(), prop_schema);
484 }
485 if let Some(ap) = &o.additional_properties {
486 match ap {
487 openapiv3::AdditionalProperties::Any(true) => {
488 def.additional_properties = Some(Box::new(SchemaDefinition::default()));
489 }
490 openapiv3::AdditionalProperties::Schema(s) => {
491 def.additional_properties =
492 Some(Box::new(resolve_and_transform(s, spec, seen)));
493 }
494 _ => {}
495 }
496 }
497 }
498 },
499 SchemaKind::OneOf { one_of } => {
500 def.one_of = one_of
501 .iter()
502 .map(|s| resolve_and_transform(s, spec, seen))
503 .collect();
504 }
505 SchemaKind::AnyOf { any_of } => {
506 def.any_of = any_of
507 .iter()
508 .map(|s| resolve_and_transform(s, spec, seen))
509 .collect();
510 }
511 SchemaKind::AllOf { all_of } => {
512 def.all_of = all_of
513 .iter()
514 .map(|s| resolve_and_transform(s, spec, seen))
515 .collect();
516 }
517 SchemaKind::Not { .. } => {
518 }
520 SchemaKind::Any(_) => {
521 }
523 }
524
525 def
526}
527
528fn format_json_value(value: &serde_json::Value) -> String {
530 match value {
531 serde_json::Value::String(s) => s.clone(),
532 other => serde_json::to_string_pretty(other).unwrap_or_default(),
533 }
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539
540 #[test]
541 fn test_parse_simple_openapi() {
542 let yaml = r#"
543openapi: "3.0.0"
544info:
545 title: Test API
546 version: "1.0.0"
547 description: A test API
548paths:
549 /users:
550 get:
551 summary: List users
552 responses:
553 "200":
554 description: Success
555"#;
556 let spec = parse_openapi(yaml).unwrap();
557 assert_eq!(spec.info.title, "Test API");
558 assert_eq!(spec.info.version, "1.0.0");
559 assert_eq!(spec.operations.len(), 1);
560 assert_eq!(spec.operations[0].method, HttpMethod::Get);
561 assert_eq!(spec.operations[0].path, "/users");
562 }
563
564 #[test]
565 fn test_self_referential_schema_terminates() {
566 let yaml = r##"
567openapi: "3.0.0"
568info:
569 title: Tree API
570 version: "1.0.0"
571paths: {}
572components:
573 schemas:
574 Node:
575 type: object
576 properties:
577 name:
578 type: string
579 children:
580 type: array
581 items:
582 $ref: "#/components/schemas/Node"
583"##;
584 let spec = parse_openapi(yaml).unwrap();
585 let node = spec.schemas.get("Node").expect("Node schema");
586
587 let children = node.properties.get("children").expect("children property");
590 let item = children.items.as_ref().expect("array items");
591 assert_eq!(item.ref_name.as_deref(), Some("Node"));
592 assert!(item.properties.is_empty());
593
594 assert_eq!(
596 node.properties.get("name").map(|p| p.schema_type.clone()),
597 Some(SchemaType::String)
598 );
599 }
600
601 #[test]
602 fn test_mutually_recursive_schemas_terminate() {
603 let yaml = r##"
604openapi: "3.0.0"
605info:
606 title: Loop API
607 version: "1.0.0"
608paths: {}
609components:
610 schemas:
611 A:
612 type: object
613 properties:
614 b:
615 $ref: "#/components/schemas/B"
616 B:
617 type: object
618 properties:
619 a:
620 $ref: "#/components/schemas/A"
621"##;
622 let spec = parse_openapi(yaml).unwrap();
623 let a = spec.schemas.get("A").expect("A schema");
624 let b_prop = a.properties.get("b").expect("b property");
625 assert_eq!(b_prop.ref_name.as_deref(), Some("B"));
626
627 let a_prop = b_prop.properties.get("a").expect("a property");
629 assert_eq!(a_prop.ref_name.as_deref(), Some("A"));
630 assert!(a_prop.properties.is_empty());
631 }
632
633 #[test]
634 fn test_parse_with_parameters() {
635 let yaml = r#"
636openapi: "3.0.0"
637info:
638 title: Test API
639 version: "1.0.0"
640paths:
641 /users/{id}:
642 get:
643 summary: Get user
644 parameters:
645 - name: id
646 in: path
647 required: true
648 schema:
649 type: string
650 - name: include
651 in: query
652 schema:
653 type: string
654 responses:
655 "200":
656 description: Success
657"#;
658 let spec = parse_openapi(yaml).unwrap();
659 assert_eq!(spec.operations[0].parameters.len(), 2);
660 assert_eq!(spec.operations[0].parameters[0].name, "id");
661 assert_eq!(
662 spec.operations[0].parameters[0].location,
663 ParameterLocation::Path
664 );
665 assert!(spec.operations[0].parameters[0].required);
666 }
667
668 #[test]
669 fn test_parse_with_request_body() {
670 let yaml = r#"
671openapi: "3.0.0"
672info:
673 title: Test API
674 version: "1.0.0"
675paths:
676 /users:
677 post:
678 summary: Create user
679 requestBody:
680 required: true
681 content:
682 application/json:
683 schema:
684 type: object
685 properties:
686 name:
687 type: string
688 responses:
689 "201":
690 description: Created
691"#;
692 let spec = parse_openapi(yaml).unwrap();
693 let rb = spec.operations[0].request_body.as_ref().unwrap();
694 assert!(rb.required);
695 assert_eq!(rb.content[0].media_type, "application/json");
696 }
697
698 #[test]
699 fn test_http_method_badge_class() {
700 assert_eq!(HttpMethod::Get.badge_class(), "badge-soft badge-success");
701 assert_eq!(HttpMethod::Post.badge_class(), "badge-soft badge-primary");
702 assert_eq!(HttpMethod::Delete.badge_class(), "badge-soft badge-error");
703 }
704}