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