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 AdditionalProperties, OpenAPI, ReferenceOr, Schema, SchemaKind, StringFormat, Type,
11 VariantOrUnknownOrEmpty,
12};
13use std::collections::{HashMap, HashSet};
14
15const X_RUST_TYPE: &str = "x-rust-type";
16const X_RUST_ATTRS: &str = "x-rust-attrs";
17
18#[derive(Debug)]
20struct FieldInfo {
21 field_type: String,
22 format: String,
23 is_nullable: bool,
24 is_array_ref: bool,
25 description: Option<String>,
26}
27
28pub(crate) fn to_pascal_case(input: &str) -> String {
31 input
32 .split(&['-', '_'][..])
33 .filter(|s| !s.is_empty())
34 .map(|s| {
35 let mut chars = s.chars();
36 match chars.next() {
37 Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
38 None => String::new(),
39 }
40 })
41 .collect::<String>()
42}
43
44fn extract_custom_attrs(schema: &Schema) -> Option<Vec<String>> {
46 schema
47 .schema_data
48 .extensions
49 .get(X_RUST_ATTRS)
50 .and_then(|value| {
51 if let Some(arr) = value.as_array() {
52 let attrs: Vec<String> = arr
53 .iter()
54 .filter_map(|v| v.as_str().map(|s| s.to_string()))
55 .collect();
56 if attrs.is_empty() {
57 None
58 } else {
59 Some(attrs)
60 }
61 } else {
62 tracing::warn!(
63 "x-rust-attrs should be an array of strings, got: {:?}",
64 value
65 );
66 None
67 }
68 })
69}
70
71pub fn parse_openapi(
72 openapi: &OpenAPI,
73) -> Result<(Vec<ModelType>, Vec<RequestModel>, Vec<ResponseModel>)> {
74 let mut models = Vec::new();
75 let mut requests = Vec::new();
76 let mut responses = Vec::new();
77
78 let mut added_models = HashSet::new();
79
80 let empty_schemas = IndexMap::new();
81 let empty_request_bodies = IndexMap::new();
82
83 let (schemas, request_bodies) = if let Some(components) = &openapi.components {
84 (&components.schemas, &components.request_bodies)
85 } else {
86 (&empty_schemas, &empty_request_bodies)
87 };
88
89 if let Some(components) = &openapi.components {
91 for (name, schema) in &components.schemas {
92 let model_types = parse_schema_to_model_type(name, schema, &components.schemas)?;
93 for model_type in model_types {
94 if added_models.insert(model_type.name().to_string()) {
95 models.push(model_type);
96 }
97 }
98 }
99
100 for (name, request_body_ref) in &components.request_bodies {
102 if let ReferenceOr::Item(request_body) = request_body_ref {
103 for media_type in request_body.content.values() {
104 if let Some(schema) = &media_type.schema {
105 let model_types =
106 parse_schema_to_model_type(name, schema, &components.schemas)?;
107 for model_type in model_types {
108 if added_models.insert(model_type.name().to_string()) {
109 models.push(model_type);
110 }
111 }
112 }
113 }
114 }
115 }
116 }
117
118 for (_path, path_item) in openapi.paths.iter() {
120 let path_item = match path_item {
121 ReferenceOr::Item(item) => item,
122 ReferenceOr::Reference { .. } => continue,
123 };
124
125 let operations = [
126 &path_item.get,
127 &path_item.post,
128 &path_item.put,
129 &path_item.delete,
130 &path_item.patch,
131 ];
132
133 for op in operations.iter().filter_map(|o| o.as_ref()) {
134 let inline_models =
135 process_operation(op, &mut requests, &mut responses, schemas, request_bodies)?;
136 for model_type in inline_models {
137 if added_models.insert(model_type.name().to_string()) {
138 models.push(model_type);
139 }
140 }
141 }
142 }
143
144 Ok((models, requests, responses))
145}
146
147fn process_operation(
148 operation: &openapiv3::Operation,
149 requests: &mut Vec<RequestModel>,
150 responses: &mut Vec<ResponseModel>,
151 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
152 request_bodies: &IndexMap<String, ReferenceOr<openapiv3::RequestBody>>,
153) -> Result<Vec<ModelType>> {
154 let mut inline_models = Vec::new();
155
156 if let Some(request_body_ref) = &operation.request_body {
158 let (request_body_data, is_inline) = match request_body_ref {
159 ReferenceOr::Item(request_body) => (Some((request_body, request_body.required)), true),
160 ReferenceOr::Reference { reference } => {
161 if let Some(rb_name) = reference.strip_prefix("#/components/requestBodies/") {
162 (
163 request_bodies.get(rb_name).and_then(|rb_ref| match rb_ref {
164 ReferenceOr::Item(rb) => Some((rb, false)),
165 ReferenceOr::Reference { .. } => None,
166 }),
167 false,
168 )
169 } else {
170 (None, false)
171 }
172 }
173 };
174
175 if let Some((request_body, is_required)) = request_body_data {
176 for (content_type, media_type) in &request_body.content {
177 if let Some(schema) = &media_type.schema {
178 let operation_name =
179 to_pascal_case(operation.operation_id.as_deref().unwrap_or("Unknown"));
180
181 let schema_type = if is_inline {
182 if let ReferenceOr::Item(schema_item) = schema {
183 if matches!(schema_item.schema_kind, SchemaKind::Type(Type::Object(_)))
184 {
185 let model_name = format!("{operation_name}RequestBody");
186 let model_types =
187 parse_schema_to_model_type(&model_name, schema, all_schemas)?;
188 inline_models.extend(model_types);
189 model_name
190 } else {
191 extract_type_and_format(schema, all_schemas)?.0
192 }
193 } else {
194 extract_type_and_format(schema, all_schemas)?.0
195 }
196 } else {
197 extract_type_and_format(schema, all_schemas)?.0
198 };
199
200 let request = RequestModel {
201 name: format!("{operation_name}Request"),
202 content_type: content_type.clone(),
203 schema: schema_type,
204 is_required,
205 };
206 requests.push(request);
207 }
208 }
209 }
210 }
211
212 for (status, response_ref) in operation.responses.responses.iter() {
214 if let ReferenceOr::Item(response) = response_ref {
215 for (content_type, media_type) in &response.content {
216 if let Some(schema) = &media_type.schema {
217 let response = ResponseModel {
218 name: format!(
219 "{}Response",
220 to_pascal_case(operation.operation_id.as_deref().unwrap_or("Unknown"))
221 ),
222 status_code: status.to_string(),
223 content_type: content_type.clone(),
224 schema: extract_type_and_format(schema, all_schemas)?.0,
225 description: Some(response.description.clone()),
226 };
227 responses.push(response);
228 }
229 }
230 }
231 }
232 Ok(inline_models)
233}
234
235fn parse_schema_to_model_type(
236 name: &str,
237 schema: &ReferenceOr<Schema>,
238 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
239) -> Result<Vec<ModelType>> {
240 match schema {
241 ReferenceOr::Reference { .. } => Ok(Vec::new()),
242 ReferenceOr::Item(schema) => {
243 if let Some(rust_type) = schema.schema_data.extensions.get(X_RUST_TYPE) {
244 if let Some(type_str) = rust_type.as_str() {
245 return Ok(vec![ModelType::TypeAlias(TypeAliasModel {
246 name: to_pascal_case(name),
247 target_type: type_str.to_string(),
248 description: schema.schema_data.description.clone(),
249 custom_attrs: extract_custom_attrs(schema),
250 })]);
251 }
252 }
253
254 match &schema.schema_kind {
255 SchemaKind::Type(Type::Object(obj)) => {
257 if obj.properties.is_empty() && obj.additional_properties.is_some() {
259 let hashmap_type = match &obj.additional_properties {
260 Some(additional_props) => match additional_props {
261 openapiv3::AdditionalProperties::Any(_) => {
262 "std::collections::HashMap<String, serde_json::Value>"
263 .to_string()
264 }
265 openapiv3::AdditionalProperties::Schema(schema_ref) => {
266 let (inner_type, _) =
267 extract_type_and_format(schema_ref, all_schemas)?;
268 format!("std::collections::HashMap<String, {inner_type}>")
269 }
270 },
271 None => {
272 "std::collections::HashMap<String, serde_json::Value>".to_string()
273 }
274 };
275 return Ok(vec![ModelType::TypeAlias(TypeAliasModel {
276 name: to_pascal_case(name),
277 target_type: hashmap_type,
278 description: schema.schema_data.description.clone(),
279 custom_attrs: extract_custom_attrs(schema),
280 })]);
281 }
282
283 let mut fields = Vec::new();
284 let mut inline_models = Vec::new();
285
286 for (field_name, field_schema) in &obj.properties {
288 if let ReferenceOr::Item(boxed_schema) = field_schema {
289 if matches!(boxed_schema.schema_kind, SchemaKind::Type(Type::Object(_)))
290 {
291 let struct_name = to_pascal_case(field_name);
292 let wrapped_schema = ReferenceOr::Item((**boxed_schema).clone());
293 let nested_models = parse_schema_to_model_type(
294 &struct_name,
295 &wrapped_schema,
296 all_schemas,
297 )?;
298 inline_models.extend(nested_models);
299 }
300 }
301
302 let (field_info, inline_model) = match field_schema {
303 ReferenceOr::Item(boxed_schema) => extract_field_info(
304 field_name,
305 &ReferenceOr::Item((**boxed_schema).clone()),
306 all_schemas,
307 )?,
308 ReferenceOr::Reference { reference } => extract_field_info(
309 field_name,
310 &ReferenceOr::Reference {
311 reference: reference.clone(),
312 },
313 all_schemas,
314 )?,
315 };
316 if let Some(inline_model) = inline_model {
317 inline_models.push(inline_model);
318 }
319 let is_required = obj.required.contains(field_name);
320 fields.push(Field {
321 name: field_name.clone(),
322 field_type: field_info.field_type,
323 format: field_info.format,
324 is_required,
325 is_array_ref: field_info.is_array_ref,
326 is_nullable: field_info.is_nullable,
327 description: field_info.description,
328 });
329 }
330
331 let mut models = inline_models;
332 if obj.properties.is_empty() && obj.additional_properties.is_none() {
333 models.push(ModelType::Struct(Model {
334 name: to_pascal_case(name),
335 fields: vec![],
336 custom_attrs: extract_custom_attrs(schema),
337 description: schema.schema_data.description.clone(),
338 }));
339 } else if !fields.is_empty() {
340 models.push(ModelType::Struct(Model {
341 name: to_pascal_case(name),
342 fields,
343 custom_attrs: extract_custom_attrs(schema),
344 description: schema.schema_data.description.clone(),
345 }));
346 }
347 Ok(models)
348 }
349
350 SchemaKind::AllOf { all_of } => {
352 let (all_fields, inline_models) =
353 resolve_all_of_fields(name, all_of, all_schemas)?;
354 let mut models = inline_models;
355
356 if !all_fields.is_empty() {
357 models.push(ModelType::Composition(CompositionModel {
358 name: to_pascal_case(name),
359 all_fields,
360 custom_attrs: extract_custom_attrs(schema),
361 }));
362 }
363
364 Ok(models)
365 }
366
367 SchemaKind::OneOf { one_of } => {
369 let (variants, inline_models) =
370 resolve_union_variants(name, one_of, all_schemas)?;
371 let mut models = inline_models;
372
373 models.push(ModelType::Union(UnionModel {
374 name: to_pascal_case(name),
375 variants,
376 union_type: UnionType::OneOf,
377 custom_attrs: extract_custom_attrs(schema),
378 }));
379
380 Ok(models)
381 }
382
383 SchemaKind::AnyOf { any_of } => {
385 let (variants, inline_models) =
386 resolve_union_variants(name, any_of, all_schemas)?;
387 let mut models = inline_models;
388
389 models.push(ModelType::Union(UnionModel {
390 name: to_pascal_case(name),
391 variants,
392 union_type: UnionType::AnyOf,
393 custom_attrs: extract_custom_attrs(schema),
394 }));
395
396 Ok(models)
397 }
398
399 SchemaKind::Type(Type::String(string_type)) => {
401 if !string_type.enumeration.is_empty() {
402 let variants: Vec<String> = string_type
403 .enumeration
404 .iter()
405 .filter_map(|value| value.clone())
406 .collect();
407
408 if !variants.is_empty() {
409 let models = vec![ModelType::Enum(EnumModel {
410 name: to_pascal_case(name),
411 variants,
412 description: schema.schema_data.description.clone(),
413 custom_attrs: extract_custom_attrs(schema),
414 })];
415
416 return Ok(models);
417 }
418 }
419 Ok(Vec::new())
420 }
421
422 SchemaKind::Type(Type::Array(array)) => {
423 let mut models = Vec::new();
424 let array_name = to_pascal_case(name);
425
426 let items = match &array.items {
427 Some(items) => items,
428 None => return Ok(Vec::new()),
429 };
430
431 match items {
432 ReferenceOr::Item(item_schema) => match &item_schema.schema_kind {
433 SchemaKind::OneOf { one_of } => {
434 let item_type_name = format!("{array_name}Item");
435
436 let (variants, inline_models) =
437 resolve_union_variants(&item_type_name, one_of, all_schemas)?;
438
439 models.extend(inline_models);
440
441 models.push(ModelType::Union(UnionModel {
442 name: item_type_name.clone(),
443 variants,
444 union_type: UnionType::OneOf,
445 custom_attrs: extract_custom_attrs(item_schema),
446 }));
447
448 models.push(ModelType::TypeAlias(TypeAliasModel {
449 name: array_name,
450 target_type: format!("Vec<{item_type_name}>"),
451 description: schema.schema_data.description.clone(),
452 custom_attrs: extract_custom_attrs(schema),
453 }));
454 }
455
456 SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
457 let item_type_name = format!("{array_name}Item");
458
459 let variants: Vec<String> =
460 s.enumeration.iter().filter_map(|v| v.clone()).collect();
461
462 models.push(ModelType::Enum(EnumModel {
463 name: item_type_name.clone(),
464 variants,
465 description: item_schema.schema_data.description.clone(),
466 custom_attrs: extract_custom_attrs(item_schema),
467 }));
468
469 models.push(ModelType::TypeAlias(TypeAliasModel {
470 name: array_name,
471 target_type: format!("Vec<{item_type_name}>"),
472 description: schema.schema_data.description.clone(),
473 custom_attrs: extract_custom_attrs(schema),
474 }));
475 }
476
477 SchemaKind::Type(Type::Integer(n)) if !n.enumeration.is_empty() => {
478 let item_type_name = format!("{array_name}Item");
479
480 let variants: Vec<String> = n
481 .enumeration
482 .iter()
483 .filter_map(|v| v.map(|num| format!("Value{num}")))
484 .collect();
485
486 models.push(ModelType::Enum(EnumModel {
487 name: item_type_name.clone(),
488 variants,
489 description: item_schema.schema_data.description.clone(),
490 custom_attrs: extract_custom_attrs(item_schema),
491 }));
492
493 models.push(ModelType::TypeAlias(TypeAliasModel {
494 name: array_name,
495 target_type: format!("Vec<{item_type_name}>"),
496 description: schema.schema_data.description.clone(),
497 custom_attrs: extract_custom_attrs(schema),
498 }));
499 }
500
501 _ => {
502 let normalized_items = match items {
503 ReferenceOr::Item(boxed_schema) => {
504 ReferenceOr::Item((**boxed_schema).clone())
505 }
506 ReferenceOr::Reference { reference } => {
507 ReferenceOr::Reference {
508 reference: reference.clone(),
509 }
510 }
511 };
512
513 let (inner_type, _) =
514 extract_type_and_format(&normalized_items, all_schemas)?;
515
516 models.push(ModelType::TypeAlias(TypeAliasModel {
517 name: array_name,
518 target_type: format!("Vec<{inner_type}>"),
519 description: schema.schema_data.description.clone(),
520 custom_attrs: extract_custom_attrs(schema),
521 }));
522 }
523 },
524
525 ReferenceOr::Reference { .. } => {
526 let normalized_items = match items {
527 ReferenceOr::Item(boxed_schema) => {
528 ReferenceOr::Item((**boxed_schema).clone())
529 }
530 ReferenceOr::Reference { reference } => ReferenceOr::Reference {
531 reference: reference.clone(),
532 },
533 };
534
535 let (inner_type, _) =
536 extract_type_and_format(&normalized_items, all_schemas)?;
537
538 models.push(ModelType::TypeAlias(TypeAliasModel {
539 name: array_name,
540 target_type: format!("Vec<{inner_type}>"),
541 description: schema.schema_data.description.clone(),
542 custom_attrs: extract_custom_attrs(schema),
543 }));
544 }
545 }
546
547 Ok(models)
548 }
549
550 _ => Ok(Vec::new()),
551 }
552 }
553 }
554}
555
556fn extract_type_and_format(
557 schema: &ReferenceOr<Schema>,
558 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
559) -> Result<(String, String)> {
560 match schema {
561 ReferenceOr::Reference { reference } => {
562 let type_name = reference.split('/').next_back().unwrap_or("Unknown");
563
564 if let Some(ReferenceOr::Item(schema)) = all_schemas.get(type_name) {
565 if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
566 return Ok((to_pascal_case(type_name), "oneOf".to_string()));
567 }
568 }
569 Ok((to_pascal_case(type_name), "reference".to_string()))
570 }
571
572 ReferenceOr::Item(schema) => match &schema.schema_kind {
573 SchemaKind::Type(Type::String(string_type)) => match &string_type.format {
574 VariantOrUnknownOrEmpty::Item(fmt) => match fmt {
575 StringFormat::DateTime => {
576 Ok(("DateTime<Utc>".to_string(), "date-time".to_string()))
577 }
578 StringFormat::Date => Ok(("NaiveDate".to_string(), "date".to_string())),
579 _ => Ok(("String".to_string(), format!("{fmt:?}"))),
580 },
581 VariantOrUnknownOrEmpty::Unknown(unknown_format) => {
582 if unknown_format.to_lowercase() == "uuid" {
583 Ok(("Uuid".to_string(), "uuid".to_string()))
584 } else {
585 Ok(("String".to_string(), unknown_format.clone()))
586 }
587 }
588 _ => Ok(("String".to_string(), "string".to_string())),
589 },
590 SchemaKind::Type(Type::Integer(_)) => Ok(("i64".to_string(), "integer".to_string())),
591 SchemaKind::Type(Type::Number(_)) => Ok(("f64".to_string(), "number".to_string())),
592 SchemaKind::Type(Type::Boolean(_)) => Ok(("bool".to_string(), "boolean".to_string())),
593 SchemaKind::Type(Type::Array(arr)) => {
594 if let Some(items) = &arr.items {
595 match items {
596 ReferenceOr::Item(boxed_schema) => extract_type_and_format(
597 &ReferenceOr::Item((**boxed_schema).clone()),
598 all_schemas,
599 ),
600
601 ReferenceOr::Reference { reference } => extract_type_and_format(
602 &ReferenceOr::Reference {
603 reference: reference.clone(),
604 },
605 all_schemas,
606 ),
607 }
608 } else {
609 Ok(("serde_json::Value".to_string(), "array".to_string()))
610 }
611 }
612 SchemaKind::Type(Type::Object(_obj)) => {
613 Ok(("serde_json::Value".to_string(), "object".to_string()))
614 }
615 _ => Ok(("serde_json::Value".to_string(), "unknown".to_string())),
616 },
617 }
618}
619
620fn extract_field_info(
622 field_name: &str,
623 schema: &ReferenceOr<Schema>,
624 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
625) -> Result<(FieldInfo, Option<ModelType>)> {
626 let (mut field_type, format) = extract_type_and_format(schema, all_schemas)?;
627
628 let (is_nullable, is_array_ref, en, description) = match schema {
629 ReferenceOr::Reference { reference } => {
630 let mut is_array_ref = false;
631 let mut is_nullable = false;
632
633 if let Some(type_name) = reference.strip_prefix("#/components/schemas/") {
634 if let Some(ReferenceOr::Item(schema)) = all_schemas.get(type_name) {
635 is_nullable = schema.schema_data.nullable;
636
637 if let SchemaKind::Type(Type::Array(array)) = &schema.schema_kind {
638 let is_items_one_of = match &array.items {
639 Some(ReferenceOr::Item(item_schema)) => {
640 matches!(item_schema.schema_kind, SchemaKind::OneOf { .. })
641 }
642 _ => false,
643 };
644
645 is_array_ref = !is_items_one_of;
646 }
647 }
648 }
649
650 (is_nullable, is_array_ref, None, None)
651 }
652
653 ReferenceOr::Item(schema) => {
654 if let Some(rust_type) = schema.schema_data.extensions.get(X_RUST_TYPE) {
655 if let Some(type_str) = rust_type.as_str() {
656 field_type = type_str.to_string();
657 }
658 }
659
660 let is_nullable = schema.schema_data.nullable;
661 let is_array_ref = matches!(schema.schema_kind, SchemaKind::Type(Type::Array(_)));
662 let description = schema.schema_data.description.clone();
663
664 let maybe_enum = match &schema.schema_kind {
665 SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
666 let variants: Vec<String> =
667 s.enumeration.iter().filter_map(|v| v.clone()).collect();
668 field_type = to_pascal_case(field_name);
669 Some(ModelType::Enum(EnumModel {
670 name: to_pascal_case(field_name),
671 variants,
672 description: schema.schema_data.description.clone(),
673 custom_attrs: extract_custom_attrs(schema),
674 }))
675 }
676 SchemaKind::Type(Type::Object(obj)) => {
677 if obj.properties.is_empty() {
678 if let Some(additional_props) = &obj.additional_properties {
679 match additional_props {
680 AdditionalProperties::Schema(schema) => {
681 let (value_type, _) =
682 extract_type_and_format(&schema.clone(), all_schemas)?;
683
684 field_type = format!(
685 "std::collections::HashMap<String, {}>",
686 value_type
687 );
688 }
689
690 AdditionalProperties::Any(true) => {
691 field_type =
692 "std::collections::HashMap<String, serde_json::Value>"
693 .to_string();
694 }
695
696 AdditionalProperties::Any(false) => {
697 field_type = "serde_json::Value".to_string();
699 }
700 }
701 None
702 } else {
703 field_type = "serde_json::Value".to_string();
704 None
705 }
706 } else {
707 let struct_name = to_pascal_case(field_name);
708 field_type = struct_name.clone();
709
710 let wrapped_schema = ReferenceOr::Item(schema.clone());
711 let models =
712 parse_schema_to_model_type(&struct_name, &wrapped_schema, all_schemas)?;
713
714 models
715 .into_iter()
716 .find(|m| matches!(m, ModelType::Struct(_)))
717 }
718 }
719 _ => None,
720 };
721 (is_nullable, is_array_ref, maybe_enum, description)
722 }
723 };
724
725 Ok((
726 FieldInfo {
727 field_type,
728 format,
729 is_nullable,
730 is_array_ref,
731 description,
732 },
733 en,
734 ))
735}
736
737fn resolve_all_of_fields(
738 _name: &str,
739 all_of: &[ReferenceOr<Schema>],
740 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
741) -> Result<(Vec<Field>, Vec<ModelType>)> {
742 let mut all_fields: HashMap<String, Field> = HashMap::new();
743 let mut models = Vec::new();
744 let mut all_required_fields = HashSet::new();
745
746 for schema_ref in all_of {
747 let schema_to_check = match schema_ref {
748 ReferenceOr::Reference { reference } => reference
749 .strip_prefix("#/components/schemas/")
750 .and_then(|schema_name| all_schemas.get(schema_name)),
751 ReferenceOr::Item(_) => Some(schema_ref),
752 };
753
754 if let Some(ReferenceOr::Item(schema)) = schema_to_check {
755 if let SchemaKind::Type(Type::Object(obj)) = &schema.schema_kind {
756 all_required_fields.extend(obj.required.iter().cloned());
757 }
758 }
759 }
760
761 for schema_ref in all_of {
763 match schema_ref {
764 ReferenceOr::Reference { reference } => {
765 if let Some(schema_name) = reference.strip_prefix("#/components/schemas/") {
766 if let Some(referenced_schema) = all_schemas.get(schema_name) {
767 let (fields, inline_models) =
768 extract_fields_from_schema(referenced_schema, all_schemas)?;
769 for field in fields {
771 if let Some(existing_field) = all_fields.get_mut(&field.name) {
772 if existing_field.field_type == "serde_json::Value" {
773 *existing_field = field;
774 }
775 } else {
776 all_fields.insert(field.name.clone(), field);
777 }
778 }
779 models.extend(inline_models);
780 }
781 }
782 }
783 ReferenceOr::Item(_schema) => {
784 let (fields, inline_models) = extract_fields_from_schema(schema_ref, all_schemas)?;
785 for field in fields {
787 if let Some(existing_field) = all_fields.get_mut(&field.name) {
788 if existing_field.field_type == "serde_json::Value" {
789 *existing_field = field;
790 }
791 } else {
792 all_fields.insert(field.name.clone(), field);
793 }
794 }
795 models.extend(inline_models);
796 }
797 }
798 }
799
800 for field in all_fields.values_mut() {
802 if all_required_fields.contains(&field.name) {
803 field.is_required = true;
804 }
805 }
806
807 Ok((all_fields.into_values().collect(), models))
808}
809
810fn resolve_union_variants(
811 name: &str,
812 schemas: &[ReferenceOr<Schema>],
813 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
814) -> Result<(Vec<UnionVariant>, Vec<ModelType>)> {
815 use std::collections::BTreeSet;
816
817 let mut variants = Vec::new();
818 let mut models = Vec::new();
819 let mut enum_values: BTreeSet<String> = BTreeSet::new();
820 let mut is_all_simple_enum = true;
821
822 for schema_ref in schemas {
823 let resolved = match schema_ref {
824 ReferenceOr::Reference { reference } => reference
825 .strip_prefix("#/components/schemas/")
826 .and_then(|n| all_schemas.get(n)),
827 ReferenceOr::Item(_) => Some(schema_ref),
828 };
829
830 let Some(resolved_schema) = resolved else {
831 is_all_simple_enum = false;
832 continue;
833 };
834
835 match resolved_schema {
836 ReferenceOr::Item(schema) => match &schema.schema_kind {
837 SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
838 enum_values.extend(s.enumeration.iter().filter_map(|v| v.as_ref().cloned()));
839 }
840 SchemaKind::Type(Type::Integer(n)) if !n.enumeration.is_empty() => {
841 enum_values.extend(
842 n.enumeration
843 .iter()
844 .filter_map(|v| v.map(|num| format!("Value{num}"))),
845 );
846 }
847
848 _ => is_all_simple_enum = false,
849 },
850 ReferenceOr::Reference { reference } => {
851 if let Some(n) = reference.strip_prefix("#/components/schemas/") {
852 if let Some(ReferenceOr::Item(inner)) = all_schemas.get(n) {
853 if let SchemaKind::Type(Type::String(s)) = &inner.schema_kind {
854 let values: Vec<String> = s
855 .enumeration
856 .iter()
857 .filter_map(|v| v.as_ref().cloned())
858 .collect();
859 enum_values.extend(values);
860 } else {
861 is_all_simple_enum = false;
862 }
863 }
864 }
865 }
866 }
867 }
868 if is_all_simple_enum && !enum_values.is_empty() {
869 let enum_name = to_pascal_case(name);
870 let enum_model = ModelType::Enum(EnumModel {
871 name: enum_name.clone(),
872 variants: enum_values.iter().map(|v| to_pascal_case(v)).collect(),
873 description: None,
874 custom_attrs: None, });
876
877 return Ok((vec![], vec![enum_model]));
878 }
879
880 for (index, schema_ref) in schemas.iter().enumerate() {
882 match schema_ref {
883 ReferenceOr::Reference { reference } => {
884 if let Some(schema_name) = reference.strip_prefix("#/components/schemas/") {
885 if let Some(referenced_schema) = all_schemas.get(schema_name) {
886 if let ReferenceOr::Item(schema) = referenced_schema {
887 if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
888 variants.push(UnionVariant {
889 name: to_pascal_case(schema_name),
890 fields: vec![],
891 primitive_type: None,
892 });
893 } else {
894 let (fields, inline_models) =
895 extract_fields_from_schema(referenced_schema, all_schemas)?;
896 variants.push(UnionVariant {
897 name: to_pascal_case(schema_name),
898 fields,
899 primitive_type: None,
900 });
901 models.extend(inline_models);
902 }
903 }
904 }
905 }
906 }
907 ReferenceOr::Item(schema) => match &schema.schema_kind {
908 SchemaKind::Type(Type::String(_)) => {
909 variants.push(UnionVariant {
910 name: "String".to_string(),
911 fields: vec![],
912 primitive_type: Some("String".to_string()),
913 });
914 }
915
916 SchemaKind::Type(Type::Integer(_)) => {
917 variants.push(UnionVariant {
918 name: "Integer".to_string(),
919 fields: vec![],
920 primitive_type: Some("i64".to_string()),
921 });
922 }
923
924 SchemaKind::Type(Type::Number(_)) => {
925 variants.push(UnionVariant {
926 name: "Number".to_string(),
927 fields: vec![],
928 primitive_type: Some("f64".to_string()),
929 });
930 }
931
932 SchemaKind::Type(Type::Boolean(_)) => {
933 variants.push(UnionVariant {
934 name: "Boolean".to_string(),
935 fields: vec![],
936 primitive_type: Some("Boolean".to_string()),
937 });
938 }
939
940 _ => {
941 let (fields, inline_models) =
942 extract_fields_from_schema(schema_ref, all_schemas)?;
943 let variant_name = format!("Variant{index}");
944 variants.push(UnionVariant {
945 name: variant_name,
946 fields,
947 primitive_type: None,
948 });
949 models.extend(inline_models);
950 }
951 },
952 }
953 }
954
955 Ok((variants, models))
956}
957
958fn extract_fields_from_schema(
959 schema_ref: &ReferenceOr<Schema>,
960 _all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
961) -> Result<(Vec<Field>, Vec<ModelType>)> {
962 let mut fields = Vec::new();
963 let mut inline_models = Vec::new();
964
965 match schema_ref {
966 ReferenceOr::Reference { .. } => Ok((fields, inline_models)),
967 ReferenceOr::Item(schema) => {
968 match &schema.schema_kind {
969 SchemaKind::Type(Type::Object(obj)) => {
970 for (field_name, field_schema) in &obj.properties {
971 let (field_info, inline_model) = match field_schema {
972 ReferenceOr::Item(boxed_schema) => extract_field_info(
973 field_name,
974 &ReferenceOr::Item((**boxed_schema).clone()),
975 _all_schemas,
976 )?,
977 ReferenceOr::Reference { reference } => extract_field_info(
978 field_name,
979 &ReferenceOr::Reference {
980 reference: reference.clone(),
981 },
982 _all_schemas,
983 )?,
984 };
985
986 let is_nullable = field_info.is_nullable
987 || field_name == "value"
988 || field_name == "default_value";
989
990 let field_type = field_info.field_type.clone();
991
992 let is_required = obj.required.contains(field_name);
993 fields.push(Field {
994 name: field_name.clone(),
995 field_type,
996 format: field_info.format,
997 is_required,
998 is_nullable,
999 is_array_ref: field_info.is_array_ref,
1000 description: field_info.description,
1001 });
1002 if let Some(inline_model) = inline_model {
1003 match &inline_model {
1004 ModelType::Struct(m) if m.fields.is_empty() => {}
1005 _ => inline_models.push(inline_model),
1006 }
1007 }
1008 }
1009 }
1010 SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
1011 let name = schema
1012 .schema_data
1013 .title
1014 .clone()
1015 .unwrap_or_else(|| "AnonymousStringEnum".to_string());
1016
1017 let enum_model = ModelType::Enum(EnumModel {
1018 name,
1019 variants: s
1020 .enumeration
1021 .iter()
1022 .filter_map(|v| v.as_ref().map(|s| to_pascal_case(s)))
1023 .collect(),
1024 description: schema.schema_data.description.clone(),
1025 custom_attrs: extract_custom_attrs(schema),
1026 });
1027
1028 inline_models.push(enum_model);
1029 }
1030 SchemaKind::Type(Type::Integer(n)) if !n.enumeration.is_empty() => {
1031 let name = schema
1032 .schema_data
1033 .title
1034 .clone()
1035 .unwrap_or_else(|| "AnonymousIntEnum".to_string());
1036
1037 let enum_model = ModelType::Enum(EnumModel {
1038 name,
1039 variants: n
1040 .enumeration
1041 .iter()
1042 .filter_map(|v| v.map(|num| format!("Value{num}")))
1043 .collect(),
1044 description: schema.schema_data.description.clone(),
1045 custom_attrs: extract_custom_attrs(schema),
1046 });
1047
1048 inline_models.push(enum_model);
1049 }
1050
1051 _ => {}
1052 }
1053
1054 Ok((fields, inline_models))
1055 }
1056 }
1057}
1058
1059#[cfg(test)]
1060mod tests {
1061 use super::*;
1062 use serde_json::json;
1063
1064 #[test]
1065 fn test_parse_inline_request_body_generates_model() {
1066 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1067 "openapi": "3.0.0",
1068 "info": { "title": "Test API", "version": "1.0.0" },
1069 "paths": {
1070 "/items": {
1071 "post": {
1072 "operationId": "createItem",
1073 "requestBody": {
1074 "content": {
1075 "application/json": {
1076 "schema": {
1077 "type": "object",
1078 "properties": {
1079 "name": { "type": "string" },
1080 "value": { "type": "integer" }
1081 },
1082 "required": ["name"]
1083 }
1084 }
1085 }
1086 },
1087 "responses": { "200": { "description": "OK" } }
1088 }
1089 }
1090 }
1091 }))
1092 .expect("Failed to deserialize OpenAPI spec");
1093
1094 let (models, requests, _responses) =
1095 parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1096
1097 assert_eq!(requests.len(), 1);
1099 let request_model = &requests[0];
1100 assert_eq!(request_model.name, "CreateItemRequest");
1101
1102 assert_eq!(request_model.schema, "CreateItemRequestBody");
1104
1105 let inline_model = models.iter().find(|m| m.name() == "CreateItemRequestBody");
1107 assert!(
1108 inline_model.is_some(),
1109 "Expected a model named 'CreateItemRequestBody' to be generated"
1110 );
1111
1112 if let Some(ModelType::Struct(model)) = inline_model {
1113 assert_eq!(model.fields.len(), 2);
1114 assert_eq!(model.fields[0].name, "name");
1115 assert_eq!(model.fields[0].field_type, "String");
1116 assert!(model.fields[0].is_required);
1117
1118 assert_eq!(model.fields[1].name, "value");
1119 assert_eq!(model.fields[1].field_type, "i64");
1120 assert!(!model.fields[1].is_required);
1121 } else {
1122 panic!("Expected a Struct model for CreateItemRequestBody");
1123 }
1124 }
1125
1126 #[test]
1127 fn test_parse_ref_request_body_works() {
1128 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1129 "openapi": "3.0.0",
1130 "info": { "title": "Test API", "version": "1.0.0" },
1131 "components": {
1132 "schemas": {
1133 "ItemData": {
1134 "type": "object",
1135 "properties": {
1136 "name": { "type": "string" }
1137 }
1138 }
1139 },
1140 "requestBodies": {
1141 "CreateItem": {
1142 "content": {
1143 "application/json": {
1144 "schema": { "$ref": "#/components/schemas/ItemData" }
1145 }
1146 }
1147 }
1148 }
1149 },
1150 "paths": {
1151 "/items": {
1152 "post": {
1153 "operationId": "createItem",
1154 "requestBody": { "$ref": "#/components/requestBodies/CreateItem" },
1155 "responses": { "200": { "description": "OK" } }
1156 }
1157 }
1158 }
1159 }))
1160 .expect("Failed to deserialize OpenAPI spec");
1161
1162 let (models, requests, _responses) =
1163 parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1164
1165 assert_eq!(requests.len(), 1);
1167 let request_model = &requests[0];
1168 assert_eq!(request_model.name, "CreateItemRequest");
1169
1170 assert_eq!(request_model.schema, "ItemData");
1172
1173 assert!(models.iter().any(|m| m.name() == "ItemData"));
1175 }
1176
1177 #[test]
1178 fn test_parse_no_request_body() {
1179 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1180 "openapi": "3.0.0",
1181 "info": { "title": "Test API", "version": "1.0.0" },
1182 "paths": {
1183 "/items": {
1184 "get": {
1185 "operationId": "listItems",
1186 "responses": { "200": { "description": "OK" } }
1187 }
1188 }
1189 }
1190 }))
1191 .expect("Failed to deserialize OpenAPI spec");
1192
1193 let (_models, requests, _responses) =
1194 parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1195
1196 assert!(requests.is_empty());
1198 }
1199
1200 #[test]
1201 fn test_nullable_reference_field() {
1202 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1204 "openapi": "3.0.0",
1205 "info": { "title": "Test API", "version": "1.0.0" },
1206 "paths": {},
1207 "components": {
1208 "schemas": {
1209 "NullableUser": {
1210 "type": "object",
1211 "nullable": true,
1212 "properties": {
1213 "name": { "type": "string" }
1214 }
1215 },
1216 "Post": {
1217 "type": "object",
1218 "properties": {
1219 "author": {
1220 "$ref": "#/components/schemas/NullableUser"
1221 }
1222 }
1223 }
1224 }
1225 }
1226 }))
1227 .expect("Failed to deserialize OpenAPI spec");
1228
1229 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1230
1231 let post_model = models.iter().find(|m| m.name() == "Post");
1233 assert!(post_model.is_some(), "Expected Post model to be generated");
1234
1235 if let Some(ModelType::Struct(post)) = post_model {
1236 let author_field = post.fields.iter().find(|f| f.name == "author");
1237 assert!(author_field.is_some(), "Expected author field");
1238
1239 let author = author_field.unwrap();
1242 assert!(
1243 author.is_nullable,
1244 "Expected author field to be nullable (from referenced schema)"
1245 );
1246 } else {
1247 panic!("Expected Post to be a Struct");
1248 }
1249 }
1250
1251 #[test]
1252 fn test_allof_required_fields_merge() {
1253 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1254 "openapi": "3.0.0",
1255 "info": { "title": "Test API", "version": "1.0.0" },
1256 "paths": {},
1257 "components": {
1258 "schemas": {
1259 "BaseEntity": {
1260 "type": "object",
1261 "properties": {
1262 "id": { "type": "string" },
1263 "created": { "type": "string" }
1264 },
1265 "required": ["id"]
1266 },
1267 "Person": {
1268 "allOf": [
1269 { "$ref": "#/components/schemas/BaseEntity" },
1270 {
1271 "type": "object",
1272 "properties": {
1273 "name": { "type": "string" },
1274 "age": { "type": "integer" }
1275 },
1276 "required": ["name"]
1277 }
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 person_model = models.iter().find(|m| m.name() == "Person");
1289 assert!(
1290 person_model.is_some(),
1291 "Expected Person model to be generated"
1292 );
1293
1294 if let Some(ModelType::Composition(person)) = person_model {
1295 let id_field = person.all_fields.iter().find(|f| f.name == "id");
1297 assert!(id_field.is_some(), "Expected id field");
1298 assert!(
1299 id_field.unwrap().is_required,
1300 "Expected id to be required from BaseEntity"
1301 );
1302
1303 let name_field = person.all_fields.iter().find(|f| f.name == "name");
1305 assert!(name_field.is_some(), "Expected name field");
1306 assert!(
1307 name_field.unwrap().is_required,
1308 "Expected name to be required from inline object"
1309 );
1310
1311 let created_field = person.all_fields.iter().find(|f| f.name == "created");
1313 assert!(created_field.is_some(), "Expected created field");
1314 assert!(
1315 !created_field.unwrap().is_required,
1316 "Expected created to be optional"
1317 );
1318
1319 let age_field = person.all_fields.iter().find(|f| f.name == "age");
1320 assert!(age_field.is_some(), "Expected age field");
1321 assert!(
1322 !age_field.unwrap().is_required,
1323 "Expected age to be optional"
1324 );
1325 } else {
1326 panic!("Expected Person to be a Composition");
1327 }
1328 }
1329
1330 #[test]
1331 fn test_x_rust_type_generates_type_alias() {
1332 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1333 "openapi": "3.0.0",
1334 "info": { "title": "Test API", "version": "1.0.0" },
1335 "paths": {},
1336 "components": {
1337 "schemas": {
1338 "User": {
1339 "type": "object",
1340 "x-rust-type": "crate::domain::User",
1341 "description": "Custom domain user type",
1342 "properties": {
1343 "name": { "type": "string" },
1344 "age": { "type": "integer" }
1345 }
1346 }
1347 }
1348 }
1349 }))
1350 .expect("Failed to deserialize OpenAPI spec");
1351
1352 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1353
1354 let user_model = models.iter().find(|m| m.name() == "User");
1356 assert!(user_model.is_some(), "Expected User model");
1357
1358 match user_model.unwrap() {
1359 ModelType::TypeAlias(alias) => {
1360 assert_eq!(alias.name, "User");
1361 assert_eq!(alias.target_type, "crate::domain::User");
1362 assert_eq!(
1363 alias.description,
1364 Some("Custom domain user type".to_string())
1365 );
1366 }
1367 _ => panic!("Expected TypeAlias, got different type"),
1368 }
1369 }
1370
1371 #[test]
1372 fn test_x_rust_type_works_with_enum() {
1373 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1374 "openapi": "3.0.0",
1375 "info": { "title": "Test API", "version": "1.0.0" },
1376 "paths": {},
1377 "components": {
1378 "schemas": {
1379 "Status": {
1380 "type": "string",
1381 "enum": ["active", "inactive"],
1382 "x-rust-type": "crate::domain::Status"
1383 }
1384 }
1385 }
1386 }))
1387 .expect("Failed to deserialize OpenAPI spec");
1388
1389 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1390
1391 let status_model = models.iter().find(|m| m.name() == "Status");
1392 assert!(status_model.is_some(), "Expected Status model");
1393
1394 assert!(
1396 matches!(status_model.unwrap(), ModelType::TypeAlias(_)),
1397 "Expected TypeAlias for enum with x-rust-type"
1398 );
1399 }
1400
1401 #[test]
1402 fn test_x_rust_type_works_with_oneof() {
1403 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1404 "openapi": "3.0.0",
1405 "info": { "title": "Test API", "version": "1.0.0" },
1406 "paths": {},
1407 "components": {
1408 "schemas": {
1409 "Payment": {
1410 "oneOf": [
1411 { "type": "object", "properties": { "card": { "type": "string" } } },
1412 { "type": "object", "properties": { "cash": { "type": "number" } } }
1413 ],
1414 "x-rust-type": "payments::Payment"
1415 }
1416 }
1417 }
1418 }))
1419 .expect("Failed to deserialize OpenAPI spec");
1420
1421 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1422
1423 let payment_model = models.iter().find(|m| m.name() == "Payment");
1424 assert!(payment_model.is_some(), "Expected Payment model");
1425
1426 match payment_model.unwrap() {
1428 ModelType::TypeAlias(alias) => {
1429 assert_eq!(alias.target_type, "payments::Payment");
1430 }
1431 _ => panic!("Expected TypeAlias for oneOf with x-rust-type"),
1432 }
1433 }
1434
1435 #[test]
1436 fn test_x_rust_attrs_on_struct() {
1437 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1438 "openapi": "3.0.0",
1439 "info": { "title": "Test API", "version": "1.0.0" },
1440 "paths": {},
1441 "components": {
1442 "schemas": {
1443 "User": {
1444 "type": "object",
1445 "x-rust-attrs": [
1446 "#[derive(Serialize, Deserialize)]",
1447 "#[serde(rename_all = \"camelCase\")]"
1448 ],
1449 "properties": {
1450 "name": { "type": "string" }
1451 }
1452 }
1453 }
1454 }
1455 }))
1456 .expect("Failed to deserialize OpenAPI spec");
1457
1458 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1459
1460 let user_model = models.iter().find(|m| m.name() == "User");
1461 assert!(user_model.is_some(), "Expected User model");
1462
1463 match user_model.unwrap() {
1464 ModelType::Struct(model) => {
1465 assert!(model.custom_attrs.is_some(), "Expected custom_attrs");
1466 let attrs = model.custom_attrs.as_ref().unwrap();
1467 assert_eq!(attrs.len(), 2);
1468 assert_eq!(attrs[0], "#[derive(Serialize, Deserialize)]");
1469 assert_eq!(attrs[1], "#[serde(rename_all = \"camelCase\")]");
1470 }
1471 _ => panic!("Expected Struct model"),
1472 }
1473 }
1474
1475 #[test]
1476 fn test_x_rust_attrs_on_enum() {
1477 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1478 "openapi": "3.0.0",
1479 "info": { "title": "Test API", "version": "1.0.0" },
1480 "paths": {},
1481 "components": {
1482 "schemas": {
1483 "Status": {
1484 "type": "string",
1485 "enum": ["active", "inactive"],
1486 "x-rust-attrs": ["#[serde(rename_all = \"UPPERCASE\")]"]
1487 }
1488 }
1489 }
1490 }))
1491 .expect("Failed to deserialize OpenAPI spec");
1492
1493 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1494
1495 let status_model = models.iter().find(|m| m.name() == "Status");
1496 assert!(status_model.is_some(), "Expected Status model");
1497
1498 match status_model.unwrap() {
1499 ModelType::Enum(enum_model) => {
1500 assert!(enum_model.custom_attrs.is_some());
1501 let attrs = enum_model.custom_attrs.as_ref().unwrap();
1502 assert_eq!(attrs.len(), 1);
1503 assert_eq!(attrs[0], "#[serde(rename_all = \"UPPERCASE\")]");
1504 }
1505 _ => panic!("Expected Enum model"),
1506 }
1507 }
1508
1509 #[test]
1510 fn test_x_rust_attrs_with_x_rust_type() {
1511 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1512 "openapi": "3.0.0",
1513 "info": { "title": "Test API", "version": "1.0.0" },
1514 "paths": {},
1515 "components": {
1516 "schemas": {
1517 "User": {
1518 "type": "object",
1519 "x-rust-type": "crate::domain::User",
1520 "x-rust-attrs": ["#[cfg_attr(test, derive(Default))]"],
1521 "properties": {
1522 "name": { "type": "string" }
1523 }
1524 }
1525 }
1526 }
1527 }))
1528 .expect("Failed to deserialize OpenAPI spec");
1529
1530 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1531
1532 let user_model = models.iter().find(|m| m.name() == "User");
1533 assert!(user_model.is_some(), "Expected User model");
1534
1535 match user_model.unwrap() {
1537 ModelType::TypeAlias(alias) => {
1538 assert_eq!(alias.target_type, "crate::domain::User");
1539 assert!(alias.custom_attrs.is_some());
1540 let attrs = alias.custom_attrs.as_ref().unwrap();
1541 assert_eq!(attrs.len(), 1);
1542 assert_eq!(attrs[0], "#[cfg_attr(test, derive(Default))]");
1543 }
1544 _ => panic!("Expected TypeAlias with custom attrs"),
1545 }
1546 }
1547
1548 #[test]
1549 fn test_x_rust_attrs_empty_array() {
1550 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1551 "openapi": "3.0.0",
1552 "info": { "title": "Test API", "version": "1.0.0" },
1553 "paths": {},
1554 "components": {
1555 "schemas": {
1556 "User": {
1557 "type": "object",
1558 "x-rust-attrs": [],
1559 "properties": {
1560 "name": { "type": "string" }
1561 }
1562 }
1563 }
1564 }
1565 }))
1566 .expect("Failed to deserialize OpenAPI spec");
1567
1568 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1569
1570 let user_model = models.iter().find(|m| m.name() == "User");
1571 assert!(user_model.is_some());
1572
1573 match user_model.unwrap() {
1574 ModelType::Struct(model) => {
1575 assert!(model.custom_attrs.is_none());
1577 }
1578 _ => panic!("Expected Struct"),
1579 }
1580 }
1581
1582 #[test]
1583 fn test_x_rust_type_on_string_property() {
1584 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1585 "openapi": "3.0.0",
1586 "info": { "title": "Test API", "version": "1.0.0" },
1587 "paths": {},
1588 "components": {
1589 "schemas": {
1590 "Document": {
1591 "type": "object",
1592 "description": "Document with custom version type",
1593 "properties": {
1594 "title": { "type": "string", "description": "Document title." },
1595 "content": { "type": "string", "description": "Document content." },
1596 "version": {
1597 "type": "string",
1598 "format": "semver",
1599 "x-rust-type": "semver::Version",
1600 "description": "Semantic version."
1601 }
1602 },
1603 "required": ["title", "content", "version"]
1604 }
1605 }
1606 }
1607 }))
1608 .expect("Failed to deserialize OpenAPI spec");
1609
1610 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1611
1612 let document_model = models.iter().find(|m| m.name() == "Document");
1613 assert!(document_model.is_some(), "Expected Document model");
1614
1615 match document_model.unwrap() {
1616 ModelType::Struct(model) => {
1617 let version_field = model.fields.iter().find(|f| f.name == "version");
1619 assert!(version_field.is_some(), "Expected version field");
1620 assert_eq!(version_field.unwrap().field_type, "semver::Version");
1621
1622 let title_field = model.fields.iter().find(|f| f.name == "title");
1624 assert_eq!(title_field.unwrap().field_type, "String");
1625
1626 let content_field = model.fields.iter().find(|f| f.name == "content");
1627 assert_eq!(content_field.unwrap().field_type, "String");
1628 }
1629 _ => panic!("Expected Struct"),
1630 }
1631 }
1632
1633 #[test]
1634 fn test_x_rust_type_on_integer_property() {
1635 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1636 "openapi": "3.0.0",
1637 "info": { "title": "Test API", "version": "1.0.0" },
1638 "paths": {},
1639 "components": {
1640 "schemas": {
1641 "Configuration": {
1642 "type": "object",
1643 "description": "Configuration with custom duration type",
1644 "properties": {
1645 "timeout": {
1646 "type": "integer",
1647 "x-rust-type": "std::time::Duration",
1648 "description": "Timeout duration."
1649 },
1650 "retries": { "type": "integer" }
1651 },
1652 "required": ["timeout", "retries"]
1653 }
1654 }
1655 }
1656 }))
1657 .expect("Failed to deserialize OpenAPI spec");
1658
1659 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1660
1661 let config_model = models.iter().find(|m| m.name() == "Configuration");
1662 assert!(config_model.is_some(), "Expected Configuration model");
1663
1664 match config_model.unwrap() {
1665 ModelType::Struct(model) => {
1666 let timeout_field = model.fields.iter().find(|f| f.name == "timeout");
1668 assert!(timeout_field.is_some(), "Expected timeout field");
1669 assert_eq!(timeout_field.unwrap().field_type, "std::time::Duration");
1670
1671 let retries_field = model.fields.iter().find(|f| f.name == "retries");
1673 assert_eq!(retries_field.unwrap().field_type, "i64");
1674 }
1675 _ => panic!("Expected Struct"),
1676 }
1677 }
1678
1679 #[test]
1680 fn test_x_rust_type_on_number_property() {
1681 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1682 "openapi": "3.0.0",
1683 "info": { "title": "Test API", "version": "1.0.0" },
1684 "paths": {},
1685 "components": {
1686 "schemas": {
1687 "Product": {
1688 "type": "object",
1689 "description": "Product with custom decimal type",
1690 "properties": {
1691 "price": {
1692 "type": "number",
1693 "x-rust-type": "decimal::Decimal",
1694 "description": "Product price."
1695 },
1696 "quantity": { "type": "number" }
1697 },
1698 "required": ["price", "quantity"]
1699 }
1700 }
1701 }
1702 }))
1703 .expect("Failed to deserialize OpenAPI spec");
1704
1705 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1706
1707 let product_model = models.iter().find(|m| m.name() == "Product");
1708 assert!(product_model.is_some(), "Expected Product model");
1709
1710 match product_model.unwrap() {
1711 ModelType::Struct(model) => {
1712 let price_field = model.fields.iter().find(|f| f.name == "price");
1714 assert!(price_field.is_some(), "Expected price field");
1715 assert_eq!(price_field.unwrap().field_type, "decimal::Decimal");
1716
1717 let quantity_field = model.fields.iter().find(|f| f.name == "quantity");
1719 assert_eq!(quantity_field.unwrap().field_type, "f64");
1720 }
1721 _ => panic!("Expected Struct"),
1722 }
1723 }
1724
1725 #[test]
1726 fn test_x_rust_type_on_nullable_property() {
1727 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1728 "openapi": "3.0.0",
1729 "info": { "title": "Test API", "version": "1.0.0" },
1730 "paths": {},
1731 "components": {
1732 "schemas": {
1733 "Settings": {
1734 "type": "object",
1735 "description": "Settings with nullable custom type",
1736 "properties": {
1737 "settings": {
1738 "type": "string",
1739 "x-rust-type": "serde_json::Value",
1740 "nullable": true,
1741 "description": "Optional settings."
1742 }
1743 }
1744 }
1745 }
1746 }
1747 }))
1748 .expect("Failed to deserialize OpenAPI spec");
1749
1750 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1751
1752 let settings_model = models.iter().find(|m| m.name() == "Settings");
1753 assert!(settings_model.is_some(), "Expected Settings model");
1754
1755 match settings_model.unwrap() {
1756 ModelType::Struct(model) => {
1757 let settings_field = model.fields.iter().find(|f| f.name == "settings");
1758 assert!(settings_field.is_some(), "Expected settings field");
1759
1760 let field = settings_field.unwrap();
1761 assert_eq!(field.field_type, "serde_json::Value");
1762 assert!(field.is_nullable, "Expected field to be nullable");
1763 }
1764 _ => panic!("Expected Struct"),
1765 }
1766 }
1767
1768 #[test]
1769 fn test_multiple_properties_with_x_rust_type() {
1770 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1771 "openapi": "3.0.0",
1772 "info": { "title": "Test API", "version": "1.0.0" },
1773 "paths": {},
1774 "components": {
1775 "schemas": {
1776 "ComplexModel": {
1777 "type": "object",
1778 "description": "Model with multiple custom-typed properties",
1779 "properties": {
1780 "id": {
1781 "type": "string",
1782 "format": "uuid",
1783 "x-rust-type": "uuid::Uuid"
1784 },
1785 "price": {
1786 "type": "number",
1787 "x-rust-type": "decimal::Decimal"
1788 },
1789 "timeout": {
1790 "type": "integer",
1791 "x-rust-type": "std::time::Duration"
1792 },
1793 "regular_field": { "type": "string" }
1794 },
1795 "required": ["id", "price", "timeout"]
1796 }
1797 }
1798 }
1799 }))
1800 .expect("Failed to deserialize OpenAPI spec");
1801
1802 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1803
1804 let model = models.iter().find(|m| m.name() == "ComplexModel");
1805 assert!(model.is_some(), "Expected ComplexModel model");
1806
1807 match model.unwrap() {
1808 ModelType::Struct(struct_model) => {
1809 let id_field = struct_model.fields.iter().find(|f| f.name == "id");
1811 assert_eq!(id_field.unwrap().field_type, "uuid::Uuid");
1812
1813 let price_field = struct_model.fields.iter().find(|f| f.name == "price");
1814 assert_eq!(price_field.unwrap().field_type, "decimal::Decimal");
1815
1816 let timeout_field = struct_model.fields.iter().find(|f| f.name == "timeout");
1817 assert_eq!(timeout_field.unwrap().field_type, "std::time::Duration");
1818
1819 let regular_field = struct_model
1821 .fields
1822 .iter()
1823 .find(|f| f.name == "regular_field");
1824 assert_eq!(regular_field.unwrap().field_type, "String");
1825
1826 assert!(!id_field.unwrap().is_nullable, "id should not be nullable");
1828 assert!(
1829 !price_field.unwrap().is_nullable,
1830 "price should not be nullable"
1831 );
1832 assert!(
1833 !timeout_field.unwrap().is_nullable,
1834 "timeout should not be nullable"
1835 );
1836 }
1839 _ => panic!("Expected Struct"),
1840 }
1841 }
1842}