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::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: IndexMap<String, Field> = IndexMap::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 fn less_value(fields: Vec<Field>, all_fields: &mut IndexMap<String, Field>) {
767 for field in fields {
768 if let Some(existing_field) = all_fields.get_mut(&field.name) {
769 if existing_field.field_type == "serde_json::Value" {
771 *existing_field = field;
772 } else if existing_field.field_type == "Option<serde_json::Value>" {
773 existing_field.field_type = format!("Option<{}>", field.field_type);
774 } else if existing_field.field_type
776 == "std::collections::HashMap<String, serde_json::Value>"
777 {
778 *existing_field = field;
779 } else if existing_field.field_type
780 == "Option<std::collections::HashMap<String, serde_json::Value>>"
781 {
782 existing_field.field_type = format!("Option<{}>", field.field_type);
783 } else if existing_field.field_type == "Vec<serde_json::Value>" {
785 existing_field.field_type = format!("Vec<{}>", field.field_type);
786 } else if existing_field.field_type == "Option<Vec<serde_json::Value>>" {
787 existing_field.field_type = format!("Option<Vec<{}>>", field.field_type);
788 }
789 } else {
790 all_fields.insert(field.name.clone(), field);
791 }
792 }
793 }
794
795 for schema_ref in all_of {
797 match schema_ref {
798 ReferenceOr::Reference { reference } => {
799 if let Some(schema_name) = reference.strip_prefix("#/components/schemas/") {
800 if let Some(referenced_schema) = all_schemas.get(schema_name) {
801 let (fields, inline_models) =
802 extract_fields_from_schema(referenced_schema, all_schemas)?;
803 less_value(fields, &mut all_fields);
805 models.extend(inline_models);
806 }
807 }
808 }
809 ReferenceOr::Item(_schema) => {
810 let (fields, inline_models) = extract_fields_from_schema(schema_ref, all_schemas)?;
811 less_value(fields, &mut all_fields);
813 models.extend(inline_models);
814 }
815 }
816 }
817
818 for field in all_fields.values_mut() {
820 if all_required_fields.contains(&field.name) {
821 field.is_required = true;
822 }
823 }
824
825 Ok((all_fields.into_values().collect(), models))
826}
827
828fn resolve_union_variants(
829 name: &str,
830 schemas: &[ReferenceOr<Schema>],
831 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
832) -> Result<(Vec<UnionVariant>, Vec<ModelType>)> {
833 use std::collections::BTreeSet;
834
835 let mut variants = Vec::new();
836 let mut models = Vec::new();
837 let mut enum_values: BTreeSet<String> = BTreeSet::new();
838 let mut is_all_simple_enum = true;
839
840 for schema_ref in schemas {
841 let resolved = match schema_ref {
842 ReferenceOr::Reference { reference } => reference
843 .strip_prefix("#/components/schemas/")
844 .and_then(|n| all_schemas.get(n)),
845 ReferenceOr::Item(_) => Some(schema_ref),
846 };
847
848 let Some(resolved_schema) = resolved else {
849 is_all_simple_enum = false;
850 continue;
851 };
852
853 match resolved_schema {
854 ReferenceOr::Item(schema) => match &schema.schema_kind {
855 SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
856 enum_values.extend(s.enumeration.iter().filter_map(|v| v.as_ref().cloned()));
857 }
858 SchemaKind::Type(Type::Integer(n)) if !n.enumeration.is_empty() => {
859 enum_values.extend(
860 n.enumeration
861 .iter()
862 .filter_map(|v| v.map(|num| format!("Value{num}"))),
863 );
864 }
865
866 _ => is_all_simple_enum = false,
867 },
868 ReferenceOr::Reference { reference } => {
869 if let Some(n) = reference.strip_prefix("#/components/schemas/") {
870 if let Some(ReferenceOr::Item(inner)) = all_schemas.get(n) {
871 if let SchemaKind::Type(Type::String(s)) = &inner.schema_kind {
872 let values: Vec<String> = s
873 .enumeration
874 .iter()
875 .filter_map(|v| v.as_ref().cloned())
876 .collect();
877 enum_values.extend(values);
878 } else {
879 is_all_simple_enum = false;
880 }
881 }
882 }
883 }
884 }
885 }
886 if is_all_simple_enum && !enum_values.is_empty() {
887 let enum_name = to_pascal_case(name);
888 let enum_model = ModelType::Enum(EnumModel {
889 name: enum_name.clone(),
890 variants: enum_values.iter().map(|v| to_pascal_case(v)).collect(),
891 description: None,
892 custom_attrs: None, });
894
895 return Ok((vec![], vec![enum_model]));
896 }
897
898 for (index, schema_ref) in schemas.iter().enumerate() {
900 match schema_ref {
901 ReferenceOr::Reference { reference } => {
902 if let Some(schema_name) = reference.strip_prefix("#/components/schemas/") {
903 if let Some(referenced_schema) = all_schemas.get(schema_name) {
904 if let ReferenceOr::Item(schema) = referenced_schema {
905 if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
906 variants.push(UnionVariant {
907 name: to_pascal_case(schema_name),
908 fields: vec![],
909 primitive_type: None,
910 });
911 } else {
912 let (fields, inline_models) =
913 extract_fields_from_schema(referenced_schema, all_schemas)?;
914 variants.push(UnionVariant {
915 name: to_pascal_case(schema_name),
916 fields,
917 primitive_type: None,
918 });
919 models.extend(inline_models);
920 }
921 }
922 }
923 }
924 }
925 ReferenceOr::Item(schema) => match &schema.schema_kind {
926 SchemaKind::Type(Type::String(_)) => {
927 variants.push(UnionVariant {
928 name: "String".to_string(),
929 fields: vec![],
930 primitive_type: Some("String".to_string()),
931 });
932 }
933
934 SchemaKind::Type(Type::Integer(_)) => {
935 variants.push(UnionVariant {
936 name: "Integer".to_string(),
937 fields: vec![],
938 primitive_type: Some("i64".to_string()),
939 });
940 }
941
942 SchemaKind::Type(Type::Number(_)) => {
943 variants.push(UnionVariant {
944 name: "Number".to_string(),
945 fields: vec![],
946 primitive_type: Some("f64".to_string()),
947 });
948 }
949
950 SchemaKind::Type(Type::Boolean(_)) => {
951 variants.push(UnionVariant {
952 name: "Boolean".to_string(),
953 fields: vec![],
954 primitive_type: Some("Boolean".to_string()),
955 });
956 }
957
958 _ => {
959 let (fields, inline_models) =
960 extract_fields_from_schema(schema_ref, all_schemas)?;
961 let variant_name = format!("Variant{index}");
962 variants.push(UnionVariant {
963 name: variant_name,
964 fields,
965 primitive_type: None,
966 });
967 models.extend(inline_models);
968 }
969 },
970 }
971 }
972
973 Ok((variants, models))
974}
975
976fn extract_fields_from_schema(
977 schema_ref: &ReferenceOr<Schema>,
978 _all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
979) -> Result<(Vec<Field>, Vec<ModelType>)> {
980 let mut fields = Vec::new();
981 let mut inline_models = Vec::new();
982
983 match schema_ref {
984 ReferenceOr::Reference { .. } => Ok((fields, inline_models)),
985 ReferenceOr::Item(schema) => {
986 match &schema.schema_kind {
987 SchemaKind::Type(Type::Object(obj)) => {
988 for (field_name, field_schema) in &obj.properties {
989 let (field_info, inline_model) = match field_schema {
990 ReferenceOr::Item(boxed_schema) => extract_field_info(
991 field_name,
992 &ReferenceOr::Item((**boxed_schema).clone()),
993 _all_schemas,
994 )?,
995 ReferenceOr::Reference { reference } => extract_field_info(
996 field_name,
997 &ReferenceOr::Reference {
998 reference: reference.clone(),
999 },
1000 _all_schemas,
1001 )?,
1002 };
1003
1004 let is_nullable = field_info.is_nullable
1005 || field_name == "value"
1006 || field_name == "default_value";
1007
1008 let field_type = field_info.field_type.clone();
1009
1010 let is_required = obj.required.contains(field_name);
1011 fields.push(Field {
1012 name: field_name.clone(),
1013 field_type,
1014 format: field_info.format,
1015 is_required,
1016 is_nullable,
1017 is_array_ref: field_info.is_array_ref,
1018 description: field_info.description,
1019 });
1020 if let Some(inline_model) = inline_model {
1021 match &inline_model {
1022 ModelType::Struct(m) if m.fields.is_empty() => {}
1023 _ => inline_models.push(inline_model),
1024 }
1025 }
1026 }
1027 }
1028 SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
1029 let name = schema
1030 .schema_data
1031 .title
1032 .clone()
1033 .unwrap_or_else(|| "AnonymousStringEnum".to_string());
1034
1035 let enum_model = ModelType::Enum(EnumModel {
1036 name,
1037 variants: s
1038 .enumeration
1039 .iter()
1040 .filter_map(|v| v.as_ref().map(|s| to_pascal_case(s)))
1041 .collect(),
1042 description: schema.schema_data.description.clone(),
1043 custom_attrs: extract_custom_attrs(schema),
1044 });
1045
1046 inline_models.push(enum_model);
1047 }
1048 SchemaKind::Type(Type::Integer(n)) if !n.enumeration.is_empty() => {
1049 let name = schema
1050 .schema_data
1051 .title
1052 .clone()
1053 .unwrap_or_else(|| "AnonymousIntEnum".to_string());
1054
1055 let enum_model = ModelType::Enum(EnumModel {
1056 name,
1057 variants: n
1058 .enumeration
1059 .iter()
1060 .filter_map(|v| v.map(|num| format!("Value{num}")))
1061 .collect(),
1062 description: schema.schema_data.description.clone(),
1063 custom_attrs: extract_custom_attrs(schema),
1064 });
1065
1066 inline_models.push(enum_model);
1067 }
1068
1069 _ => {}
1070 }
1071
1072 Ok((fields, inline_models))
1073 }
1074 }
1075}
1076
1077#[cfg(test)]
1078mod tests {
1079 use super::*;
1080 use serde_json::json;
1081
1082 #[test]
1083 fn test_parse_inline_request_body_generates_model() {
1084 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1085 "openapi": "3.0.0",
1086 "info": { "title": "Test API", "version": "1.0.0" },
1087 "paths": {
1088 "/items": {
1089 "post": {
1090 "operationId": "createItem",
1091 "requestBody": {
1092 "content": {
1093 "application/json": {
1094 "schema": {
1095 "type": "object",
1096 "properties": {
1097 "name": { "type": "string" },
1098 "value": { "type": "integer" }
1099 },
1100 "required": ["name"]
1101 }
1102 }
1103 }
1104 },
1105 "responses": { "200": { "description": "OK" } }
1106 }
1107 }
1108 }
1109 }))
1110 .expect("Failed to deserialize OpenAPI spec");
1111
1112 let (models, requests, _responses) =
1113 parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1114
1115 assert_eq!(requests.len(), 1);
1117 let request_model = &requests[0];
1118 assert_eq!(request_model.name, "CreateItemRequest");
1119
1120 assert_eq!(request_model.schema, "CreateItemRequestBody");
1122
1123 let inline_model = models.iter().find(|m| m.name() == "CreateItemRequestBody");
1125 assert!(
1126 inline_model.is_some(),
1127 "Expected a model named 'CreateItemRequestBody' to be generated"
1128 );
1129
1130 if let Some(ModelType::Struct(model)) = inline_model {
1131 assert_eq!(model.fields.len(), 2);
1132 assert_eq!(model.fields[0].name, "name");
1133 assert_eq!(model.fields[0].field_type, "String");
1134 assert!(model.fields[0].is_required);
1135
1136 assert_eq!(model.fields[1].name, "value");
1137 assert_eq!(model.fields[1].field_type, "i64");
1138 assert!(!model.fields[1].is_required);
1139 } else {
1140 panic!("Expected a Struct model for CreateItemRequestBody");
1141 }
1142 }
1143
1144 #[test]
1145 fn test_parse_ref_request_body_works() {
1146 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1147 "openapi": "3.0.0",
1148 "info": { "title": "Test API", "version": "1.0.0" },
1149 "components": {
1150 "schemas": {
1151 "ItemData": {
1152 "type": "object",
1153 "properties": {
1154 "name": { "type": "string" }
1155 }
1156 }
1157 },
1158 "requestBodies": {
1159 "CreateItem": {
1160 "content": {
1161 "application/json": {
1162 "schema": { "$ref": "#/components/schemas/ItemData" }
1163 }
1164 }
1165 }
1166 }
1167 },
1168 "paths": {
1169 "/items": {
1170 "post": {
1171 "operationId": "createItem",
1172 "requestBody": { "$ref": "#/components/requestBodies/CreateItem" },
1173 "responses": { "200": { "description": "OK" } }
1174 }
1175 }
1176 }
1177 }))
1178 .expect("Failed to deserialize OpenAPI spec");
1179
1180 let (models, requests, _responses) =
1181 parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1182
1183 assert_eq!(requests.len(), 1);
1185 let request_model = &requests[0];
1186 assert_eq!(request_model.name, "CreateItemRequest");
1187
1188 assert_eq!(request_model.schema, "ItemData");
1190
1191 assert!(models.iter().any(|m| m.name() == "ItemData"));
1193 }
1194
1195 #[test]
1196 fn test_parse_no_request_body() {
1197 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1198 "openapi": "3.0.0",
1199 "info": { "title": "Test API", "version": "1.0.0" },
1200 "paths": {
1201 "/items": {
1202 "get": {
1203 "operationId": "listItems",
1204 "responses": { "200": { "description": "OK" } }
1205 }
1206 }
1207 }
1208 }))
1209 .expect("Failed to deserialize OpenAPI spec");
1210
1211 let (_models, requests, _responses) =
1212 parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1213
1214 assert!(requests.is_empty());
1216 }
1217
1218 #[test]
1219 fn test_nullable_reference_field() {
1220 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1222 "openapi": "3.0.0",
1223 "info": { "title": "Test API", "version": "1.0.0" },
1224 "paths": {},
1225 "components": {
1226 "schemas": {
1227 "NullableUser": {
1228 "type": "object",
1229 "nullable": true,
1230 "properties": {
1231 "name": { "type": "string" }
1232 }
1233 },
1234 "Post": {
1235 "type": "object",
1236 "properties": {
1237 "author": {
1238 "$ref": "#/components/schemas/NullableUser"
1239 }
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 post_model = models.iter().find(|m| m.name() == "Post");
1251 assert!(post_model.is_some(), "Expected Post model to be generated");
1252
1253 if let Some(ModelType::Struct(post)) = post_model {
1254 let author_field = post.fields.iter().find(|f| f.name == "author");
1255 assert!(author_field.is_some(), "Expected author field");
1256
1257 let author = author_field.unwrap();
1260 assert!(
1261 author.is_nullable,
1262 "Expected author field to be nullable (from referenced schema)"
1263 );
1264 } else {
1265 panic!("Expected Post to be a Struct");
1266 }
1267 }
1268
1269 #[test]
1270 fn test_allof_required_fields_merge() {
1271 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1272 "openapi": "3.0.0",
1273 "info": { "title": "Test API", "version": "1.0.0" },
1274 "paths": {},
1275 "components": {
1276 "schemas": {
1277 "BaseEntity": {
1278 "type": "object",
1279 "properties": {
1280 "id": { "type": "string" },
1281 "created": { "type": "string" }
1282 },
1283 "required": ["id"]
1284 },
1285 "Person": {
1286 "allOf": [
1287 { "$ref": "#/components/schemas/BaseEntity" },
1288 {
1289 "type": "object",
1290 "properties": {
1291 "name": { "type": "string" },
1292 "age": { "type": "integer" }
1293 },
1294 "required": ["name"]
1295 }
1296 ]
1297 }
1298 }
1299 }
1300 }))
1301 .expect("Failed to deserialize OpenAPI spec");
1302
1303 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1304
1305 let person_model = models.iter().find(|m| m.name() == "Person");
1307 assert!(
1308 person_model.is_some(),
1309 "Expected Person model to be generated"
1310 );
1311
1312 if let Some(ModelType::Composition(person)) = person_model {
1313 let id_field = person.all_fields.iter().find(|f| f.name == "id");
1315 assert!(id_field.is_some(), "Expected id field");
1316 assert!(
1317 id_field.unwrap().is_required,
1318 "Expected id to be required from BaseEntity"
1319 );
1320
1321 let name_field = person.all_fields.iter().find(|f| f.name == "name");
1323 assert!(name_field.is_some(), "Expected name field");
1324 assert!(
1325 name_field.unwrap().is_required,
1326 "Expected name to be required from inline object"
1327 );
1328
1329 let created_field = person.all_fields.iter().find(|f| f.name == "created");
1331 assert!(created_field.is_some(), "Expected created field");
1332 assert!(
1333 !created_field.unwrap().is_required,
1334 "Expected created to be optional"
1335 );
1336
1337 let age_field = person.all_fields.iter().find(|f| f.name == "age");
1338 assert!(age_field.is_some(), "Expected age field");
1339 assert!(
1340 !age_field.unwrap().is_required,
1341 "Expected age to be optional"
1342 );
1343 } else {
1344 panic!("Expected Person to be a Composition");
1345 }
1346 }
1347
1348 #[test]
1349 fn test_x_rust_type_generates_type_alias() {
1350 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1351 "openapi": "3.0.0",
1352 "info": { "title": "Test API", "version": "1.0.0" },
1353 "paths": {},
1354 "components": {
1355 "schemas": {
1356 "User": {
1357 "type": "object",
1358 "x-rust-type": "crate::domain::User",
1359 "description": "Custom domain user type",
1360 "properties": {
1361 "name": { "type": "string" },
1362 "age": { "type": "integer" }
1363 }
1364 }
1365 }
1366 }
1367 }))
1368 .expect("Failed to deserialize OpenAPI spec");
1369
1370 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1371
1372 let user_model = models.iter().find(|m| m.name() == "User");
1374 assert!(user_model.is_some(), "Expected User model");
1375
1376 match user_model.unwrap() {
1377 ModelType::TypeAlias(alias) => {
1378 assert_eq!(alias.name, "User");
1379 assert_eq!(alias.target_type, "crate::domain::User");
1380 assert_eq!(
1381 alias.description,
1382 Some("Custom domain user type".to_string())
1383 );
1384 }
1385 _ => panic!("Expected TypeAlias, got different type"),
1386 }
1387 }
1388
1389 #[test]
1390 fn test_x_rust_type_works_with_enum() {
1391 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1392 "openapi": "3.0.0",
1393 "info": { "title": "Test API", "version": "1.0.0" },
1394 "paths": {},
1395 "components": {
1396 "schemas": {
1397 "Status": {
1398 "type": "string",
1399 "enum": ["active", "inactive"],
1400 "x-rust-type": "crate::domain::Status"
1401 }
1402 }
1403 }
1404 }))
1405 .expect("Failed to deserialize OpenAPI spec");
1406
1407 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1408
1409 let status_model = models.iter().find(|m| m.name() == "Status");
1410 assert!(status_model.is_some(), "Expected Status model");
1411
1412 assert!(
1414 matches!(status_model.unwrap(), ModelType::TypeAlias(_)),
1415 "Expected TypeAlias for enum with x-rust-type"
1416 );
1417 }
1418
1419 #[test]
1420 fn test_x_rust_type_works_with_oneof() {
1421 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1422 "openapi": "3.0.0",
1423 "info": { "title": "Test API", "version": "1.0.0" },
1424 "paths": {},
1425 "components": {
1426 "schemas": {
1427 "Payment": {
1428 "oneOf": [
1429 { "type": "object", "properties": { "card": { "type": "string" } } },
1430 { "type": "object", "properties": { "cash": { "type": "number" } } }
1431 ],
1432 "x-rust-type": "payments::Payment"
1433 }
1434 }
1435 }
1436 }))
1437 .expect("Failed to deserialize OpenAPI spec");
1438
1439 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1440
1441 let payment_model = models.iter().find(|m| m.name() == "Payment");
1442 assert!(payment_model.is_some(), "Expected Payment model");
1443
1444 match payment_model.unwrap() {
1446 ModelType::TypeAlias(alias) => {
1447 assert_eq!(alias.target_type, "payments::Payment");
1448 }
1449 _ => panic!("Expected TypeAlias for oneOf with x-rust-type"),
1450 }
1451 }
1452
1453 #[test]
1454 fn test_x_rust_attrs_on_struct() {
1455 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1456 "openapi": "3.0.0",
1457 "info": { "title": "Test API", "version": "1.0.0" },
1458 "paths": {},
1459 "components": {
1460 "schemas": {
1461 "User": {
1462 "type": "object",
1463 "x-rust-attrs": [
1464 "#[derive(Serialize, Deserialize)]",
1465 "#[serde(rename_all = \"camelCase\")]"
1466 ],
1467 "properties": {
1468 "name": { "type": "string" }
1469 }
1470 }
1471 }
1472 }
1473 }))
1474 .expect("Failed to deserialize OpenAPI spec");
1475
1476 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1477
1478 let user_model = models.iter().find(|m| m.name() == "User");
1479 assert!(user_model.is_some(), "Expected User model");
1480
1481 match user_model.unwrap() {
1482 ModelType::Struct(model) => {
1483 assert!(model.custom_attrs.is_some(), "Expected custom_attrs");
1484 let attrs = model.custom_attrs.as_ref().unwrap();
1485 assert_eq!(attrs.len(), 2);
1486 assert_eq!(attrs[0], "#[derive(Serialize, Deserialize)]");
1487 assert_eq!(attrs[1], "#[serde(rename_all = \"camelCase\")]");
1488 }
1489 _ => panic!("Expected Struct model"),
1490 }
1491 }
1492
1493 #[test]
1494 fn test_x_rust_attrs_on_enum() {
1495 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1496 "openapi": "3.0.0",
1497 "info": { "title": "Test API", "version": "1.0.0" },
1498 "paths": {},
1499 "components": {
1500 "schemas": {
1501 "Status": {
1502 "type": "string",
1503 "enum": ["active", "inactive"],
1504 "x-rust-attrs": ["#[serde(rename_all = \"UPPERCASE\")]"]
1505 }
1506 }
1507 }
1508 }))
1509 .expect("Failed to deserialize OpenAPI spec");
1510
1511 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1512
1513 let status_model = models.iter().find(|m| m.name() == "Status");
1514 assert!(status_model.is_some(), "Expected Status model");
1515
1516 match status_model.unwrap() {
1517 ModelType::Enum(enum_model) => {
1518 assert!(enum_model.custom_attrs.is_some());
1519 let attrs = enum_model.custom_attrs.as_ref().unwrap();
1520 assert_eq!(attrs.len(), 1);
1521 assert_eq!(attrs[0], "#[serde(rename_all = \"UPPERCASE\")]");
1522 }
1523 _ => panic!("Expected Enum model"),
1524 }
1525 }
1526
1527 #[test]
1528 fn test_x_rust_attrs_with_x_rust_type() {
1529 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1530 "openapi": "3.0.0",
1531 "info": { "title": "Test API", "version": "1.0.0" },
1532 "paths": {},
1533 "components": {
1534 "schemas": {
1535 "User": {
1536 "type": "object",
1537 "x-rust-type": "crate::domain::User",
1538 "x-rust-attrs": ["#[cfg_attr(test, derive(Default))]"],
1539 "properties": {
1540 "name": { "type": "string" }
1541 }
1542 }
1543 }
1544 }
1545 }))
1546 .expect("Failed to deserialize OpenAPI spec");
1547
1548 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1549
1550 let user_model = models.iter().find(|m| m.name() == "User");
1551 assert!(user_model.is_some(), "Expected User model");
1552
1553 match user_model.unwrap() {
1555 ModelType::TypeAlias(alias) => {
1556 assert_eq!(alias.target_type, "crate::domain::User");
1557 assert!(alias.custom_attrs.is_some());
1558 let attrs = alias.custom_attrs.as_ref().unwrap();
1559 assert_eq!(attrs.len(), 1);
1560 assert_eq!(attrs[0], "#[cfg_attr(test, derive(Default))]");
1561 }
1562 _ => panic!("Expected TypeAlias with custom attrs"),
1563 }
1564 }
1565
1566 #[test]
1567 fn test_x_rust_attrs_empty_array() {
1568 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1569 "openapi": "3.0.0",
1570 "info": { "title": "Test API", "version": "1.0.0" },
1571 "paths": {},
1572 "components": {
1573 "schemas": {
1574 "User": {
1575 "type": "object",
1576 "x-rust-attrs": [],
1577 "properties": {
1578 "name": { "type": "string" }
1579 }
1580 }
1581 }
1582 }
1583 }))
1584 .expect("Failed to deserialize OpenAPI spec");
1585
1586 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1587
1588 let user_model = models.iter().find(|m| m.name() == "User");
1589 assert!(user_model.is_some());
1590
1591 match user_model.unwrap() {
1592 ModelType::Struct(model) => {
1593 assert!(model.custom_attrs.is_none());
1595 }
1596 _ => panic!("Expected Struct"),
1597 }
1598 }
1599
1600 #[test]
1601 fn test_x_rust_type_on_string_property() {
1602 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1603 "openapi": "3.0.0",
1604 "info": { "title": "Test API", "version": "1.0.0" },
1605 "paths": {},
1606 "components": {
1607 "schemas": {
1608 "Document": {
1609 "type": "object",
1610 "description": "Document with custom version type",
1611 "properties": {
1612 "title": { "type": "string", "description": "Document title." },
1613 "content": { "type": "string", "description": "Document content." },
1614 "version": {
1615 "type": "string",
1616 "format": "semver",
1617 "x-rust-type": "semver::Version",
1618 "description": "Semantic version."
1619 }
1620 },
1621 "required": ["title", "content", "version"]
1622 }
1623 }
1624 }
1625 }))
1626 .expect("Failed to deserialize OpenAPI spec");
1627
1628 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1629
1630 let document_model = models.iter().find(|m| m.name() == "Document");
1631 assert!(document_model.is_some(), "Expected Document model");
1632
1633 match document_model.unwrap() {
1634 ModelType::Struct(model) => {
1635 let version_field = model.fields.iter().find(|f| f.name == "version");
1637 assert!(version_field.is_some(), "Expected version field");
1638 assert_eq!(version_field.unwrap().field_type, "semver::Version");
1639
1640 let title_field = model.fields.iter().find(|f| f.name == "title");
1642 assert_eq!(title_field.unwrap().field_type, "String");
1643
1644 let content_field = model.fields.iter().find(|f| f.name == "content");
1645 assert_eq!(content_field.unwrap().field_type, "String");
1646 }
1647 _ => panic!("Expected Struct"),
1648 }
1649 }
1650
1651 #[test]
1652 fn test_x_rust_type_on_integer_property() {
1653 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1654 "openapi": "3.0.0",
1655 "info": { "title": "Test API", "version": "1.0.0" },
1656 "paths": {},
1657 "components": {
1658 "schemas": {
1659 "Configuration": {
1660 "type": "object",
1661 "description": "Configuration with custom duration type",
1662 "properties": {
1663 "timeout": {
1664 "type": "integer",
1665 "x-rust-type": "std::time::Duration",
1666 "description": "Timeout duration."
1667 },
1668 "retries": { "type": "integer" }
1669 },
1670 "required": ["timeout", "retries"]
1671 }
1672 }
1673 }
1674 }))
1675 .expect("Failed to deserialize OpenAPI spec");
1676
1677 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1678
1679 let config_model = models.iter().find(|m| m.name() == "Configuration");
1680 assert!(config_model.is_some(), "Expected Configuration model");
1681
1682 match config_model.unwrap() {
1683 ModelType::Struct(model) => {
1684 let timeout_field = model.fields.iter().find(|f| f.name == "timeout");
1686 assert!(timeout_field.is_some(), "Expected timeout field");
1687 assert_eq!(timeout_field.unwrap().field_type, "std::time::Duration");
1688
1689 let retries_field = model.fields.iter().find(|f| f.name == "retries");
1691 assert_eq!(retries_field.unwrap().field_type, "i64");
1692 }
1693 _ => panic!("Expected Struct"),
1694 }
1695 }
1696
1697 #[test]
1698 fn test_x_rust_type_on_number_property() {
1699 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1700 "openapi": "3.0.0",
1701 "info": { "title": "Test API", "version": "1.0.0" },
1702 "paths": {},
1703 "components": {
1704 "schemas": {
1705 "Product": {
1706 "type": "object",
1707 "description": "Product with custom decimal type",
1708 "properties": {
1709 "price": {
1710 "type": "number",
1711 "x-rust-type": "decimal::Decimal",
1712 "description": "Product price."
1713 },
1714 "quantity": { "type": "number" }
1715 },
1716 "required": ["price", "quantity"]
1717 }
1718 }
1719 }
1720 }))
1721 .expect("Failed to deserialize OpenAPI spec");
1722
1723 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1724
1725 let product_model = models.iter().find(|m| m.name() == "Product");
1726 assert!(product_model.is_some(), "Expected Product model");
1727
1728 match product_model.unwrap() {
1729 ModelType::Struct(model) => {
1730 let price_field = model.fields.iter().find(|f| f.name == "price");
1732 assert!(price_field.is_some(), "Expected price field");
1733 assert_eq!(price_field.unwrap().field_type, "decimal::Decimal");
1734
1735 let quantity_field = model.fields.iter().find(|f| f.name == "quantity");
1737 assert_eq!(quantity_field.unwrap().field_type, "f64");
1738 }
1739 _ => panic!("Expected Struct"),
1740 }
1741 }
1742
1743 #[test]
1744 fn test_x_rust_type_on_nullable_property() {
1745 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1746 "openapi": "3.0.0",
1747 "info": { "title": "Test API", "version": "1.0.0" },
1748 "paths": {},
1749 "components": {
1750 "schemas": {
1751 "Settings": {
1752 "type": "object",
1753 "description": "Settings with nullable custom type",
1754 "properties": {
1755 "settings": {
1756 "type": "string",
1757 "x-rust-type": "serde_json::Value",
1758 "nullable": true,
1759 "description": "Optional settings."
1760 }
1761 }
1762 }
1763 }
1764 }
1765 }))
1766 .expect("Failed to deserialize OpenAPI spec");
1767
1768 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1769
1770 let settings_model = models.iter().find(|m| m.name() == "Settings");
1771 assert!(settings_model.is_some(), "Expected Settings model");
1772
1773 match settings_model.unwrap() {
1774 ModelType::Struct(model) => {
1775 let settings_field = model.fields.iter().find(|f| f.name == "settings");
1776 assert!(settings_field.is_some(), "Expected settings field");
1777
1778 let field = settings_field.unwrap();
1779 assert_eq!(field.field_type, "serde_json::Value");
1780 assert!(field.is_nullable, "Expected field to be nullable");
1781 }
1782 _ => panic!("Expected Struct"),
1783 }
1784 }
1785
1786 #[test]
1787 fn test_multiple_properties_with_x_rust_type() {
1788 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1789 "openapi": "3.0.0",
1790 "info": { "title": "Test API", "version": "1.0.0" },
1791 "paths": {},
1792 "components": {
1793 "schemas": {
1794 "ComplexModel": {
1795 "type": "object",
1796 "description": "Model with multiple custom-typed properties",
1797 "properties": {
1798 "id": {
1799 "type": "string",
1800 "format": "uuid",
1801 "x-rust-type": "uuid::Uuid"
1802 },
1803 "price": {
1804 "type": "number",
1805 "x-rust-type": "decimal::Decimal"
1806 },
1807 "timeout": {
1808 "type": "integer",
1809 "x-rust-type": "std::time::Duration"
1810 },
1811 "regular_field": { "type": "string" }
1812 },
1813 "required": ["id", "price", "timeout"]
1814 }
1815 }
1816 }
1817 }))
1818 .expect("Failed to deserialize OpenAPI spec");
1819
1820 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1821
1822 let model = models.iter().find(|m| m.name() == "ComplexModel");
1823 assert!(model.is_some(), "Expected ComplexModel model");
1824
1825 match model.unwrap() {
1826 ModelType::Struct(struct_model) => {
1827 let id_field = struct_model.fields.iter().find(|f| f.name == "id");
1829 assert_eq!(id_field.unwrap().field_type, "uuid::Uuid");
1830
1831 let price_field = struct_model.fields.iter().find(|f| f.name == "price");
1832 assert_eq!(price_field.unwrap().field_type, "decimal::Decimal");
1833
1834 let timeout_field = struct_model.fields.iter().find(|f| f.name == "timeout");
1835 assert_eq!(timeout_field.unwrap().field_type, "std::time::Duration");
1836
1837 let regular_field = struct_model
1839 .fields
1840 .iter()
1841 .find(|f| f.name == "regular_field");
1842 assert_eq!(regular_field.unwrap().field_type, "String");
1843
1844 assert!(!id_field.unwrap().is_nullable, "id should not be nullable");
1846 assert!(
1847 !price_field.unwrap().is_nullable,
1848 "price should not be nullable"
1849 );
1850 assert!(
1851 !timeout_field.unwrap().is_nullable,
1852 "timeout should not be nullable"
1853 );
1854 }
1857 _ => panic!("Expected Struct"),
1858 }
1859 }
1860}