Skip to main content

openapi_model_generator/
parser.rs

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