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