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