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 schemas.insert(name.clone(), transform_schema(schema, spec));
97 }
98 }
99 }
100
101 OpenApiSpec {
102 info,
103 servers,
104 operations,
105 tags,
106 schemas,
107 }
108}
109
110fn extract_operations(
112 path: &str,
113 item: &PathItem,
114 spec: &OpenAPI,
115 operations: &mut Vec<ApiOperation>,
116) {
117 let methods = [
118 (HttpMethod::Get, &item.get),
119 (HttpMethod::Post, &item.post),
120 (HttpMethod::Put, &item.put),
121 (HttpMethod::Delete, &item.delete),
122 (HttpMethod::Patch, &item.patch),
123 (HttpMethod::Head, &item.head),
124 (HttpMethod::Options, &item.options),
125 ];
126
127 for (method, op_option) in methods {
128 if let Some(op) = op_option {
129 operations.push(transform_operation(
130 path,
131 method,
132 op,
133 &item.parameters,
134 spec,
135 ));
136 }
137 }
138}
139
140fn transform_operation(
142 path: &str,
143 method: HttpMethod,
144 op: &Operation,
145 path_params: &[ReferenceOr<Parameter>],
146 spec: &OpenAPI,
147) -> ApiOperation {
148 let mut parameters: Vec<ApiParameter> = path_params
150 .iter()
151 .filter_map(|p| transform_parameter(p, spec))
152 .collect();
153
154 for param in &op.parameters {
155 if let Some(p) = transform_parameter(param, spec) {
156 if !parameters.iter().any(|existing| existing.name == p.name) {
158 parameters.push(p);
159 }
160 }
161 }
162
163 let request_body = op
164 .request_body
165 .as_ref()
166 .and_then(|rb| transform_request_body(rb, spec));
167
168 let responses = op
169 .responses
170 .responses
171 .iter()
172 .map(|(code, resp)| transform_response(code, resp, spec))
173 .collect();
174
175 ApiOperation {
176 operation_id: op.operation_id.clone(),
177 method,
178 path: path.to_string(),
179 summary: op.summary.clone(),
180 description: op.description.clone(),
181 tags: op.tags.clone(),
182 parameters,
183 request_body,
184 responses,
185 deprecated: op.deprecated,
186 }
187}
188
189fn transform_parameter(param_ref: &ReferenceOr<Parameter>, spec: &OpenAPI) -> Option<ApiParameter> {
191 let param = resolve_parameter(param_ref, spec)?;
192
193 let location = match ¶m.parameter_data_ref().format {
194 openapiv3::ParameterSchemaOrContent::Schema(_) => {
195 match param {
197 Parameter::Query { .. } => ParameterLocation::Query,
198 Parameter::Header { .. } => ParameterLocation::Header,
199 Parameter::Path { .. } => ParameterLocation::Path,
200 Parameter::Cookie { .. } => ParameterLocation::Cookie,
201 }
202 }
203 _ => return None,
204 };
205
206 let data = param.parameter_data_ref();
207 let schema = match &data.format {
208 ParameterSchemaOrContent::Schema(s) => Some(resolve_and_transform_schema(s, spec)),
209 _ => None,
210 };
211
212 Some(ApiParameter {
213 name: data.name.clone(),
214 location,
215 description: data.description.clone(),
216 required: data.required,
217 deprecated: data.deprecated.unwrap_or(false),
218 schema,
219 example: data.example.as_ref().map(format_json_value),
220 })
221}
222
223fn resolve_parameter<'a>(
225 param_ref: &'a ReferenceOr<Parameter>,
226 spec: &'a OpenAPI,
227) -> Option<&'a Parameter> {
228 match param_ref {
229 ReferenceOr::Item(param) => Some(param),
230 ReferenceOr::Reference { reference } => {
231 let name = reference.strip_prefix("#/components/parameters/")?;
232 spec.components
233 .as_ref()?
234 .parameters
235 .get(name)
236 .and_then(|p| match p {
237 ReferenceOr::Item(param) => Some(param),
238 _ => None,
239 })
240 }
241 }
242}
243
244fn transform_request_body(
246 rb_ref: &ReferenceOr<RequestBody>,
247 spec: &OpenAPI,
248) -> Option<ApiRequestBody> {
249 let rb = resolve_request_body(rb_ref, spec)?;
250
251 let content = rb
252 .content
253 .iter()
254 .map(|(media_type, media)| MediaTypeContent {
255 media_type: media_type.clone(),
256 schema: media
257 .schema
258 .as_ref()
259 .map(|s| resolve_and_transform_schema(s, spec)),
260 example: media.example.as_ref().map(format_json_value),
261 })
262 .collect();
263
264 Some(ApiRequestBody {
265 description: rb.description.clone(),
266 required: rb.required,
267 content,
268 })
269}
270
271fn resolve_request_body<'a>(
273 rb_ref: &'a ReferenceOr<RequestBody>,
274 spec: &'a OpenAPI,
275) -> Option<&'a RequestBody> {
276 match rb_ref {
277 ReferenceOr::Item(rb) => Some(rb),
278 ReferenceOr::Reference { reference } => {
279 let name = reference.strip_prefix("#/components/requestBodies/")?;
280 spec.components
281 .as_ref()?
282 .request_bodies
283 .get(name)
284 .and_then(|r| match r {
285 ReferenceOr::Item(rb) => Some(rb),
286 _ => None,
287 })
288 }
289 }
290}
291
292fn transform_response(
294 status_code: &StatusCode,
295 resp_ref: &ReferenceOr<Response>,
296 spec: &OpenAPI,
297) -> ApiResponse {
298 let status_str = match status_code {
299 StatusCode::Code(code) => code.to_string(),
300 StatusCode::Range(range) => format!("{}XX", range),
301 };
302
303 let resp = resolve_response(resp_ref, spec);
304
305 let (description, content) = if let Some(r) = resp {
306 let content = r
307 .content
308 .iter()
309 .map(|(media_type, media)| MediaTypeContent {
310 media_type: media_type.clone(),
311 schema: media
312 .schema
313 .as_ref()
314 .map(|s| resolve_and_transform_schema(s, spec)),
315 example: media.example.as_ref().map(format_json_value),
316 })
317 .collect();
318 (r.description.clone(), content)
319 } else {
320 (String::new(), Vec::new())
321 };
322
323 ApiResponse {
324 status_code: status_str,
325 description,
326 content,
327 }
328}
329
330fn resolve_response<'a>(
332 resp_ref: &'a ReferenceOr<Response>,
333 spec: &'a OpenAPI,
334) -> Option<&'a Response> {
335 match resp_ref {
336 ReferenceOr::Item(resp) => Some(resp),
337 ReferenceOr::Reference { reference } => {
338 let name = reference.strip_prefix("#/components/responses/")?;
339 spec.components
340 .as_ref()?
341 .responses
342 .get(name)
343 .and_then(|r| match r {
344 ReferenceOr::Item(resp) => Some(resp),
345 _ => None,
346 })
347 }
348 }
349}
350
351fn resolve_and_transform_schema(
353 schema_ref: &ReferenceOr<Schema>,
354 spec: &OpenAPI,
355) -> SchemaDefinition {
356 match schema_ref {
357 ReferenceOr::Item(schema) => transform_schema(schema, spec),
358 ReferenceOr::Reference { reference } => {
359 let ref_name = reference
361 .strip_prefix("#/components/schemas/")
362 .map(|s| s.to_string());
363
364 let resolved = ref_name.as_ref().and_then(|name| {
366 spec.components
367 .as_ref()?
368 .schemas
369 .get(name)
370 .and_then(|s| match s {
371 ReferenceOr::Item(schema) => Some(schema),
372 _ => None,
373 })
374 });
375
376 if let Some(schema) = resolved {
377 let mut def = transform_schema(schema, spec);
378 def.ref_name = ref_name;
379 def
380 } else {
381 SchemaDefinition {
382 ref_name,
383 ..Default::default()
384 }
385 }
386 }
387 }
388}
389
390fn resolve_and_transform_boxed_schema(
392 schema_ref: &ReferenceOr<Box<Schema>>,
393 spec: &OpenAPI,
394) -> SchemaDefinition {
395 match schema_ref {
396 ReferenceOr::Item(schema) => transform_schema(schema, spec),
397 ReferenceOr::Reference { reference } => {
398 let ref_name = reference
400 .strip_prefix("#/components/schemas/")
401 .map(|s| s.to_string());
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 let mut def = transform_schema(schema, spec);
417 def.ref_name = ref_name;
418 def
419 } else {
420 SchemaDefinition {
421 ref_name,
422 ..Default::default()
423 }
424 }
425 }
426 }
427}
428
429fn extract_format<T: std::fmt::Debug>(format: &VariantOrUnknownOrEmpty<T>) -> Option<String> {
431 match format {
432 VariantOrUnknownOrEmpty::Item(f) => Some(format!("{:?}", f).to_lowercase()),
433 VariantOrUnknownOrEmpty::Unknown(s) => Some(s.clone()),
434 VariantOrUnknownOrEmpty::Empty => None,
435 }
436}
437
438fn transform_schema(schema: &Schema, spec: &OpenAPI) -> SchemaDefinition {
440 let mut def = SchemaDefinition {
441 description: schema.schema_data.description.clone(),
442 example: schema.schema_data.example.as_ref().map(format_json_value),
443 default: schema.schema_data.default.as_ref().map(format_json_value),
444 nullable: schema.schema_data.nullable,
445 ..Default::default()
446 };
447
448 match &schema.schema_kind {
449 SchemaKind::Type(t) => match t {
450 Type::String(s) => {
451 def.schema_type = SchemaType::String;
452 def.format = extract_format(&s.format);
453 def.enum_values = s.enumeration.iter().filter_map(|v| v.clone()).collect();
454 }
455 Type::Number(n) => {
456 def.schema_type = SchemaType::Number;
457 def.format = extract_format(&n.format);
458 }
459 Type::Integer(i) => {
460 def.schema_type = SchemaType::Integer;
461 def.format = extract_format(&i.format);
462 }
463 Type::Boolean(_) => {
464 def.schema_type = SchemaType::Boolean;
465 }
466 Type::Array(a) => {
467 def.schema_type = SchemaType::Array;
468 if let Some(items) = &a.items {
469 def.items = Some(Box::new(resolve_and_transform_boxed_schema(items, spec)));
470 }
471 }
472 Type::Object(o) => {
473 def.schema_type = SchemaType::Object;
474 def.required = o.required.clone();
475 for (name, prop) in &o.properties {
476 let prop_schema = resolve_and_transform_boxed_schema(prop, spec);
477 def.properties.insert(name.clone(), prop_schema);
478 }
479 if let Some(ap) = &o.additional_properties {
480 match ap {
481 openapiv3::AdditionalProperties::Any(true) => {
482 def.additional_properties = Some(Box::new(SchemaDefinition::default()));
483 }
484 openapiv3::AdditionalProperties::Schema(s) => {
485 def.additional_properties =
486 Some(Box::new(resolve_and_transform_schema(s, spec)));
487 }
488 _ => {}
489 }
490 }
491 }
492 },
493 SchemaKind::OneOf { one_of } => {
494 def.one_of = one_of
495 .iter()
496 .map(|s| resolve_and_transform_schema(s, spec))
497 .collect();
498 }
499 SchemaKind::AnyOf { any_of } => {
500 def.any_of = any_of
501 .iter()
502 .map(|s| resolve_and_transform_schema(s, spec))
503 .collect();
504 }
505 SchemaKind::AllOf { all_of } => {
506 def.all_of = all_of
507 .iter()
508 .map(|s| resolve_and_transform_schema(s, spec))
509 .collect();
510 }
511 SchemaKind::Not { .. } => {
512 }
514 SchemaKind::Any(_) => {
515 }
517 }
518
519 def
520}
521
522fn format_json_value(value: &serde_json::Value) -> String {
524 match value {
525 serde_json::Value::String(s) => s.clone(),
526 other => serde_json::to_string_pretty(other).unwrap_or_default(),
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533
534 #[test]
535 fn test_parse_simple_openapi() {
536 let yaml = r#"
537openapi: "3.0.0"
538info:
539 title: Test API
540 version: "1.0.0"
541 description: A test API
542paths:
543 /users:
544 get:
545 summary: List users
546 responses:
547 "200":
548 description: Success
549"#;
550 let spec = parse_openapi(yaml).unwrap();
551 assert_eq!(spec.info.title, "Test API");
552 assert_eq!(spec.info.version, "1.0.0");
553 assert_eq!(spec.operations.len(), 1);
554 assert_eq!(spec.operations[0].method, HttpMethod::Get);
555 assert_eq!(spec.operations[0].path, "/users");
556 }
557
558 #[test]
559 fn test_parse_with_parameters() {
560 let yaml = r#"
561openapi: "3.0.0"
562info:
563 title: Test API
564 version: "1.0.0"
565paths:
566 /users/{id}:
567 get:
568 summary: Get user
569 parameters:
570 - name: id
571 in: path
572 required: true
573 schema:
574 type: string
575 - name: include
576 in: query
577 schema:
578 type: string
579 responses:
580 "200":
581 description: Success
582"#;
583 let spec = parse_openapi(yaml).unwrap();
584 assert_eq!(spec.operations[0].parameters.len(), 2);
585 assert_eq!(spec.operations[0].parameters[0].name, "id");
586 assert_eq!(
587 spec.operations[0].parameters[0].location,
588 ParameterLocation::Path
589 );
590 assert!(spec.operations[0].parameters[0].required);
591 }
592
593 #[test]
594 fn test_parse_with_request_body() {
595 let yaml = r#"
596openapi: "3.0.0"
597info:
598 title: Test API
599 version: "1.0.0"
600paths:
601 /users:
602 post:
603 summary: Create user
604 requestBody:
605 required: true
606 content:
607 application/json:
608 schema:
609 type: object
610 properties:
611 name:
612 type: string
613 responses:
614 "201":
615 description: Created
616"#;
617 let spec = parse_openapi(yaml).unwrap();
618 let rb = spec.operations[0].request_body.as_ref().unwrap();
619 assert!(rb.required);
620 assert_eq!(rb.content[0].media_type, "application/json");
621 }
622
623 #[test]
624 fn test_http_method_badge_class() {
625 assert_eq!(HttpMethod::Get.badge_class(), "badge-soft badge-success");
626 assert_eq!(HttpMethod::Post.badge_class(), "badge-soft badge-primary");
627 assert_eq!(HttpMethod::Delete.badge_class(), "badge-soft badge-error");
628 }
629}