1use crate::{
2 models::{
3 CompositionModel, EnumModel, Field, Model, ModelType, RequestModel, ResponseModel,
4 TypeAliasModel, UnionModel, UnionType, UnionVariant,
5 },
6 Result,
7};
8use indexmap::IndexMap;
9use openapiv3::{
10 OpenAPI, ReferenceOr, Schema, SchemaKind, StringFormat, Type, VariantOrUnknownOrEmpty,
11};
12use std::collections::HashSet;
13
14#[derive(Debug)]
16struct FieldInfo {
17 field_type: String,
18 format: String,
19 is_nullable: bool,
20}
21
22fn to_pascal_case(input: &str) -> String {
25 input
26 .split(&['-', '_'][..])
27 .filter(|s| !s.is_empty())
28 .map(|s| {
29 let mut chars = s.chars();
30 match chars.next() {
31 Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
32 None => String::new(),
33 }
34 })
35 .collect::<String>()
36}
37
38fn extract_custom_attrs(schema: &Schema) -> Option<Vec<String>> {
40 schema
41 .schema_data
42 .extensions
43 .get("x-rust-attrs")
44 .and_then(|value| {
45 if let Some(arr) = value.as_array() {
46 let attrs: Vec<String> = arr
47 .iter()
48 .filter_map(|v| v.as_str().map(|s| s.to_string()))
49 .collect();
50 if attrs.is_empty() {
51 None
52 } else {
53 Some(attrs)
54 }
55 } else {
56 tracing::warn!(
57 "x-rust-attrs should be an array of strings, got: {:?}",
58 value
59 );
60 None
61 }
62 })
63}
64
65pub fn parse_openapi(
66 openapi: &OpenAPI,
67) -> Result<(Vec<ModelType>, Vec<RequestModel>, Vec<ResponseModel>)> {
68 let mut models = Vec::new();
69 let mut requests = Vec::new();
70 let mut responses = Vec::new();
71
72 let mut added_models = HashSet::new();
73
74 let empty_schemas = IndexMap::new();
75 let empty_request_bodies = IndexMap::new();
76
77 let (schemas, request_bodies) = if let Some(components) = &openapi.components {
78 (&components.schemas, &components.request_bodies)
79 } else {
80 (&empty_schemas, &empty_request_bodies)
81 };
82
83 if let Some(components) = &openapi.components {
85 for (name, schema) in &components.schemas {
86 let model_types = parse_schema_to_model_type(name, schema, &components.schemas)?;
87 for model_type in model_types {
88 if added_models.insert(model_type.name().to_string()) {
89 models.push(model_type);
90 }
91 }
92 }
93
94 for (name, request_body_ref) in &components.request_bodies {
96 if let ReferenceOr::Item(request_body) = request_body_ref {
97 for media_type in request_body.content.values() {
98 if let Some(schema) = &media_type.schema {
99 let model_types =
100 parse_schema_to_model_type(name, schema, &components.schemas)?;
101 for model_type in model_types {
102 if added_models.insert(model_type.name().to_string()) {
103 models.push(model_type);
104 }
105 }
106 }
107 }
108 }
109 }
110 }
111
112 for (_path, path_item) in openapi.paths.iter() {
114 let path_item = match path_item {
115 ReferenceOr::Item(item) => item,
116 ReferenceOr::Reference { .. } => continue,
117 };
118
119 let operations = [
120 &path_item.get,
121 &path_item.post,
122 &path_item.put,
123 &path_item.delete,
124 &path_item.patch,
125 ];
126
127 for op in operations.iter().filter_map(|o| o.as_ref()) {
128 let inline_models =
129 process_operation(op, &mut requests, &mut responses, schemas, request_bodies)?;
130 for model_type in inline_models {
131 if added_models.insert(model_type.name().to_string()) {
132 models.push(model_type);
133 }
134 }
135 }
136 }
137
138 Ok((models, requests, responses))
139}
140
141fn process_operation(
142 operation: &openapiv3::Operation,
143 requests: &mut Vec<RequestModel>,
144 responses: &mut Vec<ResponseModel>,
145 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
146 request_bodies: &IndexMap<String, ReferenceOr<openapiv3::RequestBody>>,
147) -> Result<Vec<ModelType>> {
148 let mut inline_models = Vec::new();
149
150 if let Some(request_body_ref) = &operation.request_body {
152 let (request_body_data, is_inline) = match request_body_ref {
153 ReferenceOr::Item(request_body) => (Some((request_body, request_body.required)), true),
154 ReferenceOr::Reference { reference } => {
155 if let Some(rb_name) = reference.strip_prefix("#/components/requestBodies/") {
156 (
157 request_bodies.get(rb_name).and_then(|rb_ref| match rb_ref {
158 ReferenceOr::Item(rb) => Some((rb, false)),
159 ReferenceOr::Reference { .. } => None,
160 }),
161 false,
162 )
163 } else {
164 (None, false)
165 }
166 }
167 };
168
169 if let Some((request_body, is_required)) = request_body_data {
170 for (content_type, media_type) in &request_body.content {
171 if let Some(schema) = &media_type.schema {
172 let operation_name =
173 to_pascal_case(operation.operation_id.as_deref().unwrap_or("Unknown"));
174
175 let schema_type = if is_inline {
176 if let ReferenceOr::Item(schema_item) = schema {
177 if matches!(schema_item.schema_kind, SchemaKind::Type(Type::Object(_)))
178 {
179 let model_name = format!("{operation_name}RequestBody");
180 let model_types =
181 parse_schema_to_model_type(&model_name, schema, all_schemas)?;
182 inline_models.extend(model_types);
183 model_name
184 } else {
185 extract_type_and_format(schema, all_schemas)?.0
186 }
187 } else {
188 extract_type_and_format(schema, all_schemas)?.0
189 }
190 } else {
191 extract_type_and_format(schema, all_schemas)?.0
192 };
193
194 let request = RequestModel {
195 name: format!("{operation_name}Request"),
196 content_type: content_type.clone(),
197 schema: schema_type,
198 is_required,
199 };
200 requests.push(request);
201 }
202 }
203 }
204 }
205
206 for (status, response_ref) in operation.responses.responses.iter() {
208 if let ReferenceOr::Item(response) = response_ref {
209 for (content_type, media_type) in &response.content {
210 if let Some(schema) = &media_type.schema {
211 let response = ResponseModel {
212 name: format!(
213 "{}Response",
214 to_pascal_case(operation.operation_id.as_deref().unwrap_or("Unknown"))
215 ),
216 status_code: status.to_string(),
217 content_type: content_type.clone(),
218 schema: extract_type_and_format(schema, all_schemas)?.0,
219 description: Some(response.description.clone()),
220 };
221 responses.push(response);
222 }
223 }
224 }
225 }
226 Ok(inline_models)
227}
228
229fn parse_schema_to_model_type(
230 name: &str,
231 schema: &ReferenceOr<Schema>,
232 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
233) -> Result<Vec<ModelType>> {
234 match schema {
235 ReferenceOr::Reference { .. } => Ok(Vec::new()),
236 ReferenceOr::Item(schema) => {
237 if let Some(rust_type) = schema.schema_data.extensions.get("x-rust-type") {
238 if let Some(type_str) = rust_type.as_str() {
239 return Ok(vec![ModelType::TypeAlias(TypeAliasModel {
240 name: to_pascal_case(name),
241 target_type: type_str.to_string(),
242 description: schema.schema_data.description.clone(),
243 custom_attrs: extract_custom_attrs(schema),
244 })]);
245 }
246 }
247
248 match &schema.schema_kind {
249 SchemaKind::Type(Type::Object(obj)) => {
251 if obj.properties.is_empty() && obj.additional_properties.is_some() {
253 let hashmap_type = match &obj.additional_properties {
254 Some(additional_props) => match additional_props {
255 openapiv3::AdditionalProperties::Any(_) => {
256 "std::collections::HashMap<String, serde_json::Value>"
257 .to_string()
258 }
259 openapiv3::AdditionalProperties::Schema(schema_ref) => {
260 let (inner_type, _) =
261 extract_type_and_format(schema_ref, all_schemas)?;
262 format!("std::collections::HashMap<String, {inner_type}>")
263 }
264 },
265 None => {
266 "std::collections::HashMap<String, serde_json::Value>".to_string()
267 }
268 };
269 return Ok(vec![ModelType::TypeAlias(TypeAliasModel {
270 name: to_pascal_case(name),
271 target_type: hashmap_type,
272 description: schema.schema_data.description.clone(),
273 custom_attrs: extract_custom_attrs(schema),
274 })]);
275 }
276
277 let mut fields = Vec::new();
278 let mut inline_models = Vec::new();
279
280 for (field_name, field_schema) in &obj.properties {
282 let (field_info, inline_model) = match field_schema {
283 ReferenceOr::Item(boxed_schema) => extract_field_info(
284 field_name,
285 &ReferenceOr::Item((**boxed_schema).clone()),
286 all_schemas,
287 )?,
288 ReferenceOr::Reference { reference } => extract_field_info(
289 field_name,
290 &ReferenceOr::Reference {
291 reference: reference.clone(),
292 },
293 all_schemas,
294 )?,
295 };
296 if let Some(inline_model) = inline_model {
297 inline_models.push(inline_model);
298 }
299 let is_required = obj.required.contains(field_name);
300 fields.push(Field {
301 name: field_name.clone(),
302 field_type: field_info.field_type,
303 format: field_info.format,
304 is_required,
305 is_nullable: field_info.is_nullable,
306 });
307 }
308
309 let mut models = inline_models;
310 if obj.properties.is_empty() && obj.additional_properties.is_none() {
311 models.push(ModelType::Struct(Model {
312 name: to_pascal_case(name),
313 fields: vec![], custom_attrs: extract_custom_attrs(schema),
315 }));
316 } else if !fields.is_empty() {
317 models.push(ModelType::Struct(Model {
318 name: to_pascal_case(name),
319 fields,
320 custom_attrs: extract_custom_attrs(schema),
321 }));
322 }
323 Ok(models)
324 }
325
326 SchemaKind::AllOf { all_of } => {
328 let (all_fields, inline_models) =
329 resolve_all_of_fields(name, all_of, all_schemas)?;
330 let mut models = inline_models;
331
332 if !all_fields.is_empty() {
333 models.push(ModelType::Composition(CompositionModel {
334 name: to_pascal_case(name),
335 all_fields,
336 custom_attrs: extract_custom_attrs(schema),
337 }));
338 }
339
340 Ok(models)
341 }
342
343 SchemaKind::OneOf { one_of } => {
345 let (variants, inline_models) =
346 resolve_union_variants(name, one_of, all_schemas)?;
347 let mut models = inline_models;
348
349 models.push(ModelType::Union(UnionModel {
350 name: to_pascal_case(name),
351 variants,
352 union_type: UnionType::OneOf,
353 custom_attrs: extract_custom_attrs(schema),
354 }));
355
356 Ok(models)
357 }
358
359 SchemaKind::AnyOf { any_of } => {
361 let (variants, inline_models) =
362 resolve_union_variants(name, any_of, all_schemas)?;
363 let mut models = inline_models;
364
365 models.push(ModelType::Union(UnionModel {
366 name: to_pascal_case(name),
367 variants,
368 union_type: UnionType::AnyOf,
369 custom_attrs: extract_custom_attrs(schema),
370 }));
371
372 Ok(models)
373 }
374
375 SchemaKind::Type(Type::String(string_type)) => {
377 if !string_type.enumeration.is_empty() {
378 let variants: Vec<String> = string_type
379 .enumeration
380 .iter()
381 .filter_map(|value| value.clone())
382 .collect();
383
384 if !variants.is_empty() {
385 let models = vec![ModelType::Enum(EnumModel {
386 name: to_pascal_case(name),
387 variants,
388 description: schema.schema_data.description.clone(),
389 custom_attrs: extract_custom_attrs(schema),
390 })];
391
392 return Ok(models);
393 }
394 }
395 Ok(Vec::new())
396 }
397
398 _ => Ok(Vec::new()),
399 }
400 }
401 }
402}
403
404fn extract_type_and_format(
405 schema: &ReferenceOr<Schema>,
406 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
407) -> Result<(String, String)> {
408 match schema {
409 ReferenceOr::Reference { reference } => {
410 let type_name = reference.split('/').next_back().unwrap_or("Unknown");
411
412 if let Some(ReferenceOr::Item(schema)) = all_schemas.get(type_name) {
413 if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
414 return Ok((to_pascal_case(type_name), "oneOf".to_string()));
415 }
416 }
417 Ok((to_pascal_case(type_name), "reference".to_string()))
418 }
419
420 ReferenceOr::Item(schema) => match &schema.schema_kind {
421 SchemaKind::Type(Type::String(string_type)) => match &string_type.format {
422 VariantOrUnknownOrEmpty::Item(fmt) => match fmt {
423 StringFormat::DateTime => {
424 Ok(("DateTime<Utc>".to_string(), "date-time".to_string()))
425 }
426 StringFormat::Date => Ok(("NaiveDate".to_string(), "date".to_string())),
427 _ => Ok(("String".to_string(), format!("{fmt:?}"))),
428 },
429 VariantOrUnknownOrEmpty::Unknown(unknown_format) => {
430 if unknown_format.to_lowercase() == "uuid" {
431 Ok(("Uuid".to_string(), "uuid".to_string()))
432 } else {
433 Ok(("String".to_string(), unknown_format.clone()))
434 }
435 }
436 _ => Ok(("String".to_string(), "string".to_string())),
437 },
438 SchemaKind::Type(Type::Integer(_)) => Ok(("i64".to_string(), "integer".to_string())),
439 SchemaKind::Type(Type::Number(_)) => Ok(("f64".to_string(), "number".to_string())),
440 SchemaKind::Type(Type::Boolean(_)) => Ok(("bool".to_string(), "boolean".to_string())),
441 SchemaKind::Type(Type::Array(arr)) => {
442 if let Some(items) = &arr.items {
443 let items_ref: &ReferenceOr<Box<Schema>> = items;
444 let (inner_type, format) = match items_ref {
445 ReferenceOr::Item(boxed_schema) => extract_type_and_format(
446 &ReferenceOr::Item((**boxed_schema).clone()),
447 all_schemas,
448 )?,
449 ReferenceOr::Reference { reference } => {
450 let type_name = reference.split('/').next_back().unwrap_or("Unknown");
451
452 if let Some(ReferenceOr::Item(schema)) = all_schemas.get(type_name) {
453 if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
454 (to_pascal_case(type_name), "oneOf".to_string())
455 } else {
456 extract_type_and_format(
457 &ReferenceOr::Reference {
458 reference: reference.clone(),
459 },
460 all_schemas,
461 )?
462 }
463 } else {
464 extract_type_and_format(
465 &ReferenceOr::Reference {
466 reference: reference.clone(),
467 },
468 all_schemas,
469 )?
470 }
471 }
472 };
473 Ok((format!("Vec<{inner_type}>"), format))
474 } else {
475 Ok(("Vec<serde_json::Value>".to_string(), "array".to_string()))
476 }
477 }
478 SchemaKind::Type(Type::Object(_obj)) => {
479 Ok(("serde_json::Value".to_string(), "object".to_string()))
480 }
481 _ => Ok(("serde_json::Value".to_string(), "unknown".to_string())),
482 },
483 }
484}
485
486fn extract_field_info(
488 field_name: &str,
489 schema: &ReferenceOr<Schema>,
490 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
491) -> Result<(FieldInfo, Option<ModelType>)> {
492 let (mut field_type, format) = extract_type_and_format(schema, all_schemas)?;
493
494 let (is_nullable, en) = match schema {
495 ReferenceOr::Reference { reference } => {
496 let is_nullable =
497 if let Some(type_name) = reference.strip_prefix("#/components/schemas/") {
498 all_schemas
499 .get(type_name)
500 .and_then(|s| match s {
501 ReferenceOr::Item(schema) => Some(schema.schema_data.nullable),
502 _ => None,
503 })
504 .unwrap_or(false)
505 } else {
506 false
507 };
508 (is_nullable, None)
509 }
510
511 ReferenceOr::Item(schema) => {
512 let is_nullable = schema.schema_data.nullable;
513
514 let maybe_enum = match &schema.schema_kind {
515 SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
516 let variants: Vec<String> =
517 s.enumeration.iter().filter_map(|v| v.clone()).collect();
518 field_type = to_pascal_case(field_name);
519 Some(ModelType::Enum(EnumModel {
520 name: to_pascal_case(field_name),
521 variants,
522 description: schema.schema_data.description.clone(),
523 custom_attrs: extract_custom_attrs(schema),
524 }))
525 }
526 SchemaKind::Type(Type::Object(_)) => {
527 field_type = "serde_json::Value".to_string();
528 None
529 }
530 _ => None,
531 };
532 (is_nullable, maybe_enum)
533 }
534 };
535
536 Ok((
537 FieldInfo {
538 field_type,
539 format,
540 is_nullable,
541 },
542 en,
543 ))
544}
545
546fn resolve_all_of_fields(
547 _name: &str,
548 all_of: &[ReferenceOr<Schema>],
549 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
550) -> Result<(Vec<Field>, Vec<ModelType>)> {
551 let mut all_fields = Vec::new();
552 let mut models = Vec::new();
553 let mut all_required_fields = HashSet::new();
554
555 for schema_ref in all_of {
556 let schema_to_check = match schema_ref {
557 ReferenceOr::Reference { reference } => reference
558 .strip_prefix("#/components/schemas/")
559 .and_then(|schema_name| all_schemas.get(schema_name)),
560 ReferenceOr::Item(_) => Some(schema_ref),
561 };
562
563 if let Some(ReferenceOr::Item(schema)) = schema_to_check {
564 if let SchemaKind::Type(Type::Object(obj)) = &schema.schema_kind {
565 all_required_fields.extend(obj.required.iter().cloned());
566 }
567 }
568 }
569
570 for schema_ref in all_of {
572 match schema_ref {
573 ReferenceOr::Reference { reference } => {
574 if let Some(schema_name) = reference.strip_prefix("#/components/schemas/") {
575 if let Some(referenced_schema) = all_schemas.get(schema_name) {
576 let (fields, inline_models) =
577 extract_fields_from_schema(referenced_schema, all_schemas)?;
578 all_fields.extend(fields);
579 models.extend(inline_models);
580 }
581 }
582 }
583 ReferenceOr::Item(_schema) => {
584 let (fields, inline_models) = extract_fields_from_schema(schema_ref, all_schemas)?;
585 all_fields.extend(fields);
586 models.extend(inline_models);
587 }
588 }
589 }
590
591 for field in &mut all_fields {
593 if all_required_fields.contains(&field.name) {
594 field.is_required = true;
595 }
596 }
597
598 Ok((all_fields, models))
599}
600
601fn resolve_union_variants(
602 name: &str,
603 schemas: &[ReferenceOr<Schema>],
604 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
605) -> Result<(Vec<UnionVariant>, Vec<ModelType>)> {
606 use std::collections::BTreeSet;
607
608 let mut variants = Vec::new();
609 let mut models = Vec::new();
610 let mut enum_values: BTreeSet<String> = BTreeSet::new();
611 let mut is_all_simple_enum = true;
612
613 for schema_ref in schemas {
614 let resolved = match schema_ref {
615 ReferenceOr::Reference { reference } => reference
616 .strip_prefix("#/components/schemas/")
617 .and_then(|n| all_schemas.get(n)),
618 ReferenceOr::Item(_) => Some(schema_ref),
619 };
620
621 let Some(resolved_schema) = resolved else {
622 is_all_simple_enum = false;
623 continue;
624 };
625
626 match resolved_schema {
627 ReferenceOr::Item(schema) => match &schema.schema_kind {
628 SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
629 enum_values.extend(s.enumeration.iter().filter_map(|v| v.as_ref().cloned()));
630 }
631 SchemaKind::Type(Type::Integer(n)) if !n.enumeration.is_empty() => {
632 enum_values.extend(
633 n.enumeration
634 .iter()
635 .filter_map(|v| v.map(|num| format!("Value{num}"))),
636 );
637 }
638
639 _ => is_all_simple_enum = false,
640 },
641 ReferenceOr::Reference { reference } => {
642 if let Some(n) = reference.strip_prefix("#/components/schemas/") {
643 if let Some(ReferenceOr::Item(inner)) = all_schemas.get(n) {
644 if let SchemaKind::Type(Type::String(s)) = &inner.schema_kind {
645 let values: Vec<String> = s
646 .enumeration
647 .iter()
648 .filter_map(|v| v.as_ref().cloned())
649 .collect();
650 enum_values.extend(values);
651 } else {
652 is_all_simple_enum = false;
653 }
654 }
655 }
656 }
657 }
658 }
659 if is_all_simple_enum && !enum_values.is_empty() {
660 let enum_name = to_pascal_case(name);
661 let enum_model = ModelType::Enum(EnumModel {
662 name: enum_name.clone(),
663 variants: enum_values.iter().map(|v| to_pascal_case(v)).collect(),
664 description: None,
665 custom_attrs: None, });
667
668 return Ok((vec![], vec![enum_model]));
669 }
670
671 for (index, schema_ref) in schemas.iter().enumerate() {
673 match schema_ref {
674 ReferenceOr::Reference { reference } => {
675 if let Some(schema_name) = reference.strip_prefix("#/components/schemas/") {
676 if let Some(referenced_schema) = all_schemas.get(schema_name) {
677 if let ReferenceOr::Item(schema) = referenced_schema {
678 if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
679 variants.push(UnionVariant {
680 name: to_pascal_case(schema_name),
681 fields: vec![],
682 });
683 } else {
684 let (fields, inline_models) =
685 extract_fields_from_schema(referenced_schema, all_schemas)?;
686 variants.push(UnionVariant {
687 name: to_pascal_case(schema_name),
688 fields,
689 });
690 models.extend(inline_models);
691 }
692 }
693 }
694 }
695 }
696 ReferenceOr::Item(_) => {
697 let (fields, inline_models) = extract_fields_from_schema(schema_ref, all_schemas)?;
698 let variant_name = format!("Variant{index}");
699 variants.push(UnionVariant {
700 name: variant_name,
701 fields,
702 });
703 models.extend(inline_models);
704 }
705 }
706 }
707
708 Ok((variants, models))
709}
710
711fn extract_fields_from_schema(
712 schema_ref: &ReferenceOr<Schema>,
713 _all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
714) -> Result<(Vec<Field>, Vec<ModelType>)> {
715 let mut fields = Vec::new();
716 let mut inline_models = Vec::new();
717
718 match schema_ref {
719 ReferenceOr::Reference { .. } => Ok((fields, inline_models)),
720 ReferenceOr::Item(schema) => {
721 match &schema.schema_kind {
722 SchemaKind::Type(Type::Object(obj)) => {
723 for (field_name, field_schema) in &obj.properties {
724 let (field_info, inline_model) = match field_schema {
725 ReferenceOr::Item(boxed_schema) => extract_field_info(
726 field_name,
727 &ReferenceOr::Item((**boxed_schema).clone()),
728 _all_schemas,
729 )?,
730 ReferenceOr::Reference { reference } => extract_field_info(
731 field_name,
732 &ReferenceOr::Reference {
733 reference: reference.clone(),
734 },
735 _all_schemas,
736 )?,
737 };
738
739 let is_nullable = field_info.is_nullable
740 || field_name == "value"
741 || field_name == "default_value";
742
743 let field_type = field_info.field_type.clone();
744
745 let is_required = obj.required.contains(field_name);
746 fields.push(Field {
747 name: field_name.clone(),
748 field_type,
749 format: field_info.format,
750 is_required,
751 is_nullable,
752 });
753 if let Some(inline_model) = inline_model {
754 match &inline_model {
755 ModelType::Struct(m) if m.fields.is_empty() => {}
756 _ => inline_models.push(inline_model),
757 }
758 }
759 }
760 }
761 SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
762 let name = schema
763 .schema_data
764 .title
765 .clone()
766 .unwrap_or_else(|| "AnonymousStringEnum".to_string());
767
768 let enum_model = ModelType::Enum(EnumModel {
769 name,
770 variants: s
771 .enumeration
772 .iter()
773 .filter_map(|v| v.as_ref().map(|s| to_pascal_case(s)))
774 .collect(),
775 description: schema.schema_data.description.clone(),
776 custom_attrs: extract_custom_attrs(schema),
777 });
778
779 inline_models.push(enum_model);
780 }
781 SchemaKind::Type(Type::Integer(n)) if !n.enumeration.is_empty() => {
782 let name = schema
783 .schema_data
784 .title
785 .clone()
786 .unwrap_or_else(|| "AnonymousIntEnum".to_string());
787
788 let enum_model = ModelType::Enum(EnumModel {
789 name,
790 variants: n
791 .enumeration
792 .iter()
793 .filter_map(|v| v.map(|num| format!("Value{num}")))
794 .collect(),
795 description: schema.schema_data.description.clone(),
796 custom_attrs: extract_custom_attrs(schema),
797 });
798
799 inline_models.push(enum_model);
800 }
801
802 _ => {}
803 }
804
805 Ok((fields, inline_models))
806 }
807 }
808}
809
810#[cfg(test)]
811mod tests {
812 use super::*;
813 use serde_json::json;
814
815 #[test]
816 fn test_parse_inline_request_body_generates_model() {
817 let openapi_spec: OpenAPI = serde_json::from_value(json!({
818 "openapi": "3.0.0",
819 "info": { "title": "Test API", "version": "1.0.0" },
820 "paths": {
821 "/items": {
822 "post": {
823 "operationId": "createItem",
824 "requestBody": {
825 "content": {
826 "application/json": {
827 "schema": {
828 "type": "object",
829 "properties": {
830 "name": { "type": "string" },
831 "value": { "type": "integer" }
832 },
833 "required": ["name"]
834 }
835 }
836 }
837 },
838 "responses": { "200": { "description": "OK" } }
839 }
840 }
841 }
842 }))
843 .expect("Failed to deserialize OpenAPI spec");
844
845 let (models, requests, _responses) =
846 parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
847
848 assert_eq!(requests.len(), 1);
850 let request_model = &requests[0];
851 assert_eq!(request_model.name, "CreateItemRequest");
852
853 assert_eq!(request_model.schema, "CreateItemRequestBody");
855
856 let inline_model = models.iter().find(|m| m.name() == "CreateItemRequestBody");
858 assert!(
859 inline_model.is_some(),
860 "Expected a model named 'CreateItemRequestBody' to be generated"
861 );
862
863 if let Some(ModelType::Struct(model)) = inline_model {
864 assert_eq!(model.fields.len(), 2);
865 assert_eq!(model.fields[0].name, "name");
866 assert_eq!(model.fields[0].field_type, "String");
867 assert!(model.fields[0].is_required);
868
869 assert_eq!(model.fields[1].name, "value");
870 assert_eq!(model.fields[1].field_type, "i64");
871 assert!(!model.fields[1].is_required);
872 } else {
873 panic!("Expected a Struct model for CreateItemRequestBody");
874 }
875 }
876
877 #[test]
878 fn test_parse_ref_request_body_works() {
879 let openapi_spec: OpenAPI = serde_json::from_value(json!({
880 "openapi": "3.0.0",
881 "info": { "title": "Test API", "version": "1.0.0" },
882 "components": {
883 "schemas": {
884 "ItemData": {
885 "type": "object",
886 "properties": {
887 "name": { "type": "string" }
888 }
889 }
890 },
891 "requestBodies": {
892 "CreateItem": {
893 "content": {
894 "application/json": {
895 "schema": { "$ref": "#/components/schemas/ItemData" }
896 }
897 }
898 }
899 }
900 },
901 "paths": {
902 "/items": {
903 "post": {
904 "operationId": "createItem",
905 "requestBody": { "$ref": "#/components/requestBodies/CreateItem" },
906 "responses": { "200": { "description": "OK" } }
907 }
908 }
909 }
910 }))
911 .expect("Failed to deserialize OpenAPI spec");
912
913 let (models, requests, _responses) =
914 parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
915
916 assert_eq!(requests.len(), 1);
918 let request_model = &requests[0];
919 assert_eq!(request_model.name, "CreateItemRequest");
920
921 assert_eq!(request_model.schema, "ItemData");
923
924 assert!(models.iter().any(|m| m.name() == "ItemData"));
926 }
927
928 #[test]
929 fn test_parse_no_request_body() {
930 let openapi_spec: OpenAPI = serde_json::from_value(json!({
931 "openapi": "3.0.0",
932 "info": { "title": "Test API", "version": "1.0.0" },
933 "paths": {
934 "/items": {
935 "get": {
936 "operationId": "listItems",
937 "responses": { "200": { "description": "OK" } }
938 }
939 }
940 }
941 }))
942 .expect("Failed to deserialize OpenAPI spec");
943
944 let (_models, requests, _responses) =
945 parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
946
947 assert!(requests.is_empty());
949 }
950
951 #[test]
952 fn test_nullable_reference_field() {
953 let openapi_spec: OpenAPI = serde_json::from_value(json!({
955 "openapi": "3.0.0",
956 "info": { "title": "Test API", "version": "1.0.0" },
957 "paths": {},
958 "components": {
959 "schemas": {
960 "NullableUser": {
961 "type": "object",
962 "nullable": true,
963 "properties": {
964 "name": { "type": "string" }
965 }
966 },
967 "Post": {
968 "type": "object",
969 "properties": {
970 "author": {
971 "$ref": "#/components/schemas/NullableUser"
972 }
973 }
974 }
975 }
976 }
977 }))
978 .expect("Failed to deserialize OpenAPI spec");
979
980 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
981
982 let post_model = models.iter().find(|m| m.name() == "Post");
984 assert!(post_model.is_some(), "Expected Post model to be generated");
985
986 if let Some(ModelType::Struct(post)) = post_model {
987 let author_field = post.fields.iter().find(|f| f.name == "author");
988 assert!(author_field.is_some(), "Expected author field");
989
990 let author = author_field.unwrap();
993 assert!(
994 author.is_nullable,
995 "Expected author field to be nullable (from referenced schema)"
996 );
997 } else {
998 panic!("Expected Post to be a Struct");
999 }
1000 }
1001
1002 #[test]
1003 fn test_allof_required_fields_merge() {
1004 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1005 "openapi": "3.0.0",
1006 "info": { "title": "Test API", "version": "1.0.0" },
1007 "paths": {},
1008 "components": {
1009 "schemas": {
1010 "BaseEntity": {
1011 "type": "object",
1012 "properties": {
1013 "id": { "type": "string" },
1014 "created": { "type": "string" }
1015 },
1016 "required": ["id"]
1017 },
1018 "Person": {
1019 "allOf": [
1020 { "$ref": "#/components/schemas/BaseEntity" },
1021 {
1022 "type": "object",
1023 "properties": {
1024 "name": { "type": "string" },
1025 "age": { "type": "integer" }
1026 },
1027 "required": ["name"]
1028 }
1029 ]
1030 }
1031 }
1032 }
1033 }))
1034 .expect("Failed to deserialize OpenAPI spec");
1035
1036 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1037
1038 let person_model = models.iter().find(|m| m.name() == "Person");
1040 assert!(
1041 person_model.is_some(),
1042 "Expected Person model to be generated"
1043 );
1044
1045 if let Some(ModelType::Composition(person)) = person_model {
1046 let id_field = person.all_fields.iter().find(|f| f.name == "id");
1048 assert!(id_field.is_some(), "Expected id field");
1049 assert!(
1050 id_field.unwrap().is_required,
1051 "Expected id to be required from BaseEntity"
1052 );
1053
1054 let name_field = person.all_fields.iter().find(|f| f.name == "name");
1056 assert!(name_field.is_some(), "Expected name field");
1057 assert!(
1058 name_field.unwrap().is_required,
1059 "Expected name to be required from inline object"
1060 );
1061
1062 let created_field = person.all_fields.iter().find(|f| f.name == "created");
1064 assert!(created_field.is_some(), "Expected created field");
1065 assert!(
1066 !created_field.unwrap().is_required,
1067 "Expected created to be optional"
1068 );
1069
1070 let age_field = person.all_fields.iter().find(|f| f.name == "age");
1071 assert!(age_field.is_some(), "Expected age field");
1072 assert!(
1073 !age_field.unwrap().is_required,
1074 "Expected age to be optional"
1075 );
1076 } else {
1077 panic!("Expected Person to be a Composition");
1078 }
1079 }
1080
1081 #[test]
1082 fn test_x_rust_type_generates_type_alias() {
1083 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1084 "openapi": "3.0.0",
1085 "info": { "title": "Test API", "version": "1.0.0" },
1086 "paths": {},
1087 "components": {
1088 "schemas": {
1089 "User": {
1090 "type": "object",
1091 "x-rust-type": "crate::domain::User",
1092 "description": "Custom domain user type",
1093 "properties": {
1094 "name": { "type": "string" },
1095 "age": { "type": "integer" }
1096 }
1097 }
1098 }
1099 }
1100 }))
1101 .expect("Failed to deserialize OpenAPI spec");
1102
1103 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1104
1105 let user_model = models.iter().find(|m| m.name() == "User");
1107 assert!(user_model.is_some(), "Expected User model");
1108
1109 match user_model.unwrap() {
1110 ModelType::TypeAlias(alias) => {
1111 assert_eq!(alias.name, "User");
1112 assert_eq!(alias.target_type, "crate::domain::User");
1113 assert_eq!(
1114 alias.description,
1115 Some("Custom domain user type".to_string())
1116 );
1117 }
1118 _ => panic!("Expected TypeAlias, got different type"),
1119 }
1120 }
1121
1122 #[test]
1123 fn test_x_rust_type_works_with_enum() {
1124 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1125 "openapi": "3.0.0",
1126 "info": { "title": "Test API", "version": "1.0.0" },
1127 "paths": {},
1128 "components": {
1129 "schemas": {
1130 "Status": {
1131 "type": "string",
1132 "enum": ["active", "inactive"],
1133 "x-rust-type": "crate::domain::Status"
1134 }
1135 }
1136 }
1137 }))
1138 .expect("Failed to deserialize OpenAPI spec");
1139
1140 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1141
1142 let status_model = models.iter().find(|m| m.name() == "Status");
1143 assert!(status_model.is_some(), "Expected Status model");
1144
1145 assert!(
1147 matches!(status_model.unwrap(), ModelType::TypeAlias(_)),
1148 "Expected TypeAlias for enum with x-rust-type"
1149 );
1150 }
1151
1152 #[test]
1153 fn test_x_rust_type_works_with_oneof() {
1154 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1155 "openapi": "3.0.0",
1156 "info": { "title": "Test API", "version": "1.0.0" },
1157 "paths": {},
1158 "components": {
1159 "schemas": {
1160 "Payment": {
1161 "oneOf": [
1162 { "type": "object", "properties": { "card": { "type": "string" } } },
1163 { "type": "object", "properties": { "cash": { "type": "number" } } }
1164 ],
1165 "x-rust-type": "payments::Payment"
1166 }
1167 }
1168 }
1169 }))
1170 .expect("Failed to deserialize OpenAPI spec");
1171
1172 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1173
1174 let payment_model = models.iter().find(|m| m.name() == "Payment");
1175 assert!(payment_model.is_some(), "Expected Payment model");
1176
1177 match payment_model.unwrap() {
1179 ModelType::TypeAlias(alias) => {
1180 assert_eq!(alias.target_type, "payments::Payment");
1181 }
1182 _ => panic!("Expected TypeAlias for oneOf with x-rust-type"),
1183 }
1184 }
1185
1186 #[test]
1187 fn test_x_rust_attrs_on_struct() {
1188 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1189 "openapi": "3.0.0",
1190 "info": { "title": "Test API", "version": "1.0.0" },
1191 "paths": {},
1192 "components": {
1193 "schemas": {
1194 "User": {
1195 "type": "object",
1196 "x-rust-attrs": [
1197 "#[derive(Serialize, Deserialize)]",
1198 "#[serde(rename_all = \"camelCase\")]"
1199 ],
1200 "properties": {
1201 "name": { "type": "string" }
1202 }
1203 }
1204 }
1205 }
1206 }))
1207 .expect("Failed to deserialize OpenAPI spec");
1208
1209 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1210
1211 let user_model = models.iter().find(|m| m.name() == "User");
1212 assert!(user_model.is_some(), "Expected User model");
1213
1214 match user_model.unwrap() {
1215 ModelType::Struct(model) => {
1216 assert!(model.custom_attrs.is_some(), "Expected custom_attrs");
1217 let attrs = model.custom_attrs.as_ref().unwrap();
1218 assert_eq!(attrs.len(), 2);
1219 assert_eq!(attrs[0], "#[derive(Serialize, Deserialize)]");
1220 assert_eq!(attrs[1], "#[serde(rename_all = \"camelCase\")]");
1221 }
1222 _ => panic!("Expected Struct model"),
1223 }
1224 }
1225
1226 #[test]
1227 fn test_x_rust_attrs_on_enum() {
1228 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1229 "openapi": "3.0.0",
1230 "info": { "title": "Test API", "version": "1.0.0" },
1231 "paths": {},
1232 "components": {
1233 "schemas": {
1234 "Status": {
1235 "type": "string",
1236 "enum": ["active", "inactive"],
1237 "x-rust-attrs": ["#[serde(rename_all = \"UPPERCASE\")]"]
1238 }
1239 }
1240 }
1241 }))
1242 .expect("Failed to deserialize OpenAPI spec");
1243
1244 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1245
1246 let status_model = models.iter().find(|m| m.name() == "Status");
1247 assert!(status_model.is_some(), "Expected Status model");
1248
1249 match status_model.unwrap() {
1250 ModelType::Enum(enum_model) => {
1251 assert!(enum_model.custom_attrs.is_some());
1252 let attrs = enum_model.custom_attrs.as_ref().unwrap();
1253 assert_eq!(attrs.len(), 1);
1254 assert_eq!(attrs[0], "#[serde(rename_all = \"UPPERCASE\")]");
1255 }
1256 _ => panic!("Expected Enum model"),
1257 }
1258 }
1259
1260 #[test]
1261 fn test_x_rust_attrs_with_x_rust_type() {
1262 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1263 "openapi": "3.0.0",
1264 "info": { "title": "Test API", "version": "1.0.0" },
1265 "paths": {},
1266 "components": {
1267 "schemas": {
1268 "User": {
1269 "type": "object",
1270 "x-rust-type": "crate::domain::User",
1271 "x-rust-attrs": ["#[cfg_attr(test, derive(Default))]"],
1272 "properties": {
1273 "name": { "type": "string" }
1274 }
1275 }
1276 }
1277 }
1278 }))
1279 .expect("Failed to deserialize OpenAPI spec");
1280
1281 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1282
1283 let user_model = models.iter().find(|m| m.name() == "User");
1284 assert!(user_model.is_some(), "Expected User model");
1285
1286 match user_model.unwrap() {
1288 ModelType::TypeAlias(alias) => {
1289 assert_eq!(alias.target_type, "crate::domain::User");
1290 assert!(alias.custom_attrs.is_some());
1291 let attrs = alias.custom_attrs.as_ref().unwrap();
1292 assert_eq!(attrs.len(), 1);
1293 assert_eq!(attrs[0], "#[cfg_attr(test, derive(Default))]");
1294 }
1295 _ => panic!("Expected TypeAlias with custom attrs"),
1296 }
1297 }
1298
1299 #[test]
1300 fn test_x_rust_attrs_empty_array() {
1301 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1302 "openapi": "3.0.0",
1303 "info": { "title": "Test API", "version": "1.0.0" },
1304 "paths": {},
1305 "components": {
1306 "schemas": {
1307 "User": {
1308 "type": "object",
1309 "x-rust-attrs": [],
1310 "properties": {
1311 "name": { "type": "string" }
1312 }
1313 }
1314 }
1315 }
1316 }))
1317 .expect("Failed to deserialize OpenAPI spec");
1318
1319 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1320
1321 let user_model = models.iter().find(|m| m.name() == "User");
1322 assert!(user_model.is_some());
1323
1324 match user_model.unwrap() {
1325 ModelType::Struct(model) => {
1326 assert!(model.custom_attrs.is_none());
1328 }
1329 _ => panic!("Expected Struct"),
1330 }
1331 }
1332}