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