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 name,
307 field_name,
308 &ReferenceOr::Item((**boxed_schema).clone()),
309 all_schemas,
310 )?,
311 ReferenceOr::Reference { reference } => extract_field_info(
312 name,
313 field_name,
314 &ReferenceOr::Reference {
315 reference: reference.clone(),
316 },
317 all_schemas,
318 )?,
319 };
320 if let Some(inline_model) = inline_model {
321 inline_models.push(inline_model);
322 }
323 let is_required = obj.required.contains(field_name);
324 fields.push(Field {
325 name: field_name.clone(),
326 field_type: field_info.field_type,
327 format: field_info.format,
328 is_required,
329 is_array_ref: field_info.is_array_ref,
330 is_nullable: field_info.is_nullable,
331 description: field_info.description,
332 custom_attrs: field_info.custom_attrs,
333 validation_rules: field_info.validation_rules,
334 });
335 }
336
337 let mut models = inline_models;
338 if obj.properties.is_empty() && obj.additional_properties.is_none() {
339 models.push(ModelType::Struct(Model {
340 name: to_pascal_case(name),
341 fields: vec![],
342 custom_attrs: extract_custom_attrs(schema),
343 description: schema.schema_data.description.clone(),
344 }));
345 } else if !fields.is_empty() {
346 models.push(ModelType::Struct(Model {
347 name: to_pascal_case(name),
348 fields,
349 custom_attrs: extract_custom_attrs(schema),
350 description: schema.schema_data.description.clone(),
351 }));
352 }
353 Ok(models)
354 }
355
356 SchemaKind::AllOf { all_of } => {
358 let (all_fields, inline_models) =
359 resolve_all_of_fields(name, all_of, all_schemas)?;
360 let mut models = inline_models;
361
362 if !all_fields.is_empty() {
363 models.push(ModelType::Composition(CompositionModel {
364 name: to_pascal_case(name),
365 all_fields,
366 custom_attrs: extract_custom_attrs(schema),
367 }));
368 }
369
370 Ok(models)
371 }
372
373 SchemaKind::OneOf { one_of } => {
375 let (variants, inline_models) =
376 resolve_union_variants(name, one_of, all_schemas)?;
377 let mut models = inline_models;
378
379 models.push(ModelType::Union(UnionModel {
380 name: to_pascal_case(name),
381 variants,
382 union_type: UnionType::OneOf,
383 custom_attrs: extract_custom_attrs(schema),
384 }));
385
386 Ok(models)
387 }
388
389 SchemaKind::AnyOf { any_of } => {
391 let (variants, inline_models) =
392 resolve_union_variants(name, any_of, all_schemas)?;
393 let mut models = inline_models;
394
395 models.push(ModelType::Union(UnionModel {
396 name: to_pascal_case(name),
397 variants,
398 union_type: UnionType::AnyOf,
399 custom_attrs: extract_custom_attrs(schema),
400 }));
401
402 Ok(models)
403 }
404
405 SchemaKind::Type(Type::String(string_type)) => {
407 if !string_type.enumeration.is_empty() {
408 let variants: Vec<String> = string_type
409 .enumeration
410 .iter()
411 .filter_map(|value| value.clone())
412 .collect();
413
414 if !variants.is_empty() {
415 let models = vec![ModelType::Enum(EnumModel {
416 name: to_pascal_case(name),
417 variants,
418 description: schema.schema_data.description.clone(),
419 custom_attrs: extract_custom_attrs(schema),
420 })];
421
422 return Ok(models);
423 }
424 }
425 Ok(Vec::new())
426 }
427
428 SchemaKind::Type(Type::Array(array)) => {
429 let mut models = Vec::new();
430 let array_name = to_pascal_case(name);
431
432 let items = match &array.items {
433 Some(items) => items,
434 None => return Ok(Vec::new()),
435 };
436
437 match items {
438 ReferenceOr::Item(item_schema) => match &item_schema.schema_kind {
439 SchemaKind::OneOf { one_of } => {
440 let item_type_name = format!("{array_name}Item");
441
442 let (variants, inline_models) =
443 resolve_union_variants(&item_type_name, one_of, all_schemas)?;
444
445 models.extend(inline_models);
446
447 models.push(ModelType::Union(UnionModel {
448 name: item_type_name.clone(),
449 variants,
450 union_type: UnionType::OneOf,
451 custom_attrs: extract_custom_attrs(item_schema),
452 }));
453
454 models.push(ModelType::TypeAlias(TypeAliasModel {
455 name: array_name,
456 target_type: format!("Vec<{item_type_name}>"),
457 description: schema.schema_data.description.clone(),
458 custom_attrs: extract_custom_attrs(schema),
459 }));
460 }
461
462 SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
463 let item_type_name = format!("{array_name}Item");
464
465 let variants: Vec<String> =
466 s.enumeration.iter().filter_map(|v| v.clone()).collect();
467
468 models.push(ModelType::Enum(EnumModel {
469 name: item_type_name.clone(),
470 variants,
471 description: item_schema.schema_data.description.clone(),
472 custom_attrs: extract_custom_attrs(item_schema),
473 }));
474
475 models.push(ModelType::TypeAlias(TypeAliasModel {
476 name: array_name,
477 target_type: format!("Vec<{item_type_name}>"),
478 description: schema.schema_data.description.clone(),
479 custom_attrs: extract_custom_attrs(schema),
480 }));
481 }
482
483 SchemaKind::Type(Type::Integer(n)) if !n.enumeration.is_empty() => {
484 let item_type_name = format!("{array_name}Item");
485
486 let variants: Vec<String> = n
487 .enumeration
488 .iter()
489 .filter_map(|v| v.map(|num| format!("Value{num}")))
490 .collect();
491
492 models.push(ModelType::Enum(EnumModel {
493 name: item_type_name.clone(),
494 variants,
495 description: item_schema.schema_data.description.clone(),
496 custom_attrs: extract_custom_attrs(item_schema),
497 }));
498
499 models.push(ModelType::TypeAlias(TypeAliasModel {
500 name: array_name,
501 target_type: format!("Vec<{item_type_name}>"),
502 description: schema.schema_data.description.clone(),
503 custom_attrs: extract_custom_attrs(schema),
504 }));
505 }
506
507 _ => {
508 let normalized_items = match items {
509 ReferenceOr::Item(boxed_schema) => {
510 ReferenceOr::Item((**boxed_schema).clone())
511 }
512 ReferenceOr::Reference { reference } => {
513 ReferenceOr::Reference {
514 reference: reference.clone(),
515 }
516 }
517 };
518
519 let (inner_type, _) =
520 extract_type_and_format(&normalized_items, all_schemas)?;
521
522 models.push(ModelType::TypeAlias(TypeAliasModel {
523 name: array_name,
524 target_type: format!("Vec<{inner_type}>"),
525 description: schema.schema_data.description.clone(),
526 custom_attrs: extract_custom_attrs(schema),
527 }));
528 }
529 },
530
531 ReferenceOr::Reference { .. } => {
532 let normalized_items = match items {
533 ReferenceOr::Item(boxed_schema) => {
534 ReferenceOr::Item((**boxed_schema).clone())
535 }
536 ReferenceOr::Reference { reference } => ReferenceOr::Reference {
537 reference: reference.clone(),
538 },
539 };
540
541 let (inner_type, _) =
542 extract_type_and_format(&normalized_items, all_schemas)?;
543
544 models.push(ModelType::TypeAlias(TypeAliasModel {
545 name: array_name,
546 target_type: format!("Vec<{inner_type}>"),
547 description: schema.schema_data.description.clone(),
548 custom_attrs: extract_custom_attrs(schema),
549 }));
550 }
551 }
552
553 Ok(models)
554 }
555
556 _ => Ok(Vec::new()),
557 }
558 }
559 }
560}
561
562fn extract_validation_rules(schema: &Schema) -> Option<crate::models::ValidationRules> {
563 use crate::models::ValidationRules;
564
565 let mut rules = ValidationRules::default();
566
567 match &schema.schema_kind {
569 openapiv3::SchemaKind::Type(openapiv3::Type::String(string_type)) => {
570 match &string_type.format {
572 openapiv3::VariantOrUnknownOrEmpty::Item(_fmt) => {
573 }
576 openapiv3::VariantOrUnknownOrEmpty::Unknown(unknown_format) => {
577 match unknown_format.to_lowercase().as_str() {
578 "email" => rules.email = true,
579 "uri" | "url" => rules.url = true,
580 _ => {}
581 }
582 }
583 _ => {}
584 }
585
586 rules.min_length = string_type.min_length;
588 rules.max_length = string_type.max_length;
589 rules.pattern = string_type.pattern.clone();
590 }
591 openapiv3::SchemaKind::Type(openapiv3::Type::Integer(integer_type)) => {
592 rules.minimum = integer_type.minimum.map(|v| v as f64);
593 rules.maximum = integer_type.maximum.map(|v| v as f64);
594 rules.exclusive_minimum = integer_type.exclusive_minimum;
595 rules.exclusive_maximum = integer_type.exclusive_maximum;
596 rules.multiple_of = integer_type.multiple_of.map(|v| v as f64);
597 }
598 openapiv3::SchemaKind::Type(openapiv3::Type::Number(number_type)) => {
599 rules.minimum = number_type.minimum;
600 rules.maximum = number_type.maximum;
601 rules.exclusive_minimum = number_type.exclusive_minimum;
602 rules.exclusive_maximum = number_type.exclusive_maximum;
603 rules.multiple_of = number_type.multiple_of;
604 }
605 openapiv3::SchemaKind::Type(openapiv3::Type::Array(array_type)) => {
606 rules.min_items = array_type.min_items;
607 rules.max_items = array_type.max_items;
608 rules.unique_items = array_type.unique_items;
609 }
610 _ => {}
611 }
612
613 if rules.has_any() {
614 Some(rules)
615 } else {
616 None
617 }
618}
619
620fn extract_type_and_format(
621 schema: &ReferenceOr<Schema>,
622 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
623) -> Result<(String, String)> {
624 match schema {
625 ReferenceOr::Reference { reference } => {
626 let type_name = reference.split('/').next_back().unwrap_or("Unknown");
627
628 if let Some(ReferenceOr::Item(schema)) = all_schemas.get(type_name) {
629 if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
630 return Ok((to_pascal_case(type_name), "oneOf".to_string()));
631 }
632 }
633 Ok((to_pascal_case(type_name), "reference".to_string()))
634 }
635
636 ReferenceOr::Item(schema) => match &schema.schema_kind {
637 SchemaKind::Type(Type::String(string_type)) => match &string_type.format {
638 VariantOrUnknownOrEmpty::Item(fmt) => match fmt {
639 StringFormat::DateTime => {
640 Ok(("DateTime<Utc>".to_string(), "date-time".to_string()))
641 }
642 StringFormat::Date => Ok(("NaiveDate".to_string(), "date".to_string())),
643 _ => Ok(("String".to_string(), format!("{fmt:?}"))),
644 },
645 VariantOrUnknownOrEmpty::Unknown(unknown_format) => {
646 if unknown_format.to_lowercase() == "uuid" {
647 Ok(("Uuid".to_string(), "uuid".to_string()))
648 } else {
649 Ok(("String".to_string(), unknown_format.clone()))
650 }
651 }
652 _ => Ok(("String".to_string(), "string".to_string())),
653 },
654 SchemaKind::Type(Type::Integer(_)) => Ok(("i64".to_string(), "integer".to_string())),
655 SchemaKind::Type(Type::Number(_)) => Ok(("f64".to_string(), "number".to_string())),
656 SchemaKind::Type(Type::Boolean(_)) => Ok(("bool".to_string(), "boolean".to_string())),
657 SchemaKind::Type(Type::Array(arr)) => {
658 if let Some(items) = &arr.items {
659 match items {
660 ReferenceOr::Item(boxed_schema) => extract_type_and_format(
661 &ReferenceOr::Item((**boxed_schema).clone()),
662 all_schemas,
663 ),
664
665 ReferenceOr::Reference { reference } => extract_type_and_format(
666 &ReferenceOr::Reference {
667 reference: reference.clone(),
668 },
669 all_schemas,
670 ),
671 }
672 } else {
673 Ok(("serde_json::Value".to_string(), "array".to_string()))
674 }
675 }
676 SchemaKind::Type(Type::Object(_obj)) => {
677 Ok(("serde_json::Value".to_string(), "object".to_string()))
678 }
679 _ => Ok(("serde_json::Value".to_string(), "unknown".to_string())),
680 },
681 }
682}
683
684fn extract_field_info(
686 parent_name: &str,
687 field_name: &str,
688 schema: &ReferenceOr<Schema>,
689 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
690) -> Result<(FieldInfo, Option<ModelType>)> {
691 let (mut field_type, format) = extract_type_and_format(schema, all_schemas)?;
692
693 let (is_nullable, is_array_ref, en, description, custom_attrs, validation_rules) = match schema
694 {
695 ReferenceOr::Reference { reference } => {
696 let is_array_ref = false;
697 let mut is_nullable = false;
698 let mut validation_rules = None;
699
700 if let Some(type_name) = reference.strip_prefix("#/components/schemas/") {
701 if let Some(ReferenceOr::Item(schema)) = all_schemas.get(type_name) {
702 is_nullable = schema.schema_data.nullable;
703 validation_rules = extract_validation_rules(schema);
707 }
708 }
709
710 (
711 is_nullable,
712 is_array_ref,
713 None,
714 None,
715 None,
716 validation_rules,
717 )
718 }
719
720 ReferenceOr::Item(schema) => {
721 if let Some(rust_type) = schema.schema_data.extensions.get(X_RUST_TYPE) {
722 if let Some(type_str) = rust_type.as_str() {
723 field_type = type_str.to_string();
724 }
725 }
726
727 let is_nullable = schema.schema_data.nullable;
728 let is_array_ref = matches!(schema.schema_kind, SchemaKind::Type(Type::Array(_)));
729 let description = schema.schema_data.description.clone();
730 let validation_rules = extract_validation_rules(schema);
731
732 let maybe_enum = match &schema.schema_kind {
733 SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
734 let variants: Vec<String> =
735 s.enumeration.iter().filter_map(|v| v.clone()).collect();
736 let enum_name = format!("{}{}", parent_name, to_pascal_case(field_name));
737 field_type = enum_name.clone();
738 Some(ModelType::Enum(EnumModel {
739 name: enum_name,
740 variants,
741 description: schema.schema_data.description.clone(),
742 custom_attrs: extract_custom_attrs(schema),
743 }))
744 }
745 SchemaKind::Type(Type::Object(obj)) => {
746 if obj.properties.is_empty() {
747 if let Some(additional_props) = &obj.additional_properties {
748 match additional_props {
749 AdditionalProperties::Schema(schema) => {
750 let (value_type, _) =
751 extract_type_and_format(&schema.clone(), all_schemas)?;
752
753 field_type = format!(
754 "std::collections::HashMap<String, {}>",
755 value_type
756 );
757 }
758
759 AdditionalProperties::Any(true) => {
760 field_type =
761 "std::collections::HashMap<String, serde_json::Value>"
762 .to_string();
763 }
764
765 AdditionalProperties::Any(false) => {
766 field_type = "serde_json::Value".to_string();
768 }
769 }
770 None
771 } else {
772 field_type = "serde_json::Value".to_string();
773 None
774 }
775 } else {
776 let struct_name = to_pascal_case(field_name);
777 field_type = struct_name.clone();
778
779 let wrapped_schema = ReferenceOr::Item(schema.clone());
780 let models =
781 parse_schema_to_model_type(&struct_name, &wrapped_schema, all_schemas)?;
782
783 models
784 .into_iter()
785 .find(|m| matches!(m, ModelType::Struct(_)))
786 }
787 }
788 _ => None,
789 };
790 let field_custom_attrs = if maybe_enum.is_some() {
794 None
795 } else {
796 extract_custom_attrs(schema)
797 };
798 (
799 is_nullable,
800 is_array_ref,
801 maybe_enum,
802 description,
803 field_custom_attrs,
804 validation_rules,
805 )
806 }
807 };
808
809 Ok((
810 FieldInfo {
811 field_type,
812 format,
813 is_nullable,
814 is_array_ref,
815 description,
816 custom_attrs,
817 validation_rules,
818 },
819 en,
820 ))
821}
822
823fn resolve_all_of_fields(
824 name: &str,
825 all_of: &[ReferenceOr<Schema>],
826 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
827) -> Result<(Vec<Field>, Vec<ModelType>)> {
828 let mut all_fields: IndexMap<String, Field> = IndexMap::new();
829 let mut models = Vec::new();
830 let mut all_required_fields = HashSet::new();
831
832 for schema_ref in all_of {
833 let schema_to_check = match schema_ref {
834 ReferenceOr::Reference { reference } => reference
835 .strip_prefix("#/components/schemas/")
836 .and_then(|schema_name| all_schemas.get(schema_name)),
837 ReferenceOr::Item(_) => Some(schema_ref),
838 };
839
840 if let Some(ReferenceOr::Item(schema)) = schema_to_check {
841 if let SchemaKind::Type(Type::Object(obj)) = &schema.schema_kind {
842 all_required_fields.extend(obj.required.iter().cloned());
843 }
844 }
845 }
846
847 const PRIMITIVE_TYPES: &[&str] = &["String", "i64", "f64", "bool"];
852
853 fn less_value(fields: Vec<Field>, all_fields: &mut IndexMap<String, Field>) {
864 for field in fields {
865 if let Some(existing_field) = all_fields.get_mut(&field.name) {
866 if existing_field.field_type == "serde_json::Value" {
868 *existing_field = field;
869 } else if existing_field.field_type == "Option<serde_json::Value>" {
870 existing_field.field_type = format!("Option<{}>", field.field_type);
871 } else if existing_field.field_type
873 == "std::collections::HashMap<String, serde_json::Value>"
874 {
875 *existing_field = field;
876 } else if existing_field.field_type
877 == "Option<std::collections::HashMap<String, serde_json::Value>>"
878 {
879 existing_field.field_type = format!("Option<{}>", field.field_type);
880 } else if existing_field.field_type == "Vec<serde_json::Value>" {
882 existing_field.field_type = format!("Vec<{}>", field.field_type);
883 } else if existing_field.field_type == "Option<Vec<serde_json::Value>>" {
884 existing_field.field_type = format!("Option<Vec<{}>>", field.field_type);
885 } else if PRIMITIVE_TYPES.contains(&existing_field.field_type.as_str())
890 && !PRIMITIVE_TYPES.contains(&field.field_type.as_str())
891 && field.field_type != "serde_json::Value"
892 {
893 existing_field.field_type = field.field_type;
894 }
895 } else {
896 all_fields.insert(field.name.clone(), field);
897 }
898 }
899 }
900
901 for schema_ref in all_of {
903 match schema_ref {
904 ReferenceOr::Reference { reference } => {
905 if let Some(schema_name) = reference.strip_prefix("#/components/schemas/") {
906 if let Some(referenced_schema) = all_schemas.get(schema_name) {
907 let (fields, inline_models) =
908 extract_fields_from_schema(name, referenced_schema, all_schemas)?;
909 less_value(fields, &mut all_fields);
911 models.extend(inline_models);
912 }
913 }
914 }
915 ReferenceOr::Item(_schema) => {
916 let (fields, inline_models) =
917 extract_fields_from_schema(name, schema_ref, all_schemas)?;
918 less_value(fields, &mut all_fields);
920 models.extend(inline_models);
921 }
922 }
923 }
924
925 for field in all_fields.values_mut() {
927 if all_required_fields.contains(&field.name) {
928 field.is_required = true;
929 }
930 }
931
932 Ok((all_fields.into_values().collect(), models))
933}
934
935fn resolve_union_variants(
936 name: &str,
937 schemas: &[ReferenceOr<Schema>],
938 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
939) -> Result<(Vec<UnionVariant>, Vec<ModelType>)> {
940 use std::collections::BTreeSet;
941
942 let mut variants = Vec::new();
943 let mut models = Vec::new();
944 let mut enum_values: BTreeSet<String> = BTreeSet::new();
945 let mut is_all_simple_enum = true;
946
947 for schema_ref in schemas {
948 let resolved = match schema_ref {
949 ReferenceOr::Reference { reference } => reference
950 .strip_prefix("#/components/schemas/")
951 .and_then(|n| all_schemas.get(n)),
952 ReferenceOr::Item(_) => Some(schema_ref),
953 };
954
955 let Some(resolved_schema) = resolved else {
956 is_all_simple_enum = false;
957 continue;
958 };
959
960 match resolved_schema {
961 ReferenceOr::Item(schema) => match &schema.schema_kind {
962 SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
963 enum_values.extend(s.enumeration.iter().filter_map(|v| v.as_ref().cloned()));
964 }
965 SchemaKind::Type(Type::Integer(n)) if !n.enumeration.is_empty() => {
966 enum_values.extend(
967 n.enumeration
968 .iter()
969 .filter_map(|v| v.map(|num| format!("Value{num}"))),
970 );
971 }
972
973 _ => is_all_simple_enum = false,
974 },
975 ReferenceOr::Reference { reference } => {
976 if let Some(n) = reference.strip_prefix("#/components/schemas/") {
977 if let Some(ReferenceOr::Item(inner)) = all_schemas.get(n) {
978 if let SchemaKind::Type(Type::String(s)) = &inner.schema_kind {
979 let values: Vec<String> = s
980 .enumeration
981 .iter()
982 .filter_map(|v| v.as_ref().cloned())
983 .collect();
984 enum_values.extend(values);
985 } else {
986 is_all_simple_enum = false;
987 }
988 }
989 }
990 }
991 }
992 }
993 if is_all_simple_enum && !enum_values.is_empty() {
994 let enum_name = to_pascal_case(name);
995 let enum_model = ModelType::Enum(EnumModel {
996 name: enum_name.clone(),
997 variants: enum_values.iter().map(|v| to_pascal_case(v)).collect(),
998 description: None,
999 custom_attrs: None, });
1001
1002 return Ok((vec![], vec![enum_model]));
1003 }
1004
1005 for (index, schema_ref) in schemas.iter().enumerate() {
1007 match schema_ref {
1008 ReferenceOr::Reference { reference } => {
1009 if let Some(schema_name) = reference.strip_prefix("#/components/schemas/") {
1010 if let Some(referenced_schema) = all_schemas.get(schema_name) {
1011 if let ReferenceOr::Item(schema) = referenced_schema {
1012 if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
1013 variants.push(UnionVariant {
1014 name: to_pascal_case(schema_name),
1015 fields: vec![],
1016 primitive_type: None,
1017 });
1018 } else {
1019 let (fields, inline_models) = extract_fields_from_schema(
1020 schema_name,
1021 referenced_schema,
1022 all_schemas,
1023 )?;
1024 variants.push(UnionVariant {
1025 name: to_pascal_case(schema_name),
1026 fields,
1027 primitive_type: None,
1028 });
1029 models.extend(inline_models);
1030 }
1031 }
1032 }
1033 }
1034 }
1035 ReferenceOr::Item(schema) => match &schema.schema_kind {
1036 SchemaKind::Type(Type::String(_)) => {
1037 variants.push(UnionVariant {
1038 name: "String".to_string(),
1039 fields: vec![],
1040 primitive_type: Some("String".to_string()),
1041 });
1042 }
1043
1044 SchemaKind::Type(Type::Integer(_)) => {
1045 variants.push(UnionVariant {
1046 name: "Integer".to_string(),
1047 fields: vec![],
1048 primitive_type: Some("i64".to_string()),
1049 });
1050 }
1051
1052 SchemaKind::Type(Type::Number(_)) => {
1053 variants.push(UnionVariant {
1054 name: "Number".to_string(),
1055 fields: vec![],
1056 primitive_type: Some("f64".to_string()),
1057 });
1058 }
1059
1060 SchemaKind::Type(Type::Boolean(_)) => {
1061 variants.push(UnionVariant {
1062 name: "Boolean".to_string(),
1063 fields: vec![],
1064 primitive_type: Some("Boolean".to_string()),
1065 });
1066 }
1067
1068 _ => {
1069 let (fields, inline_models) =
1070 extract_fields_from_schema(name, schema_ref, all_schemas)?;
1071 let variant_name = format!("Variant{index}");
1072 variants.push(UnionVariant {
1073 name: variant_name,
1074 fields,
1075 primitive_type: None,
1076 });
1077 models.extend(inline_models);
1078 }
1079 },
1080 }
1081 }
1082
1083 Ok((variants, models))
1084}
1085
1086fn extract_fields_from_schema(
1087 parent_name: &str,
1088 schema_ref: &ReferenceOr<Schema>,
1089 all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
1090) -> Result<(Vec<Field>, Vec<ModelType>)> {
1091 let mut fields = Vec::new();
1092 let mut inline_models = Vec::new();
1093
1094 match schema_ref {
1095 ReferenceOr::Reference { .. } => Ok((fields, inline_models)),
1096 ReferenceOr::Item(schema) => {
1097 match &schema.schema_kind {
1098 SchemaKind::Type(Type::Object(obj)) => {
1099 for (field_name, field_schema) in &obj.properties {
1100 let (field_info, inline_model) = match field_schema {
1101 ReferenceOr::Item(boxed_schema) => extract_field_info(
1102 parent_name,
1103 field_name,
1104 &ReferenceOr::Item((**boxed_schema).clone()),
1105 all_schemas,
1106 )?,
1107 ReferenceOr::Reference { reference } => extract_field_info(
1108 parent_name,
1109 field_name,
1110 &ReferenceOr::Reference {
1111 reference: reference.clone(),
1112 },
1113 all_schemas,
1114 )?,
1115 };
1116
1117 let is_nullable = field_info.is_nullable
1118 || field_name == "value"
1119 || field_name == "default_value";
1120
1121 let field_type = field_info.field_type.clone();
1122
1123 let is_required = obj.required.contains(field_name);
1124 fields.push(Field {
1125 name: field_name.clone(),
1126 field_type,
1127 format: field_info.format,
1128 is_required,
1129 is_nullable,
1130 is_array_ref: field_info.is_array_ref,
1131 description: field_info.description,
1132 custom_attrs: field_info.custom_attrs,
1133 validation_rules: field_info.validation_rules,
1134 });
1135 if let Some(inline_model) = inline_model {
1136 match &inline_model {
1137 ModelType::Struct(m) if m.fields.is_empty() => {}
1138 _ => inline_models.push(inline_model),
1139 }
1140 }
1141 }
1142 }
1143 SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
1144 let name = schema
1145 .schema_data
1146 .title
1147 .clone()
1148 .unwrap_or_else(|| "AnonymousStringEnum".to_string());
1149
1150 let enum_model = ModelType::Enum(EnumModel {
1151 name,
1152 variants: s
1153 .enumeration
1154 .iter()
1155 .filter_map(|v| v.as_ref().map(|s| to_pascal_case(s)))
1156 .collect(),
1157 description: schema.schema_data.description.clone(),
1158 custom_attrs: extract_custom_attrs(schema),
1159 });
1160
1161 inline_models.push(enum_model);
1162 }
1163 SchemaKind::Type(Type::Integer(n)) if !n.enumeration.is_empty() => {
1164 let name = schema
1165 .schema_data
1166 .title
1167 .clone()
1168 .unwrap_or_else(|| "AnonymousIntEnum".to_string());
1169
1170 let enum_model = ModelType::Enum(EnumModel {
1171 name,
1172 variants: n
1173 .enumeration
1174 .iter()
1175 .filter_map(|v| v.map(|num| format!("Value{num}")))
1176 .collect(),
1177 description: schema.schema_data.description.clone(),
1178 custom_attrs: extract_custom_attrs(schema),
1179 });
1180
1181 inline_models.push(enum_model);
1182 }
1183
1184 _ => {}
1185 }
1186
1187 Ok((fields, inline_models))
1188 }
1189 }
1190}
1191
1192#[cfg(test)]
1193mod tests {
1194 use super::*;
1195 use serde_json::json;
1196
1197 #[test]
1198 fn test_parse_inline_request_body_generates_model() {
1199 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1200 "openapi": "3.0.0",
1201 "info": { "title": "Test API", "version": "1.0.0" },
1202 "paths": {
1203 "/items": {
1204 "post": {
1205 "operationId": "createItem",
1206 "requestBody": {
1207 "content": {
1208 "application/json": {
1209 "schema": {
1210 "type": "object",
1211 "properties": {
1212 "name": { "type": "string" },
1213 "value": { "type": "integer" }
1214 },
1215 "required": ["name"]
1216 }
1217 }
1218 }
1219 },
1220 "responses": { "200": { "description": "OK" } }
1221 }
1222 }
1223 }
1224 }))
1225 .expect("Failed to deserialize OpenAPI spec");
1226
1227 let (models, requests, _responses) =
1228 parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1229
1230 assert_eq!(requests.len(), 1);
1232 let request_model = &requests[0];
1233 assert_eq!(request_model.name, "CreateItemRequest");
1234
1235 assert_eq!(request_model.schema, "CreateItemRequestBody");
1237
1238 let inline_model = models.iter().find(|m| m.name() == "CreateItemRequestBody");
1240 assert!(
1241 inline_model.is_some(),
1242 "Expected a model named 'CreateItemRequestBody' to be generated"
1243 );
1244
1245 if let Some(ModelType::Struct(model)) = inline_model {
1246 assert_eq!(model.fields.len(), 2);
1247 assert_eq!(model.fields[0].name, "name");
1248 assert_eq!(model.fields[0].field_type, "String");
1249 assert!(model.fields[0].is_required);
1250
1251 assert_eq!(model.fields[1].name, "value");
1252 assert_eq!(model.fields[1].field_type, "i64");
1253 assert!(!model.fields[1].is_required);
1254 } else {
1255 panic!("Expected a Struct model for CreateItemRequestBody");
1256 }
1257 }
1258
1259 #[test]
1260 fn test_parse_ref_request_body_works() {
1261 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1262 "openapi": "3.0.0",
1263 "info": { "title": "Test API", "version": "1.0.0" },
1264 "components": {
1265 "schemas": {
1266 "ItemData": {
1267 "type": "object",
1268 "properties": {
1269 "name": { "type": "string" }
1270 }
1271 }
1272 },
1273 "requestBodies": {
1274 "CreateItem": {
1275 "content": {
1276 "application/json": {
1277 "schema": { "$ref": "#/components/schemas/ItemData" }
1278 }
1279 }
1280 }
1281 }
1282 },
1283 "paths": {
1284 "/items": {
1285 "post": {
1286 "operationId": "createItem",
1287 "requestBody": { "$ref": "#/components/requestBodies/CreateItem" },
1288 "responses": { "200": { "description": "OK" } }
1289 }
1290 }
1291 }
1292 }))
1293 .expect("Failed to deserialize OpenAPI spec");
1294
1295 let (models, requests, _responses) =
1296 parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1297
1298 assert_eq!(requests.len(), 1);
1300 let request_model = &requests[0];
1301 assert_eq!(request_model.name, "CreateItemRequest");
1302
1303 assert_eq!(request_model.schema, "ItemData");
1305
1306 assert!(models.iter().any(|m| m.name() == "ItemData"));
1308 }
1309
1310 #[test]
1311 fn test_parse_no_request_body() {
1312 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1313 "openapi": "3.0.0",
1314 "info": { "title": "Test API", "version": "1.0.0" },
1315 "paths": {
1316 "/items": {
1317 "get": {
1318 "operationId": "listItems",
1319 "responses": { "200": { "description": "OK" } }
1320 }
1321 }
1322 }
1323 }))
1324 .expect("Failed to deserialize OpenAPI spec");
1325
1326 let (_models, requests, _responses) =
1327 parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1328
1329 assert!(requests.is_empty());
1331 }
1332
1333 #[test]
1334 fn test_nullable_reference_field() {
1335 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1337 "openapi": "3.0.0",
1338 "info": { "title": "Test API", "version": "1.0.0" },
1339 "paths": {},
1340 "components": {
1341 "schemas": {
1342 "NullableUser": {
1343 "type": "object",
1344 "nullable": true,
1345 "properties": {
1346 "name": { "type": "string" }
1347 }
1348 },
1349 "Post": {
1350 "type": "object",
1351 "properties": {
1352 "author": {
1353 "$ref": "#/components/schemas/NullableUser"
1354 }
1355 }
1356 }
1357 }
1358 }
1359 }))
1360 .expect("Failed to deserialize OpenAPI spec");
1361
1362 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1363
1364 let post_model = models.iter().find(|m| m.name() == "Post");
1366 assert!(post_model.is_some(), "Expected Post model to be generated");
1367
1368 if let Some(ModelType::Struct(post)) = post_model {
1369 let author_field = post.fields.iter().find(|f| f.name == "author");
1370 assert!(author_field.is_some(), "Expected author field");
1371
1372 let author = author_field.unwrap();
1375 assert!(
1376 author.is_nullable,
1377 "Expected author field to be nullable (from referenced schema)"
1378 );
1379 } else {
1380 panic!("Expected Post to be a Struct");
1381 }
1382 }
1383
1384 #[test]
1385 fn test_allof_required_fields_merge() {
1386 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1387 "openapi": "3.0.0",
1388 "info": { "title": "Test API", "version": "1.0.0" },
1389 "paths": {},
1390 "components": {
1391 "schemas": {
1392 "BaseEntity": {
1393 "type": "object",
1394 "properties": {
1395 "id": { "type": "string" },
1396 "created": { "type": "string" }
1397 },
1398 "required": ["id"]
1399 },
1400 "Person": {
1401 "allOf": [
1402 { "$ref": "#/components/schemas/BaseEntity" },
1403 {
1404 "type": "object",
1405 "properties": {
1406 "name": { "type": "string" },
1407 "age": { "type": "integer" }
1408 },
1409 "required": ["name"]
1410 }
1411 ]
1412 }
1413 }
1414 }
1415 }))
1416 .expect("Failed to deserialize OpenAPI spec");
1417
1418 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1419
1420 let person_model = models.iter().find(|m| m.name() == "Person");
1422 assert!(
1423 person_model.is_some(),
1424 "Expected Person model to be generated"
1425 );
1426
1427 if let Some(ModelType::Composition(person)) = person_model {
1428 let id_field = person.all_fields.iter().find(|f| f.name == "id");
1430 assert!(id_field.is_some(), "Expected id field");
1431 assert!(
1432 id_field.unwrap().is_required,
1433 "Expected id to be required from BaseEntity"
1434 );
1435
1436 let name_field = person.all_fields.iter().find(|f| f.name == "name");
1438 assert!(name_field.is_some(), "Expected name field");
1439 assert!(
1440 name_field.unwrap().is_required,
1441 "Expected name to be required from inline object"
1442 );
1443
1444 let created_field = person.all_fields.iter().find(|f| f.name == "created");
1446 assert!(created_field.is_some(), "Expected created field");
1447 assert!(
1448 !created_field.unwrap().is_required,
1449 "Expected created to be optional"
1450 );
1451
1452 let age_field = person.all_fields.iter().find(|f| f.name == "age");
1453 assert!(age_field.is_some(), "Expected age field");
1454 assert!(
1455 !age_field.unwrap().is_required,
1456 "Expected age to be optional"
1457 );
1458 } else {
1459 panic!("Expected Person to be a Composition");
1460 }
1461 }
1462
1463 #[test]
1464 fn test_x_rust_type_generates_type_alias() {
1465 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1466 "openapi": "3.0.0",
1467 "info": { "title": "Test API", "version": "1.0.0" },
1468 "paths": {},
1469 "components": {
1470 "schemas": {
1471 "User": {
1472 "type": "object",
1473 "x-rust-type": "crate::domain::User",
1474 "description": "Custom domain user type",
1475 "properties": {
1476 "name": { "type": "string" },
1477 "age": { "type": "integer" }
1478 }
1479 }
1480 }
1481 }
1482 }))
1483 .expect("Failed to deserialize OpenAPI spec");
1484
1485 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1486
1487 let user_model = models.iter().find(|m| m.name() == "User");
1489 assert!(user_model.is_some(), "Expected User model");
1490
1491 match user_model.unwrap() {
1492 ModelType::TypeAlias(alias) => {
1493 assert_eq!(alias.name, "User");
1494 assert_eq!(alias.target_type, "crate::domain::User");
1495 assert_eq!(
1496 alias.description,
1497 Some("Custom domain user type".to_string())
1498 );
1499 }
1500 _ => panic!("Expected TypeAlias, got different type"),
1501 }
1502 }
1503
1504 #[test]
1505 fn test_x_rust_type_works_with_enum() {
1506 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1507 "openapi": "3.0.0",
1508 "info": { "title": "Test API", "version": "1.0.0" },
1509 "paths": {},
1510 "components": {
1511 "schemas": {
1512 "Status": {
1513 "type": "string",
1514 "enum": ["active", "inactive"],
1515 "x-rust-type": "crate::domain::Status"
1516 }
1517 }
1518 }
1519 }))
1520 .expect("Failed to deserialize OpenAPI spec");
1521
1522 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1523
1524 let status_model = models.iter().find(|m| m.name() == "Status");
1525 assert!(status_model.is_some(), "Expected Status model");
1526
1527 assert!(
1529 matches!(status_model.unwrap(), ModelType::TypeAlias(_)),
1530 "Expected TypeAlias for enum with x-rust-type"
1531 );
1532 }
1533
1534 #[test]
1535 fn test_x_rust_type_works_with_oneof() {
1536 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1537 "openapi": "3.0.0",
1538 "info": { "title": "Test API", "version": "1.0.0" },
1539 "paths": {},
1540 "components": {
1541 "schemas": {
1542 "Payment": {
1543 "oneOf": [
1544 { "type": "object", "properties": { "card": { "type": "string" } } },
1545 { "type": "object", "properties": { "cash": { "type": "number" } } }
1546 ],
1547 "x-rust-type": "payments::Payment"
1548 }
1549 }
1550 }
1551 }))
1552 .expect("Failed to deserialize OpenAPI spec");
1553
1554 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1555
1556 let payment_model = models.iter().find(|m| m.name() == "Payment");
1557 assert!(payment_model.is_some(), "Expected Payment model");
1558
1559 match payment_model.unwrap() {
1561 ModelType::TypeAlias(alias) => {
1562 assert_eq!(alias.target_type, "payments::Payment");
1563 }
1564 _ => panic!("Expected TypeAlias for oneOf with x-rust-type"),
1565 }
1566 }
1567
1568 #[test]
1569 fn test_x_rust_attrs_on_struct() {
1570 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1571 "openapi": "3.0.0",
1572 "info": { "title": "Test API", "version": "1.0.0" },
1573 "paths": {},
1574 "components": {
1575 "schemas": {
1576 "User": {
1577 "type": "object",
1578 "x-rust-attrs": [
1579 "#[derive(Serialize, Deserialize)]",
1580 "#[serde(rename_all = \"camelCase\")]"
1581 ],
1582 "properties": {
1583 "name": { "type": "string" }
1584 }
1585 }
1586 }
1587 }
1588 }))
1589 .expect("Failed to deserialize OpenAPI spec");
1590
1591 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1592
1593 let user_model = models.iter().find(|m| m.name() == "User");
1594 assert!(user_model.is_some(), "Expected User model");
1595
1596 match user_model.unwrap() {
1597 ModelType::Struct(model) => {
1598 assert!(model.custom_attrs.is_some(), "Expected custom_attrs");
1599 let attrs = model.custom_attrs.as_ref().unwrap();
1600 assert_eq!(attrs.len(), 2);
1601 assert_eq!(attrs[0], "#[derive(Serialize, Deserialize)]");
1602 assert_eq!(attrs[1], "#[serde(rename_all = \"camelCase\")]");
1603 }
1604 _ => panic!("Expected Struct model"),
1605 }
1606 }
1607
1608 #[test]
1609 fn test_x_rust_attrs_on_enum() {
1610 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1611 "openapi": "3.0.0",
1612 "info": { "title": "Test API", "version": "1.0.0" },
1613 "paths": {},
1614 "components": {
1615 "schemas": {
1616 "Status": {
1617 "type": "string",
1618 "enum": ["active", "inactive"],
1619 "x-rust-attrs": ["#[serde(rename_all = \"UPPERCASE\")]"]
1620 }
1621 }
1622 }
1623 }))
1624 .expect("Failed to deserialize OpenAPI spec");
1625
1626 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1627
1628 let status_model = models.iter().find(|m| m.name() == "Status");
1629 assert!(status_model.is_some(), "Expected Status model");
1630
1631 match status_model.unwrap() {
1632 ModelType::Enum(enum_model) => {
1633 assert!(enum_model.custom_attrs.is_some());
1634 let attrs = enum_model.custom_attrs.as_ref().unwrap();
1635 assert_eq!(attrs.len(), 1);
1636 assert_eq!(attrs[0], "#[serde(rename_all = \"UPPERCASE\")]");
1637 }
1638 _ => panic!("Expected Enum model"),
1639 }
1640 }
1641
1642 #[test]
1643 fn test_x_rust_attrs_with_x_rust_type() {
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-type": "crate::domain::User",
1653 "x-rust-attrs": ["#[cfg_attr(test, derive(Default))]"],
1654 "properties": {
1655 "name": { "type": "string" }
1656 }
1657 }
1658 }
1659 }
1660 }))
1661 .expect("Failed to deserialize OpenAPI spec");
1662
1663 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1664
1665 let user_model = models.iter().find(|m| m.name() == "User");
1666 assert!(user_model.is_some(), "Expected User model");
1667
1668 match user_model.unwrap() {
1670 ModelType::TypeAlias(alias) => {
1671 assert_eq!(alias.target_type, "crate::domain::User");
1672 assert!(alias.custom_attrs.is_some());
1673 let attrs = alias.custom_attrs.as_ref().unwrap();
1674 assert_eq!(attrs.len(), 1);
1675 assert_eq!(attrs[0], "#[cfg_attr(test, derive(Default))]");
1676 }
1677 _ => panic!("Expected TypeAlias with custom attrs"),
1678 }
1679 }
1680
1681 #[test]
1682 fn test_x_rust_attrs_empty_array() {
1683 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1684 "openapi": "3.0.0",
1685 "info": { "title": "Test API", "version": "1.0.0" },
1686 "paths": {},
1687 "components": {
1688 "schemas": {
1689 "User": {
1690 "type": "object",
1691 "x-rust-attrs": [],
1692 "properties": {
1693 "name": { "type": "string" }
1694 }
1695 }
1696 }
1697 }
1698 }))
1699 .expect("Failed to deserialize OpenAPI spec");
1700
1701 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1702
1703 let user_model = models.iter().find(|m| m.name() == "User");
1704 assert!(user_model.is_some());
1705
1706 match user_model.unwrap() {
1707 ModelType::Struct(model) => {
1708 assert!(model.custom_attrs.is_none());
1710 }
1711 _ => panic!("Expected Struct"),
1712 }
1713 }
1714
1715 #[test]
1716 fn test_x_rust_type_on_string_property() {
1717 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1718 "openapi": "3.0.0",
1719 "info": { "title": "Test API", "version": "1.0.0" },
1720 "paths": {},
1721 "components": {
1722 "schemas": {
1723 "Document": {
1724 "type": "object",
1725 "description": "Document with custom version type",
1726 "properties": {
1727 "title": { "type": "string", "description": "Document title." },
1728 "content": { "type": "string", "description": "Document content." },
1729 "version": {
1730 "type": "string",
1731 "format": "semver",
1732 "x-rust-type": "semver::Version",
1733 "description": "Semantic version."
1734 }
1735 },
1736 "required": ["title", "content", "version"]
1737 }
1738 }
1739 }
1740 }))
1741 .expect("Failed to deserialize OpenAPI spec");
1742
1743 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1744
1745 let document_model = models.iter().find(|m| m.name() == "Document");
1746 assert!(document_model.is_some(), "Expected Document model");
1747
1748 match document_model.unwrap() {
1749 ModelType::Struct(model) => {
1750 let version_field = model.fields.iter().find(|f| f.name == "version");
1752 assert!(version_field.is_some(), "Expected version field");
1753 assert_eq!(version_field.unwrap().field_type, "semver::Version");
1754
1755 let title_field = model.fields.iter().find(|f| f.name == "title");
1757 assert_eq!(title_field.unwrap().field_type, "String");
1758
1759 let content_field = model.fields.iter().find(|f| f.name == "content");
1760 assert_eq!(content_field.unwrap().field_type, "String");
1761 }
1762 _ => panic!("Expected Struct"),
1763 }
1764 }
1765
1766 #[test]
1767 fn test_x_rust_type_on_integer_property() {
1768 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1769 "openapi": "3.0.0",
1770 "info": { "title": "Test API", "version": "1.0.0" },
1771 "paths": {},
1772 "components": {
1773 "schemas": {
1774 "Configuration": {
1775 "type": "object",
1776 "description": "Configuration with custom duration type",
1777 "properties": {
1778 "timeout": {
1779 "type": "integer",
1780 "x-rust-type": "std::time::Duration",
1781 "description": "Timeout duration."
1782 },
1783 "retries": { "type": "integer" }
1784 },
1785 "required": ["timeout", "retries"]
1786 }
1787 }
1788 }
1789 }))
1790 .expect("Failed to deserialize OpenAPI spec");
1791
1792 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1793
1794 let config_model = models.iter().find(|m| m.name() == "Configuration");
1795 assert!(config_model.is_some(), "Expected Configuration model");
1796
1797 match config_model.unwrap() {
1798 ModelType::Struct(model) => {
1799 let timeout_field = model.fields.iter().find(|f| f.name == "timeout");
1801 assert!(timeout_field.is_some(), "Expected timeout field");
1802 assert_eq!(timeout_field.unwrap().field_type, "std::time::Duration");
1803
1804 let retries_field = model.fields.iter().find(|f| f.name == "retries");
1806 assert_eq!(retries_field.unwrap().field_type, "i64");
1807 }
1808 _ => panic!("Expected Struct"),
1809 }
1810 }
1811
1812 #[test]
1813 fn test_x_rust_type_on_number_property() {
1814 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1815 "openapi": "3.0.0",
1816 "info": { "title": "Test API", "version": "1.0.0" },
1817 "paths": {},
1818 "components": {
1819 "schemas": {
1820 "Product": {
1821 "type": "object",
1822 "description": "Product with custom decimal type",
1823 "properties": {
1824 "price": {
1825 "type": "number",
1826 "x-rust-type": "decimal::Decimal",
1827 "description": "Product price."
1828 },
1829 "quantity": { "type": "number" }
1830 },
1831 "required": ["price", "quantity"]
1832 }
1833 }
1834 }
1835 }))
1836 .expect("Failed to deserialize OpenAPI spec");
1837
1838 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1839
1840 let product_model = models.iter().find(|m| m.name() == "Product");
1841 assert!(product_model.is_some(), "Expected Product model");
1842
1843 match product_model.unwrap() {
1844 ModelType::Struct(model) => {
1845 let price_field = model.fields.iter().find(|f| f.name == "price");
1847 assert!(price_field.is_some(), "Expected price field");
1848 assert_eq!(price_field.unwrap().field_type, "decimal::Decimal");
1849
1850 let quantity_field = model.fields.iter().find(|f| f.name == "quantity");
1852 assert_eq!(quantity_field.unwrap().field_type, "f64");
1853 }
1854 _ => panic!("Expected Struct"),
1855 }
1856 }
1857
1858 #[test]
1859 fn test_x_rust_type_on_nullable_property() {
1860 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1861 "openapi": "3.0.0",
1862 "info": { "title": "Test API", "version": "1.0.0" },
1863 "paths": {},
1864 "components": {
1865 "schemas": {
1866 "Settings": {
1867 "type": "object",
1868 "description": "Settings with nullable custom type",
1869 "properties": {
1870 "settings": {
1871 "type": "string",
1872 "x-rust-type": "serde_json::Value",
1873 "nullable": true,
1874 "description": "Optional settings."
1875 }
1876 }
1877 }
1878 }
1879 }
1880 }))
1881 .expect("Failed to deserialize OpenAPI spec");
1882
1883 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1884
1885 let settings_model = models.iter().find(|m| m.name() == "Settings");
1886 assert!(settings_model.is_some(), "Expected Settings model");
1887
1888 match settings_model.unwrap() {
1889 ModelType::Struct(model) => {
1890 let settings_field = model.fields.iter().find(|f| f.name == "settings");
1891 assert!(settings_field.is_some(), "Expected settings field");
1892
1893 let field = settings_field.unwrap();
1894 assert_eq!(field.field_type, "serde_json::Value");
1895 assert!(field.is_nullable, "Expected field to be nullable");
1896 }
1897 _ => panic!("Expected Struct"),
1898 }
1899 }
1900
1901 #[test]
1902 fn test_multiple_properties_with_x_rust_type() {
1903 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1904 "openapi": "3.0.0",
1905 "info": { "title": "Test API", "version": "1.0.0" },
1906 "paths": {},
1907 "components": {
1908 "schemas": {
1909 "ComplexModel": {
1910 "type": "object",
1911 "description": "Model with multiple custom-typed properties",
1912 "properties": {
1913 "id": {
1914 "type": "string",
1915 "format": "uuid",
1916 "x-rust-type": "uuid::Uuid"
1917 },
1918 "price": {
1919 "type": "number",
1920 "x-rust-type": "decimal::Decimal"
1921 },
1922 "timeout": {
1923 "type": "integer",
1924 "x-rust-type": "std::time::Duration"
1925 },
1926 "regular_field": { "type": "string" }
1927 },
1928 "required": ["id", "price", "timeout"]
1929 }
1930 }
1931 }
1932 }))
1933 .expect("Failed to deserialize OpenAPI spec");
1934
1935 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1936
1937 let model = models.iter().find(|m| m.name() == "ComplexModel");
1938 assert!(model.is_some(), "Expected ComplexModel model");
1939
1940 match model.unwrap() {
1941 ModelType::Struct(struct_model) => {
1942 let id_field = struct_model.fields.iter().find(|f| f.name == "id");
1944 assert_eq!(id_field.unwrap().field_type, "uuid::Uuid");
1945
1946 let price_field = struct_model.fields.iter().find(|f| f.name == "price");
1947 assert_eq!(price_field.unwrap().field_type, "decimal::Decimal");
1948
1949 let timeout_field = struct_model.fields.iter().find(|f| f.name == "timeout");
1950 assert_eq!(timeout_field.unwrap().field_type, "std::time::Duration");
1951
1952 let regular_field = struct_model
1954 .fields
1955 .iter()
1956 .find(|f| f.name == "regular_field");
1957 assert_eq!(regular_field.unwrap().field_type, "String");
1958
1959 assert!(!id_field.unwrap().is_nullable, "id should not be nullable");
1961 assert!(
1962 !price_field.unwrap().is_nullable,
1963 "price should not be nullable"
1964 );
1965 assert!(
1966 !timeout_field.unwrap().is_nullable,
1967 "timeout should not be nullable"
1968 );
1969 }
1972 _ => panic!("Expected Struct"),
1973 }
1974 }
1975
1976 #[test]
1977 fn test_x_rust_attrs_on_field() {
1978 let openapi_spec: OpenAPI = serde_json::from_value(json!({
1979 "openapi": "3.0.0",
1980 "info": { "title": "Test API", "version": "1.0.0" },
1981 "paths": {},
1982 "components": {
1983 "schemas": {
1984 "FrontendEvent": {
1985 "type": "object",
1986 "properties": {
1987 "field": {
1988 "type": "integer",
1989 "minimum": 0,
1990 "maximum": 100,
1991 "nullable": true,
1992 "x-rust-attrs": ["#[validate(range(min = 0, max = 100))]"]
1993 },
1994 "name": { "type": "string" }
1995 }
1996 }
1997 }
1998 }
1999 }))
2000 .expect("Failed to deserialize OpenAPI spec");
2001
2002 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
2003
2004 let model = models.iter().find(|m| m.name() == "FrontendEvent");
2005 assert!(model.is_some(), "Expected FrontendEvent model");
2006
2007 match model.unwrap() {
2008 ModelType::Struct(struct_model) => {
2009 let field = struct_model.fields.iter().find(|f| f.name == "field");
2010 assert!(field.is_some(), "Expected progress_percent field");
2011 let field = field.unwrap();
2012 assert_eq!(field.field_type, "i64");
2013 assert!(
2014 field.custom_attrs.is_some(),
2015 "Expected field-level x-rust-attrs"
2016 );
2017 let attrs = field.custom_attrs.as_ref().unwrap();
2018 assert_eq!(attrs.len(), 1);
2019 assert_eq!(attrs[0], "#[validate(range(min = 0, max = 100))]");
2020
2021 let name_field = struct_model.fields.iter().find(|f| f.name == "name");
2022 assert!(name_field.unwrap().custom_attrs.is_none());
2023 }
2024 _ => panic!("Expected Struct"),
2025 }
2026 }
2027
2028 #[test]
2032 fn test_inline_enum_fields_on_different_structs_get_unique_names() {
2033 let openapi_spec: OpenAPI = serde_json::from_value(json!({
2034 "openapi": "3.0.0",
2035 "info": { "title": "Test API", "version": "1.0.0" },
2036 "paths": {},
2037 "components": {
2038 "schemas": {
2039 "SignalA": {
2040 "type": "object",
2041 "properties": {
2042 "type": {
2043 "type": "string",
2044 "enum": ["variant_a"]
2045 }
2046 }
2047 },
2048 "SignalB": {
2049 "type": "object",
2050 "properties": {
2051 "type": {
2052 "type": "string",
2053 "enum": ["variant_b"]
2054 }
2055 }
2056 }
2057 }
2058 }
2059 }))
2060 .expect("Failed to deserialize OpenAPI spec");
2061
2062 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
2063
2064 let signal_a_type = models.iter().find(|m| m.name() == "SignalAType");
2066 let signal_b_type = models.iter().find(|m| m.name() == "SignalBType");
2067
2068 assert!(
2069 signal_a_type.is_some(),
2070 "Expected SignalAType enum to be generated"
2071 );
2072 assert!(
2073 signal_b_type.is_some(),
2074 "Expected SignalBType enum to be generated"
2075 );
2076
2077 match signal_a_type.unwrap() {
2079 ModelType::Enum(e) => {
2080 assert_eq!(e.variants, vec!["variant_a"]);
2081 }
2082 _ => panic!("Expected Enum for SignalAType"),
2083 }
2084 match signal_b_type.unwrap() {
2085 ModelType::Enum(e) => {
2086 assert_eq!(e.variants, vec!["variant_b"]);
2087 }
2088 _ => panic!("Expected Enum for SignalBType"),
2089 }
2090
2091 let signal_a = models.iter().find(|m| m.name() == "SignalA");
2093 assert!(signal_a.is_some(), "Expected SignalA struct");
2094 if let Some(ModelType::Struct(s)) = signal_a {
2095 let type_field = s.fields.iter().find(|f| f.name == "type").unwrap();
2096 assert_eq!(type_field.field_type, "SignalAType");
2097 }
2098
2099 let signal_b = models.iter().find(|m| m.name() == "SignalB");
2100 assert!(signal_b.is_some(), "Expected SignalB struct");
2101 if let Some(ModelType::Struct(s)) = signal_b {
2102 let type_field = s.fields.iter().find(|f| f.name == "type").unwrap();
2103 assert_eq!(type_field.field_type, "SignalBType");
2104 }
2105 }
2106
2107 #[test]
2112 fn test_x_rust_attrs_on_inline_enum_field_go_to_enum_not_field() {
2113 let openapi_spec: OpenAPI = serde_json::from_value(json!({
2114 "openapi": "3.0.0",
2115 "info": { "title": "Test API", "version": "1.0.0" },
2116 "paths": {},
2117 "components": {
2118 "schemas": {
2119 "Signal": {
2120 "type": "object",
2121 "properties": {
2122 "kind": {
2123 "type": "string",
2124 "enum": ["started", "stopped"],
2125 "x-rust-attrs": [
2126 "#[derive(derive_more::Display, Debug, Clone)]"
2127 ]
2128 },
2129 "name": { "type": "string" }
2130 }
2131 }
2132 }
2133 }
2134 }))
2135 .expect("Failed to deserialize OpenAPI spec");
2136
2137 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
2138
2139 let kind_enum = models.iter().find(|m| m.name() == "SignalKind");
2141 assert!(
2142 kind_enum.is_some(),
2143 "Expected SignalKind enum to be generated"
2144 );
2145 match kind_enum.unwrap() {
2146 ModelType::Enum(e) => {
2147 assert!(
2148 e.custom_attrs.is_some(),
2149 "x-rust-attrs should be on the generated enum"
2150 );
2151 let attrs = e.custom_attrs.as_ref().unwrap();
2152 assert!(attrs.iter().any(|a| a.contains("derive")));
2153 }
2154 _ => panic!("Expected Enum for SignalKind"),
2155 }
2156
2157 let signal = models.iter().find(|m| m.name() == "Signal");
2160 assert!(signal.is_some(), "Expected Signal struct");
2161 if let Some(ModelType::Struct(s)) = signal {
2162 let kind_field = s.fields.iter().find(|f| f.name == "kind").unwrap();
2163 assert!(
2164 kind_field.custom_attrs.is_none(),
2165 "x-rust-attrs must not appear on the struct field when they target a generated inline enum"
2166 );
2167 }
2168 }
2169
2170 #[test]
2174 fn test_allof_primitive_field_narrowed_to_specific_type() {
2175 let openapi_spec: OpenAPI = serde_json::from_value(json!({
2176 "openapi": "3.0.0",
2177 "info": { "title": "Test API", "version": "1.0.0" },
2178 "paths": {},
2179 "components": {
2180 "schemas": {
2181 "BaseSignal": {
2182 "type": "object",
2183 "required": ["type"],
2184 "properties": {
2185 "type": {
2186 "type": "string",
2187 "description": "The signal type identifier."
2188 },
2189 "name": {
2190 "type": "string",
2191 "description": "Human-readable label."
2192 }
2193 }
2194 },
2195 "ConcreteSignal": {
2196 "allOf": [
2197 { "$ref": "#/components/schemas/BaseSignal" },
2198 {
2199 "type": "object",
2200 "required": ["type"],
2201 "properties": {
2202 "type": {
2203 "type": "string",
2204 "enum": ["concrete"]
2205 }
2206 }
2207 }
2208 ]
2209 }
2210 }
2211 }
2212 }))
2213 .expect("Failed to deserialize OpenAPI spec");
2214
2215 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
2216
2217 let type_enum = models.iter().find(|m| m.name() == "ConcreteSignalType");
2219 assert!(
2220 type_enum.is_some(),
2221 "Expected ConcreteSignalType enum to be generated"
2222 );
2223 match type_enum.unwrap() {
2224 ModelType::Enum(e) => assert_eq!(e.variants, vec!["concrete"]),
2225 _ => panic!("Expected Enum for ConcreteSignalType"),
2226 }
2227
2228 let concrete = models.iter().find(|m| m.name() == "ConcreteSignal");
2230 assert!(concrete.is_some(), "Expected ConcreteSignal model");
2231 if let Some(ModelType::Composition(c)) = concrete {
2232 let type_field = c.all_fields.iter().find(|f| f.name == "type").unwrap();
2233 assert_eq!(
2234 type_field.field_type, "ConcreteSignalType",
2235 "allOf should narrow plain String to the more specific enum type"
2236 );
2237 } else {
2238 panic!("Expected ConcreteSignal to be a Composition");
2239 }
2240 }
2241
2242 #[test]
2247 fn test_x_rust_attrs_from_ref_target_not_propagated_to_field() {
2248 let openapi_spec: OpenAPI = serde_json::from_value(json!({
2249 "openapi": "3.0.0",
2250 "info": { "title": "Test API", "version": "1.0.0" },
2251 "paths": {},
2252 "components": {
2253 "schemas": {
2254 "Address": {
2255 "type": "object",
2256 "x-rust-attrs": ["#[derive(Hash, Eq, PartialEq)]"],
2257 "properties": {
2258 "street": { "type": "string" }
2259 }
2260 },
2261 "Person": {
2262 "type": "object",
2263 "properties": {
2264 "name": { "type": "string" },
2265 "address": { "$ref": "#/components/schemas/Address" }
2266 }
2267 }
2268 }
2269 }
2270 }))
2271 .expect("Failed to deserialize OpenAPI spec");
2272
2273 let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
2274
2275 let person = models.iter().find(|m| m.name() == "Person");
2276 assert!(person.is_some(), "Expected Person model");
2277
2278 if let Some(ModelType::Struct(s)) = person {
2279 let address_field = s.fields.iter().find(|f| f.name == "address").unwrap();
2280 assert_eq!(address_field.field_type, "Address");
2281 assert!(
2282 address_field.custom_attrs.is_none(),
2283 "x-rust-attrs from the referenced Address type must not appear on the Person.address field"
2284 );
2285 } else {
2286 panic!("Expected Person to be a Struct");
2287 }
2288 }
2289}