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") {
239 if let Some(type_str) = rust_type.as_str() {
240 return Ok(vec![ModelType::TypeAlias(TypeAliasModel {
241 name: to_pascal_case(name),
242 target_type: type_str.to_string(),
243 description: schema.schema_data.description.clone(),
244 custom_attrs: extract_custom_attrs(schema),
245 })]);
246 }
247 }
248
249 match &schema.schema_kind {
250 SchemaKind::Type(Type::Object(obj)) => {
252 let mut fields = Vec::new();
253 let mut inline_models = Vec::new();
254 for (field_name, field_schema) in &obj.properties {
255 let (field_info, inline_model) = match field_schema {
256 ReferenceOr::Item(boxed_schema) => extract_field_info(
257 field_name,
258 &ReferenceOr::Item((**boxed_schema).clone()),
259 all_schemas,
260 )?,
261 ReferenceOr::Reference { reference } => extract_field_info(
262 field_name,
263 &ReferenceOr::Reference {
264 reference: reference.clone(),
265 },
266 all_schemas,
267 )?,
268 };
269 if let Some(inline_model) = inline_model {
270 inline_models.push(inline_model);
271 }
272 let is_required = obj.required.contains(field_name);
273 fields.push(Field {
274 name: field_name.clone(),
275 field_type: field_info.field_type,
276 format: field_info.format,
277 is_required,
278 is_nullable: field_info.is_nullable,
279 });
280 }
281 let mut models = inline_models;
282 if !fields.is_empty() {
283 models.push(ModelType::Struct(Model {
284 name: to_pascal_case(name),
285 fields,
286 custom_attrs: extract_custom_attrs(schema),
287 }));
288 }
289 Ok(models)
290 }
291
292 SchemaKind::AllOf { all_of } => {
294 let (all_fields, inline_models) =
295 resolve_all_of_fields(name, all_of, all_schemas)?;
296 let mut models = inline_models;
297
298 if !all_fields.is_empty() {
299 models.push(ModelType::Composition(CompositionModel {
300 name: to_pascal_case(name),
301 all_fields,
302 custom_attrs: extract_custom_attrs(schema),
303 }));
304 }
305
306 Ok(models)
307 }
308
309 SchemaKind::OneOf { one_of } => {
311 let (variants, inline_models) =
312 resolve_union_variants(name, one_of, all_schemas)?;
313 let mut models = inline_models;
314
315 models.push(ModelType::Union(UnionModel {
316 name: to_pascal_case(name),
317 variants,
318 union_type: UnionType::OneOf,
319 custom_attrs: extract_custom_attrs(schema),
320 }));
321
322 Ok(models)
323 }
324
325 SchemaKind::AnyOf { any_of } => {
327 let (variants, inline_models) =
328 resolve_union_variants(name, any_of, all_schemas)?;
329 let mut models = inline_models;
330
331 models.push(ModelType::Union(UnionModel {
332 name: to_pascal_case(name),
333 variants,
334 union_type: UnionType::AnyOf,
335 custom_attrs: extract_custom_attrs(schema),
336 }));
337
338 Ok(models)
339 }
340
341 SchemaKind::Type(Type::String(string_type)) => {
343 if !string_type.enumeration.is_empty() {
344 let variants: Vec<String> = string_type
345 .enumeration
346 .iter()
347 .filter_map(|value| value.clone())
348 .collect();
349
350 if !variants.is_empty() {
351 let models = vec![ModelType::Enum(EnumModel {
352 name: to_pascal_case(name),
353 variants,
354 description: schema.schema_data.description.clone(),
355 custom_attrs: extract_custom_attrs(schema),
356 })];
357
358 return Ok(models);
359 }
360 }
361 Ok(Vec::new())
362 }
363
364 _ => Ok(Vec::new()),
365 }
366 }
367 }
368}
369
370fn extract_type_and_format(
371 schema: &ReferenceOr<Schema>,
372 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
373) -> Result<(String, String)> {
374 match schema {
375 ReferenceOr::Reference { reference } => {
376 let type_name = reference.split('/').next_back().unwrap_or("Unknown");
377
378 if let Some(ReferenceOr::Item(schema)) = all_schemas.get(type_name) {
379 if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
380 return Ok((to_pascal_case(type_name), "oneOf".to_string()));
381 }
382 }
383 Ok((to_pascal_case(type_name), "reference".to_string()))
384 }
385
386 ReferenceOr::Item(schema) => match &schema.schema_kind {
387 SchemaKind::Type(Type::String(string_type)) => match &string_type.format {
388 VariantOrUnknownOrEmpty::Item(fmt) => match fmt {
389 StringFormat::DateTime => {
390 Ok(("DateTime<Utc>".to_string(), "date-time".to_string()))
391 }
392 StringFormat::Date => Ok(("NaiveDate".to_string(), "date".to_string())),
393 _ => Ok(("String".to_string(), format!("{fmt:?}"))),
394 },
395 VariantOrUnknownOrEmpty::Unknown(unknown_format) => {
396 if unknown_format.to_lowercase() == "uuid" {
397 Ok(("Uuid".to_string(), "uuid".to_string()))
398 } else {
399 Ok(("String".to_string(), unknown_format.clone()))
400 }
401 }
402 _ => Ok(("String".to_string(), "string".to_string())),
403 },
404 SchemaKind::Type(Type::Integer(_)) => Ok(("i64".to_string(), "integer".to_string())),
405 SchemaKind::Type(Type::Number(_)) => Ok(("f64".to_string(), "number".to_string())),
406 SchemaKind::Type(Type::Boolean(_)) => Ok(("bool".to_string(), "boolean".to_string())),
407 SchemaKind::Type(Type::Array(arr)) => {
408 if let Some(items) = &arr.items {
409 let items_ref: &ReferenceOr<Box<Schema>> = items;
410 let (inner_type, format) = match items_ref {
411 ReferenceOr::Item(boxed_schema) => extract_type_and_format(
412 &ReferenceOr::Item((**boxed_schema).clone()),
413 all_schemas,
414 )?,
415 ReferenceOr::Reference { reference } => {
416 let type_name = reference.split('/').next_back().unwrap_or("Unknown");
417
418 if let Some(ReferenceOr::Item(schema)) = all_schemas.get(type_name) {
419 if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
420 (to_pascal_case(type_name), "oneOf".to_string())
421 } else {
422 extract_type_and_format(
423 &ReferenceOr::Reference {
424 reference: reference.clone(),
425 },
426 all_schemas,
427 )?
428 }
429 } else {
430 extract_type_and_format(
431 &ReferenceOr::Reference {
432 reference: reference.clone(),
433 },
434 all_schemas,
435 )?
436 }
437 }
438 };
439 Ok((format!("Vec<{inner_type}>"), format))
440 } else {
441 Ok(("Vec<serde_json::Value>".to_string(), "array".to_string()))
442 }
443 }
444 SchemaKind::Type(Type::Object(_obj)) => {
445 Ok(("serde_json::Value".to_string(), "object".to_string()))
446 }
447 _ => Ok(("serde_json::Value".to_string(), "unknown".to_string())),
448 },
449 }
450}
451
452fn extract_field_info(
454 field_name: &str,
455 schema: &ReferenceOr<Schema>,
456 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
457) -> Result<(FieldInfo, Option<ModelType>)> {
458 let (mut field_type, format) = extract_type_and_format(schema, all_schemas)?;
459
460 let (is_nullable, en) = match schema {
461 ReferenceOr::Reference { reference } => {
462 let is_nullable =
463 if let Some(type_name) = reference.strip_prefix("#/components/schemas/") {
464 all_schemas
465 .get(type_name)
466 .and_then(|s| match s {
467 ReferenceOr::Item(schema) => Some(schema.schema_data.nullable),
468 _ => None,
469 })
470 .unwrap_or(false)
471 } else {
472 false
473 };
474 (is_nullable, None)
475 }
476
477 ReferenceOr::Item(schema) => {
478 let is_nullable = schema.schema_data.nullable;
479
480 let maybe_enum = match &schema.schema_kind {
481 SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
482 let variants: Vec<String> =
483 s.enumeration.iter().filter_map(|v| v.clone()).collect();
484 field_type = to_pascal_case(field_name);
485 Some(ModelType::Enum(EnumModel {
486 name: to_pascal_case(field_name),
487 variants,
488 description: schema.schema_data.description.clone(),
489 custom_attrs: extract_custom_attrs(schema),
490 }))
491 }
492 SchemaKind::Type(Type::Object(_)) => {
493 field_type = "serde_json::Value".to_string();
494 None
495 }
496 _ => None,
497 };
498 (is_nullable, maybe_enum)
499 }
500 };
501
502 Ok((
503 FieldInfo {
504 field_type,
505 format,
506 is_nullable,
507 },
508 en,
509 ))
510}
511
512fn resolve_all_of_fields(
513 _name: &str,
514 all_of: &[ReferenceOr<Schema>],
515 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
516) -> Result<(Vec<Field>, Vec<ModelType>)> {
517 let mut all_fields = Vec::new();
518 let mut models = Vec::new();
519 let mut all_required_fields = HashSet::new();
520
521 for schema_ref in all_of {
522 let schema_to_check = match schema_ref {
523 ReferenceOr::Reference { reference } => reference
524 .strip_prefix("#/components/schemas/")
525 .and_then(|schema_name| all_schemas.get(schema_name)),
526 ReferenceOr::Item(_) => Some(schema_ref),
527 };
528
529 if let Some(ReferenceOr::Item(schema)) = schema_to_check {
530 if let SchemaKind::Type(Type::Object(obj)) = &schema.schema_kind {
531 all_required_fields.extend(obj.required.iter().cloned());
532 }
533 }
534 }
535
536 for schema_ref in all_of {
538 match schema_ref {
539 ReferenceOr::Reference { reference } => {
540 if let Some(schema_name) = reference.strip_prefix("#/components/schemas/") {
541 if let Some(referenced_schema) = all_schemas.get(schema_name) {
542 let (fields, inline_models) =
543 extract_fields_from_schema(referenced_schema, all_schemas)?;
544 all_fields.extend(fields);
545 models.extend(inline_models);
546 }
547 }
548 }
549 ReferenceOr::Item(_schema) => {
550 let (fields, inline_models) = extract_fields_from_schema(schema_ref, all_schemas)?;
551 all_fields.extend(fields);
552 models.extend(inline_models);
553 }
554 }
555 }
556
557 for field in &mut all_fields {
559 if all_required_fields.contains(&field.name) {
560 field.is_required = true;
561 }
562 }
563
564 Ok((all_fields, models))
565}
566
567fn resolve_union_variants(
568 name: &str,
569 schemas: &[ReferenceOr<Schema>],
570 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
571) -> Result<(Vec<UnionVariant>, Vec<ModelType>)> {
572 use std::collections::BTreeSet;
573
574 let mut variants = Vec::new();
575 let mut models = Vec::new();
576 let mut enum_values: BTreeSet<String> = BTreeSet::new();
577 let mut is_all_simple_enum = true;
578
579 for schema_ref in schemas {
580 let resolved = match schema_ref {
581 ReferenceOr::Reference { reference } => reference
582 .strip_prefix("#/components/schemas/")
583 .and_then(|n| all_schemas.get(n)),
584 ReferenceOr::Item(_) => Some(schema_ref),
585 };
586
587 let Some(resolved_schema) = resolved else {
588 is_all_simple_enum = false;
589 continue;
590 };
591
592 match resolved_schema {
593 ReferenceOr::Item(schema) => match &schema.schema_kind {
594 SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
595 enum_values.extend(s.enumeration.iter().filter_map(|v| v.as_ref().cloned()));
596 }
597 SchemaKind::Type(Type::Integer(n)) if !n.enumeration.is_empty() => {
598 enum_values.extend(
599 n.enumeration
600 .iter()
601 .filter_map(|v| v.map(|num| format!("Value{num}"))),
602 );
603 }
604
605 _ => is_all_simple_enum = false,
606 },
607 ReferenceOr::Reference { reference } => {
608 if let Some(n) = reference.strip_prefix("#/components/schemas/") {
609 if let Some(ReferenceOr::Item(inner)) = all_schemas.get(n) {
610 if let SchemaKind::Type(Type::String(s)) = &inner.schema_kind {
611 let values: Vec<String> = s
612 .enumeration
613 .iter()
614 .filter_map(|v| v.as_ref().cloned())
615 .collect();
616 enum_values.extend(values);
617 } else {
618 is_all_simple_enum = false;
619 }
620 }
621 }
622 }
623 }
624 }
625 if is_all_simple_enum && !enum_values.is_empty() {
626 let enum_name = to_pascal_case(name);
627 let enum_model = ModelType::Enum(EnumModel {
628 name: enum_name.clone(),
629 variants: enum_values.iter().map(|v| to_pascal_case(v)).collect(),
630 description: None,
631 custom_attrs: None, });
633
634 return Ok((vec![], vec![enum_model]));
635 }
636
637 for (index, schema_ref) in schemas.iter().enumerate() {
639 match schema_ref {
640 ReferenceOr::Reference { reference } => {
641 if let Some(schema_name) = reference.strip_prefix("#/components/schemas/") {
642 if let Some(referenced_schema) = all_schemas.get(schema_name) {
643 if let ReferenceOr::Item(schema) = referenced_schema {
644 if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
645 variants.push(UnionVariant {
646 name: to_pascal_case(schema_name),
647 fields: vec![],
648 });
649 } else {
650 let (fields, inline_models) =
651 extract_fields_from_schema(referenced_schema, all_schemas)?;
652 variants.push(UnionVariant {
653 name: to_pascal_case(schema_name),
654 fields,
655 });
656 models.extend(inline_models);
657 }
658 }
659 }
660 }
661 }
662 ReferenceOr::Item(_) => {
663 let (fields, inline_models) = extract_fields_from_schema(schema_ref, all_schemas)?;
664 let variant_name = format!("Variant{index}");
665 variants.push(UnionVariant {
666 name: variant_name,
667 fields,
668 });
669 models.extend(inline_models);
670 }
671 }
672 }
673
674 Ok((variants, models))
675}
676
677fn extract_fields_from_schema(
678 schema_ref: &ReferenceOr<Schema>,
679 _all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
680) -> Result<(Vec<Field>, Vec<ModelType>)> {
681 let mut fields = Vec::new();
682 let mut inline_models = Vec::new();
683
684 match schema_ref {
685 ReferenceOr::Reference { .. } => Ok((fields, inline_models)),
686 ReferenceOr::Item(schema) => {
687 match &schema.schema_kind {
688 SchemaKind::Type(Type::Object(obj)) => {
689 for (field_name, field_schema) in &obj.properties {
690 let (field_info, inline_model) = match field_schema {
691 ReferenceOr::Item(boxed_schema) => extract_field_info(
692 field_name,
693 &ReferenceOr::Item((**boxed_schema).clone()),
694 _all_schemas,
695 )?,
696 ReferenceOr::Reference { reference } => extract_field_info(
697 field_name,
698 &ReferenceOr::Reference {
699 reference: reference.clone(),
700 },
701 _all_schemas,
702 )?,
703 };
704
705 let is_nullable = field_info.is_nullable
706 || field_name == "value"
707 || field_name == "default_value";
708
709 let field_type = field_info.field_type.clone();
710
711 let is_required = obj.required.contains(field_name);
712 fields.push(Field {
713 name: field_name.clone(),
714 field_type,
715 format: field_info.format,
716 is_required,
717 is_nullable,
718 });
719 if let Some(inline_model) = inline_model {
720 match &inline_model {
721 ModelType::Struct(m) if m.fields.is_empty() => {}
722 _ => inline_models.push(inline_model),
723 }
724 }
725 }
726 }
727 SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
728 let name = schema
729 .schema_data
730 .title
731 .clone()
732 .unwrap_or_else(|| "AnonymousStringEnum".to_string());
733
734 let enum_model = ModelType::Enum(EnumModel {
735 name,
736 variants: s
737 .enumeration
738 .iter()
739 .filter_map(|v| v.as_ref().map(|s| to_pascal_case(s)))
740 .collect(),
741 description: schema.schema_data.description.clone(),
742 custom_attrs: extract_custom_attrs(schema),
743 });
744
745 inline_models.push(enum_model);
746 }
747 SchemaKind::Type(Type::Integer(n)) if !n.enumeration.is_empty() => {
748 let name = schema
749 .schema_data
750 .title
751 .clone()
752 .unwrap_or_else(|| "AnonymousIntEnum".to_string());
753
754 let enum_model = ModelType::Enum(EnumModel {
755 name,
756 variants: n
757 .enumeration
758 .iter()
759 .filter_map(|v| v.map(|num| format!("Value{num}")))
760 .collect(),
761 description: schema.schema_data.description.clone(),
762 custom_attrs: extract_custom_attrs(schema),
763 });
764
765 inline_models.push(enum_model);
766 }
767
768 _ => {}
769 }
770
771 Ok((fields, inline_models))
772 }
773 }
774}
775
776#[cfg(test)]
777mod tests {
778 use super::*;
779 use serde_json::json;
780
781 #[test]
782 fn test_parse_inline_request_body_generates_model() {
783 let openapi_spec: OpenAPI = serde_json::from_value(json!({
784 "openapi": "3.0.0",
785 "info": { "title": "Test API", "version": "1.0.0" },
786 "paths": {
787 "/items": {
788 "post": {
789 "operationId": "createItem",
790 "requestBody": {
791 "content": {
792 "application/json": {
793 "schema": {
794 "type": "object",
795 "properties": {
796 "name": { "type": "string" },
797 "value": { "type": "integer" }
798 },
799 "required": ["name"]
800 }
801 }
802 }
803 },
804 "responses": { "200": { "description": "OK" } }
805 }
806 }
807 }
808 }))
809 .expect("Failed to deserialize OpenAPI spec");
810
811 let (models, requests, _responses) =
812 parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
813
814 assert_eq!(requests.len(), 1);
816 let request_model = &requests[0];
817 assert_eq!(request_model.name, "CreateItemRequest");
818
819 assert_eq!(request_model.schema, "CreateItemRequestBody");
821
822 let inline_model = models.iter().find(|m| m.name() == "CreateItemRequestBody");
824 assert!(
825 inline_model.is_some(),
826 "Expected a model named 'CreateItemRequestBody' to be generated"
827 );
828
829 if let Some(ModelType::Struct(model)) = inline_model {
830 assert_eq!(model.fields.len(), 2);
831 assert_eq!(model.fields[0].name, "name");
832 assert_eq!(model.fields[0].field_type, "String");
833 assert!(model.fields[0].is_required);
834
835 assert_eq!(model.fields[1].name, "value");
836 assert_eq!(model.fields[1].field_type, "i64");
837 assert!(!model.fields[1].is_required);
838 } else {
839 panic!("Expected a Struct model for CreateItemRequestBody");
840 }
841 }
842
843 #[test]
844 fn test_parse_ref_request_body_works() {
845 let openapi_spec: OpenAPI = serde_json::from_value(json!({
846 "openapi": "3.0.0",
847 "info": { "title": "Test API", "version": "1.0.0" },
848 "components": {
849 "schemas": {
850 "ItemData": {
851 "type": "object",
852 "properties": {
853 "name": { "type": "string" }
854 }
855 }
856 },
857 "requestBodies": {
858 "CreateItem": {
859 "content": {
860 "application/json": {
861 "schema": { "$ref": "#/components/schemas/ItemData" }
862 }
863 }
864 }
865 }
866 },
867 "paths": {
868 "/items": {
869 "post": {
870 "operationId": "createItem",
871 "requestBody": { "$ref": "#/components/requestBodies/CreateItem" },
872 "responses": { "200": { "description": "OK" } }
873 }
874 }
875 }
876 }))
877 .expect("Failed to deserialize OpenAPI spec");
878
879 let (models, requests, _responses) =
880 parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
881
882 assert_eq!(requests.len(), 1);
884 let request_model = &requests[0];
885 assert_eq!(request_model.name, "CreateItemRequest");
886
887 assert_eq!(request_model.schema, "ItemData");
889
890 assert!(models.iter().any(|m| m.name() == "ItemData"));
892 }
893
894 #[test]
895 fn test_parse_no_request_body() {
896 let openapi_spec: OpenAPI = serde_json::from_value(json!({
897 "openapi": "3.0.0",
898 "info": { "title": "Test API", "version": "1.0.0" },
899 "paths": {
900 "/items": {
901 "get": {
902 "operationId": "listItems",
903 "responses": { "200": { "description": "OK" } }
904 }
905 }
906 }
907 }))
908 .expect("Failed to deserialize OpenAPI spec");
909
910 let (_models, requests, _responses) =
911 parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
912
913 assert!(requests.is_empty());
915 }
916
917 #[test]
918 fn test_nullable_reference_field() {
919 let openapi_spec: OpenAPI = serde_json::from_value(json!({
921 "openapi": "3.0.0",
922 "info": { "title": "Test API", "version": "1.0.0" },
923 "paths": {},
924 "components": {
925 "schemas": {
926 "NullableUser": {
927 "type": "object",
928 "nullable": true,
929 "properties": {
930 "name": { "type": "string" }
931 }
932 },
933 "Post": {
934 "type": "object",
935 "properties": {
936 "author": {
937 "$ref": "#/components/schemas/NullableUser"
938 }
939 }
940 }
941 }
942 }
943 }))
944 .expect("Failed to deserialize OpenAPI spec");
945
946 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
947
948 let post_model = models.iter().find(|m| m.name() == "Post");
950 assert!(post_model.is_some(), "Expected Post model to be generated");
951
952 if let Some(ModelType::Struct(post)) = post_model {
953 let author_field = post.fields.iter().find(|f| f.name == "author");
954 assert!(author_field.is_some(), "Expected author field");
955
956 let author = author_field.unwrap();
959 assert!(
960 author.is_nullable,
961 "Expected author field to be nullable (from referenced schema)"
962 );
963 } else {
964 panic!("Expected Post to be a Struct");
965 }
966 }
967
968 #[test]
969 fn test_allof_required_fields_merge() {
970 let openapi_spec: OpenAPI = serde_json::from_value(json!({
971 "openapi": "3.0.0",
972 "info": { "title": "Test API", "version": "1.0.0" },
973 "paths": {},
974 "components": {
975 "schemas": {
976 "BaseEntity": {
977 "type": "object",
978 "properties": {
979 "id": { "type": "string" },
980 "created": { "type": "string" }
981 },
982 "required": ["id"]
983 },
984 "Person": {
985 "allOf": [
986 { "$ref": "#/components/schemas/BaseEntity" },
987 {
988 "type": "object",
989 "properties": {
990 "name": { "type": "string" },
991 "age": { "type": "integer" }
992 },
993 "required": ["name"]
994 }
995 ]
996 }
997 }
998 }
999 }))
1000 .expect("Failed to deserialize OpenAPI spec");
1001
1002 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1003
1004 let person_model = models.iter().find(|m| m.name() == "Person");
1006 assert!(
1007 person_model.is_some(),
1008 "Expected Person model to be generated"
1009 );
1010
1011 if let Some(ModelType::Composition(person)) = person_model {
1012 let id_field = person.all_fields.iter().find(|f| f.name == "id");
1014 assert!(id_field.is_some(), "Expected id field");
1015 assert!(
1016 id_field.unwrap().is_required,
1017 "Expected id to be required from BaseEntity"
1018 );
1019
1020 let name_field = person.all_fields.iter().find(|f| f.name == "name");
1022 assert!(name_field.is_some(), "Expected name field");
1023 assert!(
1024 name_field.unwrap().is_required,
1025 "Expected name to be required from inline object"
1026 );
1027
1028 let created_field = person.all_fields.iter().find(|f| f.name == "created");
1030 assert!(created_field.is_some(), "Expected created field");
1031 assert!(
1032 !created_field.unwrap().is_required,
1033 "Expected created to be optional"
1034 );
1035
1036 let age_field = person.all_fields.iter().find(|f| f.name == "age");
1037 assert!(age_field.is_some(), "Expected age field");
1038 assert!(
1039 !age_field.unwrap().is_required,
1040 "Expected age to be optional"
1041 );
1042 } else {
1043 panic!("Expected Person to be a Composition");
1044 }
1045 }
1046
1047 #[test]
1048 fn test_x_rust_type_generates_type_alias() {
1049 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1050 "openapi": "3.0.0",
1051 "info": { "title": "Test API", "version": "1.0.0" },
1052 "paths": {},
1053 "components": {
1054 "schemas": {
1055 "User": {
1056 "type": "object",
1057 "x-rust-type": "crate::domain::User",
1058 "description": "Custom domain user type",
1059 "properties": {
1060 "name": { "type": "string" },
1061 "age": { "type": "integer" }
1062 }
1063 }
1064 }
1065 }
1066 }))
1067 .expect("Failed to deserialize OpenAPI spec");
1068
1069 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1070
1071 let user_model = models.iter().find(|m| m.name() == "User");
1073 assert!(user_model.is_some(), "Expected User model");
1074
1075 match user_model.unwrap() {
1076 ModelType::TypeAlias(alias) => {
1077 assert_eq!(alias.name, "User");
1078 assert_eq!(alias.target_type, "crate::domain::User");
1079 assert_eq!(
1080 alias.description,
1081 Some("Custom domain user type".to_string())
1082 );
1083 }
1084 _ => panic!("Expected TypeAlias, got different type"),
1085 }
1086 }
1087
1088 #[test]
1089 fn test_x_rust_type_works_with_enum() {
1090 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1091 "openapi": "3.0.0",
1092 "info": { "title": "Test API", "version": "1.0.0" },
1093 "paths": {},
1094 "components": {
1095 "schemas": {
1096 "Status": {
1097 "type": "string",
1098 "enum": ["active", "inactive"],
1099 "x-rust-type": "crate::domain::Status"
1100 }
1101 }
1102 }
1103 }))
1104 .expect("Failed to deserialize OpenAPI spec");
1105
1106 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1107
1108 let status_model = models.iter().find(|m| m.name() == "Status");
1109 assert!(status_model.is_some(), "Expected Status model");
1110
1111 assert!(
1113 matches!(status_model.unwrap(), ModelType::TypeAlias(_)),
1114 "Expected TypeAlias for enum with x-rust-type"
1115 );
1116 }
1117
1118 #[test]
1119 fn test_x_rust_type_works_with_oneof() {
1120 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1121 "openapi": "3.0.0",
1122 "info": { "title": "Test API", "version": "1.0.0" },
1123 "paths": {},
1124 "components": {
1125 "schemas": {
1126 "Payment": {
1127 "oneOf": [
1128 { "type": "object", "properties": { "card": { "type": "string" } } },
1129 { "type": "object", "properties": { "cash": { "type": "number" } } }
1130 ],
1131 "x-rust-type": "payments::Payment"
1132 }
1133 }
1134 }
1135 }))
1136 .expect("Failed to deserialize OpenAPI spec");
1137
1138 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1139
1140 let payment_model = models.iter().find(|m| m.name() == "Payment");
1141 assert!(payment_model.is_some(), "Expected Payment model");
1142
1143 match payment_model.unwrap() {
1145 ModelType::TypeAlias(alias) => {
1146 assert_eq!(alias.target_type, "payments::Payment");
1147 }
1148 _ => panic!("Expected TypeAlias for oneOf with x-rust-type"),
1149 }
1150 }
1151
1152 #[test]
1153 fn test_x_rust_attrs_on_struct() {
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 "User": {
1161 "type": "object",
1162 "x-rust-attrs": [
1163 "#[derive(Serialize, Deserialize)]",
1164 "#[serde(rename_all = \"camelCase\")]"
1165 ],
1166 "properties": {
1167 "name": { "type": "string" }
1168 }
1169 }
1170 }
1171 }
1172 }))
1173 .expect("Failed to deserialize OpenAPI spec");
1174
1175 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1176
1177 let user_model = models.iter().find(|m| m.name() == "User");
1178 assert!(user_model.is_some(), "Expected User model");
1179
1180 match user_model.unwrap() {
1181 ModelType::Struct(model) => {
1182 assert!(model.custom_attrs.is_some(), "Expected custom_attrs");
1183 let attrs = model.custom_attrs.as_ref().unwrap();
1184 assert_eq!(attrs.len(), 2);
1185 assert_eq!(attrs[0], "#[derive(Serialize, Deserialize)]");
1186 assert_eq!(attrs[1], "#[serde(rename_all = \"camelCase\")]");
1187 }
1188 _ => panic!("Expected Struct model"),
1189 }
1190 }
1191
1192 #[test]
1193 fn test_x_rust_attrs_on_enum() {
1194 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1195 "openapi": "3.0.0",
1196 "info": { "title": "Test API", "version": "1.0.0" },
1197 "paths": {},
1198 "components": {
1199 "schemas": {
1200 "Status": {
1201 "type": "string",
1202 "enum": ["active", "inactive"],
1203 "x-rust-attrs": ["#[serde(rename_all = \"UPPERCASE\")]"]
1204 }
1205 }
1206 }
1207 }))
1208 .expect("Failed to deserialize OpenAPI spec");
1209
1210 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1211
1212 let status_model = models.iter().find(|m| m.name() == "Status");
1213 assert!(status_model.is_some(), "Expected Status model");
1214
1215 match status_model.unwrap() {
1216 ModelType::Enum(enum_model) => {
1217 assert!(enum_model.custom_attrs.is_some());
1218 let attrs = enum_model.custom_attrs.as_ref().unwrap();
1219 assert_eq!(attrs.len(), 1);
1220 assert_eq!(attrs[0], "#[serde(rename_all = \"UPPERCASE\")]");
1221 }
1222 _ => panic!("Expected Enum model"),
1223 }
1224 }
1225
1226 #[test]
1227 fn test_x_rust_attrs_with_x_rust_type() {
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 "User": {
1235 "type": "object",
1236 "x-rust-type": "crate::domain::User",
1237 "x-rust-attrs": ["#[cfg_attr(test, derive(Default))]"],
1238 "properties": {
1239 "name": { "type": "string" }
1240 }
1241 }
1242 }
1243 }
1244 }))
1245 .expect("Failed to deserialize OpenAPI spec");
1246
1247 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1248
1249 let user_model = models.iter().find(|m| m.name() == "User");
1250 assert!(user_model.is_some(), "Expected User model");
1251
1252 match user_model.unwrap() {
1254 ModelType::TypeAlias(alias) => {
1255 assert_eq!(alias.target_type, "crate::domain::User");
1256 assert!(alias.custom_attrs.is_some());
1257 let attrs = alias.custom_attrs.as_ref().unwrap();
1258 assert_eq!(attrs.len(), 1);
1259 assert_eq!(attrs[0], "#[cfg_attr(test, derive(Default))]");
1260 }
1261 _ => panic!("Expected TypeAlias with custom attrs"),
1262 }
1263 }
1264
1265 #[test]
1266 fn test_x_rust_attrs_empty_array() {
1267 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1268 "openapi": "3.0.0",
1269 "info": { "title": "Test API", "version": "1.0.0" },
1270 "paths": {},
1271 "components": {
1272 "schemas": {
1273 "User": {
1274 "type": "object",
1275 "x-rust-attrs": [],
1276 "properties": {
1277 "name": { "type": "string" }
1278 }
1279 }
1280 }
1281 }
1282 }))
1283 .expect("Failed to deserialize OpenAPI spec");
1284
1285 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1286
1287 let user_model = models.iter().find(|m| m.name() == "User");
1288 assert!(user_model.is_some());
1289
1290 match user_model.unwrap() {
1291 ModelType::Struct(model) => {
1292 assert!(model.custom_attrs.is_none());
1294 }
1295 _ => panic!("Expected Struct"),
1296 }
1297 }
1298}