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