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    OpenAPI, ReferenceOr, Schema, SchemaKind, StringFormat, Type, VariantOrUnknownOrEmpty,
11};
12use std::collections::HashSet;
13
14/// Information about a field extracted from OpenAPI schema
15#[derive(Debug)]
16struct FieldInfo {
17    field_type: String,
18    format: String,
19    is_nullable: bool,
20}
21
22/// Converts camelCase to PascalCase
23/// Example: "createRole" -> "CreateRole", "listRoles" -> "ListRoles", "listRoles-Input" -> "ListRolesInput"
24fn to_pascal_case(input: &str) -> String {
25    input
26        .split(&['-', '_'][..])
27        .filter(|s| !s.is_empty())
28        .map(|s| {
29            let mut chars = s.chars();
30            match chars.next() {
31                Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
32                None => String::new(),
33            }
34        })
35        .collect::<String>()
36}
37
38/// Extracts custom Rust attributes from x-rust-attrs extension
39fn extract_custom_attrs(schema: &Schema) -> Option<Vec<String>> {
40    schema
41        .schema_data
42        .extensions
43        .get("x-rust-attrs")
44        .and_then(|value| {
45            if let Some(arr) = value.as_array() {
46                let attrs: Vec<String> = arr
47                    .iter()
48                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
49                    .collect();
50                if attrs.is_empty() {
51                    None
52                } else {
53                    Some(attrs)
54                }
55            } else {
56                tracing::warn!(
57                    "x-rust-attrs should be an array of strings, got: {:?}",
58                    value
59                );
60                None
61            }
62        })
63}
64
65pub fn parse_openapi(
66    openapi: &OpenAPI,
67) -> Result<(Vec<ModelType>, Vec<RequestModel>, Vec<ResponseModel>)> {
68    let mut models = Vec::new();
69    let mut requests = Vec::new();
70    let mut responses = Vec::new();
71
72    let mut added_models = HashSet::new();
73
74    let empty_schemas = IndexMap::new();
75    let empty_request_bodies = IndexMap::new();
76
77    let (schemas, request_bodies) = if let Some(components) = &openapi.components {
78        (&components.schemas, &components.request_bodies)
79    } else {
80        (&empty_schemas, &empty_request_bodies)
81    };
82
83    // Parse components/schemas
84    if let Some(components) = &openapi.components {
85        for (name, schema) in &components.schemas {
86            let model_types = parse_schema_to_model_type(name, schema, &components.schemas)?;
87            for model_type in model_types {
88                if added_models.insert(model_type.name().to_string()) {
89                    models.push(model_type);
90                }
91            }
92        }
93
94        // Parse components/requestBodies - извлекаем схемы и создаем модели
95        for (name, request_body_ref) in &components.request_bodies {
96            if let ReferenceOr::Item(request_body) = request_body_ref {
97                for media_type in request_body.content.values() {
98                    if let Some(schema) = &media_type.schema {
99                        let model_types =
100                            parse_schema_to_model_type(name, schema, &components.schemas)?;
101                        for model_type in model_types {
102                            if added_models.insert(model_type.name().to_string()) {
103                                models.push(model_type);
104                            }
105                        }
106                    }
107                }
108            }
109        }
110    }
111
112    // Parse paths
113    for (_path, path_item) in openapi.paths.iter() {
114        let path_item = match path_item {
115            ReferenceOr::Item(item) => item,
116            ReferenceOr::Reference { .. } => continue,
117        };
118
119        let operations = [
120            &path_item.get,
121            &path_item.post,
122            &path_item.put,
123            &path_item.delete,
124            &path_item.patch,
125        ];
126
127        for op in operations.iter().filter_map(|o| o.as_ref()) {
128            let inline_models =
129                process_operation(op, &mut requests, &mut responses, schemas, request_bodies)?;
130            for model_type in inline_models {
131                if added_models.insert(model_type.name().to_string()) {
132                    models.push(model_type);
133                }
134            }
135        }
136    }
137
138    Ok((models, requests, responses))
139}
140
141fn process_operation(
142    operation: &openapiv3::Operation,
143    requests: &mut Vec<RequestModel>,
144    responses: &mut Vec<ResponseModel>,
145    all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
146    request_bodies: &IndexMap<String, ReferenceOr<openapiv3::RequestBody>>,
147) -> Result<Vec<ModelType>> {
148    let mut inline_models = Vec::new();
149
150    // Parse request body
151    if let Some(request_body_ref) = &operation.request_body {
152        let (request_body_data, is_inline) = match request_body_ref {
153            ReferenceOr::Item(request_body) => (Some((request_body, request_body.required)), true),
154            ReferenceOr::Reference { reference } => {
155                if let Some(rb_name) = reference.strip_prefix("#/components/requestBodies/") {
156                    (
157                        request_bodies.get(rb_name).and_then(|rb_ref| match rb_ref {
158                            ReferenceOr::Item(rb) => Some((rb, false)),
159                            ReferenceOr::Reference { .. } => None,
160                        }),
161                        false,
162                    )
163                } else {
164                    (None, false)
165                }
166            }
167        };
168
169        if let Some((request_body, is_required)) = request_body_data {
170            for (content_type, media_type) in &request_body.content {
171                if let Some(schema) = &media_type.schema {
172                    let operation_name =
173                        to_pascal_case(operation.operation_id.as_deref().unwrap_or("Unknown"));
174
175                    let schema_type = if is_inline {
176                        if let ReferenceOr::Item(schema_item) = schema {
177                            if matches!(schema_item.schema_kind, SchemaKind::Type(Type::Object(_)))
178                            {
179                                let model_name = format!("{operation_name}RequestBody");
180                                let model_types =
181                                    parse_schema_to_model_type(&model_name, schema, all_schemas)?;
182                                inline_models.extend(model_types);
183                                model_name
184                            } else {
185                                extract_type_and_format(schema, all_schemas)?.0
186                            }
187                        } else {
188                            extract_type_and_format(schema, all_schemas)?.0
189                        }
190                    } else {
191                        extract_type_and_format(schema, all_schemas)?.0
192                    };
193
194                    let request = RequestModel {
195                        name: format!("{operation_name}Request"),
196                        content_type: content_type.clone(),
197                        schema: schema_type,
198                        is_required,
199                    };
200                    requests.push(request);
201                }
202            }
203        }
204    }
205
206    // Parse responses
207    for (status, response_ref) in operation.responses.responses.iter() {
208        if let ReferenceOr::Item(response) = response_ref {
209            for (content_type, media_type) in &response.content {
210                if let Some(schema) = &media_type.schema {
211                    let response = ResponseModel {
212                        name: format!(
213                            "{}Response",
214                            to_pascal_case(operation.operation_id.as_deref().unwrap_or("Unknown"))
215                        ),
216                        status_code: status.to_string(),
217                        content_type: content_type.clone(),
218                        schema: extract_type_and_format(schema, all_schemas)?.0,
219                        description: Some(response.description.clone()),
220                    };
221                    responses.push(response);
222                }
223            }
224        }
225    }
226    Ok(inline_models)
227}
228
229fn parse_schema_to_model_type(
230    name: &str,
231    schema: &ReferenceOr<Schema>,
232    all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
233) -> Result<Vec<ModelType>> {
234    match schema {
235        ReferenceOr::Reference { .. } => Ok(Vec::new()),
236        ReferenceOr::Item(schema) => {
237            if let Some(rust_type) = schema.schema_data.extensions.get("x-rust-type") {
238                if let Some(type_str) = rust_type.as_str() {
239                    return Ok(vec![ModelType::TypeAlias(TypeAliasModel {
240                        name: to_pascal_case(name),
241                        target_type: type_str.to_string(),
242                        description: schema.schema_data.description.clone(),
243                        custom_attrs: extract_custom_attrs(schema),
244                    })]);
245                }
246            }
247
248            match &schema.schema_kind {
249                // regular objects
250                SchemaKind::Type(Type::Object(obj)) => {
251                    // Special case: object with only additionalProperties (no regular properties)
252                    if obj.properties.is_empty() && obj.additional_properties.is_some() {
253                        let hashmap_type = match &obj.additional_properties {
254                            Some(additional_props) => match additional_props {
255                                openapiv3::AdditionalProperties::Any(_) => {
256                                    "std::collections::HashMap<String, serde_json::Value>"
257                                        .to_string()
258                                }
259                                openapiv3::AdditionalProperties::Schema(schema_ref) => {
260                                    let (inner_type, _) =
261                                        extract_type_and_format(schema_ref, all_schemas)?;
262                                    format!("std::collections::HashMap<String, {inner_type}>")
263                                }
264                            },
265                            None => {
266                                "std::collections::HashMap<String, serde_json::Value>".to_string()
267                            }
268                        };
269                        return Ok(vec![ModelType::TypeAlias(TypeAliasModel {
270                            name: to_pascal_case(name),
271                            target_type: hashmap_type,
272                            description: schema.schema_data.description.clone(),
273                            custom_attrs: extract_custom_attrs(schema),
274                        })]);
275                    }
276
277                    let mut fields = Vec::new();
278                    let mut inline_models = Vec::new();
279
280                    // Process regular properties
281                    for (field_name, field_schema) in &obj.properties {
282                        let (field_info, inline_model) = match field_schema {
283                            ReferenceOr::Item(boxed_schema) => extract_field_info(
284                                field_name,
285                                &ReferenceOr::Item((**boxed_schema).clone()),
286                                all_schemas,
287                            )?,
288                            ReferenceOr::Reference { reference } => extract_field_info(
289                                field_name,
290                                &ReferenceOr::Reference {
291                                    reference: reference.clone(),
292                                },
293                                all_schemas,
294                            )?,
295                        };
296                        if let Some(inline_model) = inline_model {
297                            inline_models.push(inline_model);
298                        }
299                        let is_required = obj.required.contains(field_name);
300                        fields.push(Field {
301                            name: field_name.clone(),
302                            field_type: field_info.field_type,
303                            format: field_info.format,
304                            is_required,
305                            is_nullable: field_info.is_nullable,
306                        });
307                    }
308
309                    let mut models = inline_models;
310                    if obj.properties.is_empty() && obj.additional_properties.is_none() {
311                        models.push(ModelType::Struct(Model {
312                            name: to_pascal_case(name),
313                            fields: vec![], // Пустая структура
314                            custom_attrs: extract_custom_attrs(schema),
315                        }));
316                    } else if !fields.is_empty() {
317                        models.push(ModelType::Struct(Model {
318                            name: to_pascal_case(name),
319                            fields,
320                            custom_attrs: extract_custom_attrs(schema),
321                        }));
322                    }
323                    Ok(models)
324                }
325
326                // allOf
327                SchemaKind::AllOf { all_of } => {
328                    let (all_fields, inline_models) =
329                        resolve_all_of_fields(name, all_of, all_schemas)?;
330                    let mut models = inline_models;
331
332                    if !all_fields.is_empty() {
333                        models.push(ModelType::Composition(CompositionModel {
334                            name: to_pascal_case(name),
335                            all_fields,
336                            custom_attrs: extract_custom_attrs(schema),
337                        }));
338                    }
339
340                    Ok(models)
341                }
342
343                // oneOf
344                SchemaKind::OneOf { one_of } => {
345                    let (variants, inline_models) =
346                        resolve_union_variants(name, one_of, all_schemas)?;
347                    let mut models = inline_models;
348
349                    models.push(ModelType::Union(UnionModel {
350                        name: to_pascal_case(name),
351                        variants,
352                        union_type: UnionType::OneOf,
353                        custom_attrs: extract_custom_attrs(schema),
354                    }));
355
356                    Ok(models)
357                }
358
359                // anyOf
360                SchemaKind::AnyOf { any_of } => {
361                    let (variants, inline_models) =
362                        resolve_union_variants(name, any_of, all_schemas)?;
363                    let mut models = inline_models;
364
365                    models.push(ModelType::Union(UnionModel {
366                        name: to_pascal_case(name),
367                        variants,
368                        union_type: UnionType::AnyOf,
369                        custom_attrs: extract_custom_attrs(schema),
370                    }));
371
372                    Ok(models)
373                }
374
375                // enum strings
376                SchemaKind::Type(Type::String(string_type)) => {
377                    if !string_type.enumeration.is_empty() {
378                        let variants: Vec<String> = string_type
379                            .enumeration
380                            .iter()
381                            .filter_map(|value| value.clone())
382                            .collect();
383
384                        if !variants.is_empty() {
385                            let models = vec![ModelType::Enum(EnumModel {
386                                name: to_pascal_case(name),
387                                variants,
388                                description: schema.schema_data.description.clone(),
389                                custom_attrs: extract_custom_attrs(schema),
390                            })];
391
392                            return Ok(models);
393                        }
394                    }
395                    Ok(Vec::new())
396                }
397
398                _ => Ok(Vec::new()),
399            }
400        }
401    }
402}
403
404fn extract_type_and_format(
405    schema: &ReferenceOr<Schema>,
406    all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
407) -> Result<(String, String)> {
408    match schema {
409        ReferenceOr::Reference { reference } => {
410            let type_name = reference.split('/').next_back().unwrap_or("Unknown");
411
412            if let Some(ReferenceOr::Item(schema)) = all_schemas.get(type_name) {
413                if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
414                    return Ok((to_pascal_case(type_name), "oneOf".to_string()));
415                }
416            }
417            Ok((to_pascal_case(type_name), "reference".to_string()))
418        }
419
420        ReferenceOr::Item(schema) => match &schema.schema_kind {
421            SchemaKind::Type(Type::String(string_type)) => match &string_type.format {
422                VariantOrUnknownOrEmpty::Item(fmt) => match fmt {
423                    StringFormat::DateTime => {
424                        Ok(("DateTime<Utc>".to_string(), "date-time".to_string()))
425                    }
426                    StringFormat::Date => Ok(("NaiveDate".to_string(), "date".to_string())),
427                    _ => Ok(("String".to_string(), format!("{fmt:?}"))),
428                },
429                VariantOrUnknownOrEmpty::Unknown(unknown_format) => {
430                    if unknown_format.to_lowercase() == "uuid" {
431                        Ok(("Uuid".to_string(), "uuid".to_string()))
432                    } else {
433                        Ok(("String".to_string(), unknown_format.clone()))
434                    }
435                }
436                _ => Ok(("String".to_string(), "string".to_string())),
437            },
438            SchemaKind::Type(Type::Integer(_)) => Ok(("i64".to_string(), "integer".to_string())),
439            SchemaKind::Type(Type::Number(_)) => Ok(("f64".to_string(), "number".to_string())),
440            SchemaKind::Type(Type::Boolean(_)) => Ok(("bool".to_string(), "boolean".to_string())),
441            SchemaKind::Type(Type::Array(arr)) => {
442                if let Some(items) = &arr.items {
443                    let items_ref: &ReferenceOr<Box<Schema>> = items;
444                    let (inner_type, format) = match items_ref {
445                        ReferenceOr::Item(boxed_schema) => extract_type_and_format(
446                            &ReferenceOr::Item((**boxed_schema).clone()),
447                            all_schemas,
448                        )?,
449                        ReferenceOr::Reference { reference } => {
450                            let type_name = reference.split('/').next_back().unwrap_or("Unknown");
451
452                            if let Some(ReferenceOr::Item(schema)) = all_schemas.get(type_name) {
453                                if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
454                                    (to_pascal_case(type_name), "oneOf".to_string())
455                                } else {
456                                    extract_type_and_format(
457                                        &ReferenceOr::Reference {
458                                            reference: reference.clone(),
459                                        },
460                                        all_schemas,
461                                    )?
462                                }
463                            } else {
464                                extract_type_and_format(
465                                    &ReferenceOr::Reference {
466                                        reference: reference.clone(),
467                                    },
468                                    all_schemas,
469                                )?
470                            }
471                        }
472                    };
473                    Ok((format!("Vec<{inner_type}>"), format))
474                } else {
475                    Ok(("Vec<serde_json::Value>".to_string(), "array".to_string()))
476                }
477            }
478            SchemaKind::Type(Type::Object(_obj)) => {
479                Ok(("serde_json::Value".to_string(), "object".to_string()))
480            }
481            _ => Ok(("serde_json::Value".to_string(), "unknown".to_string())),
482        },
483    }
484}
485
486/// Extracts field information including type, format, and nullable flag from OpenAPI schema
487fn extract_field_info(
488    field_name: &str,
489    schema: &ReferenceOr<Schema>,
490    all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
491) -> Result<(FieldInfo, Option<ModelType>)> {
492    let (mut field_type, format) = extract_type_and_format(schema, all_schemas)?;
493
494    let (is_nullable, en) = match schema {
495        ReferenceOr::Reference { reference } => {
496            let is_nullable =
497                if let Some(type_name) = reference.strip_prefix("#/components/schemas/") {
498                    all_schemas
499                        .get(type_name)
500                        .and_then(|s| match s {
501                            ReferenceOr::Item(schema) => Some(schema.schema_data.nullable),
502                            _ => None,
503                        })
504                        .unwrap_or(false)
505                } else {
506                    false
507                };
508            (is_nullable, None)
509        }
510
511        ReferenceOr::Item(schema) => {
512            let is_nullable = schema.schema_data.nullable;
513
514            let maybe_enum = match &schema.schema_kind {
515                SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
516                    let variants: Vec<String> =
517                        s.enumeration.iter().filter_map(|v| v.clone()).collect();
518                    field_type = to_pascal_case(field_name);
519                    Some(ModelType::Enum(EnumModel {
520                        name: to_pascal_case(field_name),
521                        variants,
522                        description: schema.schema_data.description.clone(),
523                        custom_attrs: extract_custom_attrs(schema),
524                    }))
525                }
526                SchemaKind::Type(Type::Object(_)) => {
527                    field_type = "serde_json::Value".to_string();
528                    None
529                }
530                _ => None,
531            };
532            (is_nullable, maybe_enum)
533        }
534    };
535
536    Ok((
537        FieldInfo {
538            field_type,
539            format,
540            is_nullable,
541        },
542        en,
543    ))
544}
545
546fn resolve_all_of_fields(
547    _name: &str,
548    all_of: &[ReferenceOr<Schema>],
549    all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
550) -> Result<(Vec<Field>, Vec<ModelType>)> {
551    let mut all_fields = Vec::new();
552    let mut models = Vec::new();
553    let mut all_required_fields = HashSet::new();
554
555    for schema_ref in all_of {
556        let schema_to_check = match schema_ref {
557            ReferenceOr::Reference { reference } => reference
558                .strip_prefix("#/components/schemas/")
559                .and_then(|schema_name| all_schemas.get(schema_name)),
560            ReferenceOr::Item(_) => Some(schema_ref),
561        };
562
563        if let Some(ReferenceOr::Item(schema)) = schema_to_check {
564            if let SchemaKind::Type(Type::Object(obj)) = &schema.schema_kind {
565                all_required_fields.extend(obj.required.iter().cloned());
566            }
567        }
568    }
569
570    // Теперь собираем поля из всех схем
571    for schema_ref in all_of {
572        match schema_ref {
573            ReferenceOr::Reference { reference } => {
574                if let Some(schema_name) = reference.strip_prefix("#/components/schemas/") {
575                    if let Some(referenced_schema) = all_schemas.get(schema_name) {
576                        let (fields, inline_models) =
577                            extract_fields_from_schema(referenced_schema, all_schemas)?;
578                        all_fields.extend(fields);
579                        models.extend(inline_models);
580                    }
581                }
582            }
583            ReferenceOr::Item(_schema) => {
584                let (fields, inline_models) = extract_fields_from_schema(schema_ref, all_schemas)?;
585                all_fields.extend(fields);
586                models.extend(inline_models);
587            }
588        }
589    }
590
591    // Обновляем is_required для полей на основе объединенного множества required
592    for field in &mut all_fields {
593        if all_required_fields.contains(&field.name) {
594            field.is_required = true;
595        }
596    }
597
598    Ok((all_fields, models))
599}
600
601fn resolve_union_variants(
602    name: &str,
603    schemas: &[ReferenceOr<Schema>],
604    all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
605) -> Result<(Vec<UnionVariant>, Vec<ModelType>)> {
606    use std::collections::BTreeSet;
607
608    let mut variants = Vec::new();
609    let mut models = Vec::new();
610    let mut enum_values: BTreeSet<String> = BTreeSet::new();
611    let mut is_all_simple_enum = true;
612
613    for schema_ref in schemas {
614        let resolved = match schema_ref {
615            ReferenceOr::Reference { reference } => reference
616                .strip_prefix("#/components/schemas/")
617                .and_then(|n| all_schemas.get(n)),
618            ReferenceOr::Item(_) => Some(schema_ref),
619        };
620
621        let Some(resolved_schema) = resolved else {
622            is_all_simple_enum = false;
623            continue;
624        };
625
626        match resolved_schema {
627            ReferenceOr::Item(schema) => match &schema.schema_kind {
628                SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
629                    enum_values.extend(s.enumeration.iter().filter_map(|v| v.as_ref().cloned()));
630                }
631                SchemaKind::Type(Type::Integer(n)) if !n.enumeration.is_empty() => {
632                    enum_values.extend(
633                        n.enumeration
634                            .iter()
635                            .filter_map(|v| v.map(|num| format!("Value{num}"))),
636                    );
637                }
638
639                _ => is_all_simple_enum = false,
640            },
641            ReferenceOr::Reference { reference } => {
642                if let Some(n) = reference.strip_prefix("#/components/schemas/") {
643                    if let Some(ReferenceOr::Item(inner)) = all_schemas.get(n) {
644                        if let SchemaKind::Type(Type::String(s)) = &inner.schema_kind {
645                            let values: Vec<String> = s
646                                .enumeration
647                                .iter()
648                                .filter_map(|v| v.as_ref().cloned())
649                                .collect();
650                            enum_values.extend(values);
651                        } else {
652                            is_all_simple_enum = false;
653                        }
654                    }
655                }
656            }
657        }
658    }
659    if is_all_simple_enum && !enum_values.is_empty() {
660        let enum_name = to_pascal_case(name);
661        let enum_model = ModelType::Enum(EnumModel {
662            name: enum_name.clone(),
663            variants: enum_values.iter().map(|v| to_pascal_case(v)).collect(),
664            description: None,
665            custom_attrs: None, // Collective enum from multiple schemas, no single source for attrs
666        });
667
668        return Ok((vec![], vec![enum_model]));
669    }
670
671    // fallback for usual union-schemas
672    for (index, schema_ref) in schemas.iter().enumerate() {
673        match schema_ref {
674            ReferenceOr::Reference { reference } => {
675                if let Some(schema_name) = reference.strip_prefix("#/components/schemas/") {
676                    if let Some(referenced_schema) = all_schemas.get(schema_name) {
677                        if let ReferenceOr::Item(schema) = referenced_schema {
678                            if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
679                                variants.push(UnionVariant {
680                                    name: to_pascal_case(schema_name),
681                                    fields: vec![],
682                                });
683                            } else {
684                                let (fields, inline_models) =
685                                    extract_fields_from_schema(referenced_schema, all_schemas)?;
686                                variants.push(UnionVariant {
687                                    name: to_pascal_case(schema_name),
688                                    fields,
689                                });
690                                models.extend(inline_models);
691                            }
692                        }
693                    }
694                }
695            }
696            ReferenceOr::Item(_) => {
697                let (fields, inline_models) = extract_fields_from_schema(schema_ref, all_schemas)?;
698                let variant_name = format!("Variant{index}");
699                variants.push(UnionVariant {
700                    name: variant_name,
701                    fields,
702                });
703                models.extend(inline_models);
704            }
705        }
706    }
707
708    Ok((variants, models))
709}
710
711fn extract_fields_from_schema(
712    schema_ref: &ReferenceOr<Schema>,
713    _all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
714) -> Result<(Vec<Field>, Vec<ModelType>)> {
715    let mut fields = Vec::new();
716    let mut inline_models = Vec::new();
717
718    match schema_ref {
719        ReferenceOr::Reference { .. } => Ok((fields, inline_models)),
720        ReferenceOr::Item(schema) => {
721            match &schema.schema_kind {
722                SchemaKind::Type(Type::Object(obj)) => {
723                    for (field_name, field_schema) in &obj.properties {
724                        let (field_info, inline_model) = match field_schema {
725                            ReferenceOr::Item(boxed_schema) => extract_field_info(
726                                field_name,
727                                &ReferenceOr::Item((**boxed_schema).clone()),
728                                _all_schemas,
729                            )?,
730                            ReferenceOr::Reference { reference } => extract_field_info(
731                                field_name,
732                                &ReferenceOr::Reference {
733                                    reference: reference.clone(),
734                                },
735                                _all_schemas,
736                            )?,
737                        };
738
739                        let is_nullable = field_info.is_nullable
740                            || field_name == "value"
741                            || field_name == "default_value";
742
743                        let field_type = field_info.field_type.clone();
744
745                        let is_required = obj.required.contains(field_name);
746                        fields.push(Field {
747                            name: field_name.clone(),
748                            field_type,
749                            format: field_info.format,
750                            is_required,
751                            is_nullable,
752                        });
753                        if let Some(inline_model) = inline_model {
754                            match &inline_model {
755                                ModelType::Struct(m) if m.fields.is_empty() => {}
756                                _ => inline_models.push(inline_model),
757                            }
758                        }
759                    }
760                }
761                SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
762                    let name = schema
763                        .schema_data
764                        .title
765                        .clone()
766                        .unwrap_or_else(|| "AnonymousStringEnum".to_string());
767
768                    let enum_model = ModelType::Enum(EnumModel {
769                        name,
770                        variants: s
771                            .enumeration
772                            .iter()
773                            .filter_map(|v| v.as_ref().map(|s| to_pascal_case(s)))
774                            .collect(),
775                        description: schema.schema_data.description.clone(),
776                        custom_attrs: extract_custom_attrs(schema),
777                    });
778
779                    inline_models.push(enum_model);
780                }
781                SchemaKind::Type(Type::Integer(n)) if !n.enumeration.is_empty() => {
782                    let name = schema
783                        .schema_data
784                        .title
785                        .clone()
786                        .unwrap_or_else(|| "AnonymousIntEnum".to_string());
787
788                    let enum_model = ModelType::Enum(EnumModel {
789                        name,
790                        variants: n
791                            .enumeration
792                            .iter()
793                            .filter_map(|v| v.map(|num| format!("Value{num}")))
794                            .collect(),
795                        description: schema.schema_data.description.clone(),
796                        custom_attrs: extract_custom_attrs(schema),
797                    });
798
799                    inline_models.push(enum_model);
800                }
801
802                _ => {}
803            }
804
805            Ok((fields, inline_models))
806        }
807    }
808}
809
810#[cfg(test)]
811mod tests {
812    use super::*;
813    use serde_json::json;
814
815    #[test]
816    fn test_parse_inline_request_body_generates_model() {
817        let openapi_spec: OpenAPI = serde_json::from_value(json!({
818            "openapi": "3.0.0",
819            "info": { "title": "Test API", "version": "1.0.0" },
820            "paths": {
821                "/items": {
822                    "post": {
823                        "operationId": "createItem",
824                        "requestBody": {
825                            "content": {
826                                "application/json": {
827                                    "schema": {
828                                        "type": "object",
829                                        "properties": {
830                                            "name": { "type": "string" },
831                                            "value": { "type": "integer" }
832                                        },
833                                        "required": ["name"]
834                                    }
835                                }
836                            }
837                        },
838                        "responses": { "200": { "description": "OK" } }
839                    }
840                }
841            }
842        }))
843        .expect("Failed to deserialize OpenAPI spec");
844
845        let (models, requests, _responses) =
846            parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
847
848        // 1. Проверяем, что модель запроса была создана
849        assert_eq!(requests.len(), 1);
850        let request_model = &requests[0];
851        assert_eq!(request_model.name, "CreateItemRequest");
852
853        // 2. Проверяем, что схема запроса ссылается на НОВУЮ модель, а не на Value
854        assert_eq!(request_model.schema, "CreateItemRequestBody");
855
856        // 3. Проверяем, что сама модель для тела запроса была сгенерирована
857        let inline_model = models.iter().find(|m| m.name() == "CreateItemRequestBody");
858        assert!(
859            inline_model.is_some(),
860            "Expected a model named 'CreateItemRequestBody' to be generated"
861        );
862
863        if let Some(ModelType::Struct(model)) = inline_model {
864            assert_eq!(model.fields.len(), 2);
865            assert_eq!(model.fields[0].name, "name");
866            assert_eq!(model.fields[0].field_type, "String");
867            assert!(model.fields[0].is_required);
868
869            assert_eq!(model.fields[1].name, "value");
870            assert_eq!(model.fields[1].field_type, "i64");
871            assert!(!model.fields[1].is_required);
872        } else {
873            panic!("Expected a Struct model for CreateItemRequestBody");
874        }
875    }
876
877    #[test]
878    fn test_parse_ref_request_body_works() {
879        let openapi_spec: OpenAPI = serde_json::from_value(json!({
880            "openapi": "3.0.0",
881            "info": { "title": "Test API", "version": "1.0.0" },
882            "components": {
883                "schemas": {
884                    "ItemData": {
885                        "type": "object",
886                        "properties": {
887                            "name": { "type": "string" }
888                        }
889                    }
890                },
891                "requestBodies": {
892                    "CreateItem": {
893                        "content": {
894                            "application/json": {
895                                "schema": { "$ref": "#/components/schemas/ItemData" }
896                            }
897                        }
898                    }
899                }
900            },
901            "paths": {
902                "/items": {
903                    "post": {
904                        "operationId": "createItem",
905                        "requestBody": { "$ref": "#/components/requestBodies/CreateItem" },
906                        "responses": { "200": { "description": "OK" } }
907                    }
908                }
909            }
910        }))
911        .expect("Failed to deserialize OpenAPI spec");
912
913        let (models, requests, _responses) =
914            parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
915
916        // Проверяем, что модель запроса была создана
917        assert_eq!(requests.len(), 1);
918        let request_model = &requests[0];
919        assert_eq!(request_model.name, "CreateItemRequest");
920
921        // Проверяем, что схема ссылается на существующую модель
922        assert_eq!(request_model.schema, "ItemData");
923
924        // Проверяем, что сама модель ItemData существует в списке моделей
925        assert!(models.iter().any(|m| m.name() == "ItemData"));
926    }
927
928    #[test]
929    fn test_parse_no_request_body() {
930        let openapi_spec: OpenAPI = serde_json::from_value(json!({
931            "openapi": "3.0.0",
932            "info": { "title": "Test API", "version": "1.0.0" },
933            "paths": {
934                "/items": {
935                    "get": {
936                        "operationId": "listItems",
937                        "responses": { "200": { "description": "OK" } }
938                    }
939                }
940            }
941        }))
942        .expect("Failed to deserialize OpenAPI spec");
943
944        let (_models, requests, _responses) =
945            parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
946
947        // Убеждаемся, что моделей запросов не создано
948        assert!(requests.is_empty());
949    }
950
951    #[test]
952    fn test_nullable_reference_field() {
953        // Тест проверяет что nullable корректно читается из целевой схемы при использовании $ref
954        let openapi_spec: OpenAPI = serde_json::from_value(json!({
955            "openapi": "3.0.0",
956            "info": { "title": "Test API", "version": "1.0.0" },
957            "paths": {},
958            "components": {
959                "schemas": {
960                    "NullableUser": {
961                        "type": "object",
962                        "nullable": true,
963                        "properties": {
964                            "name": { "type": "string" }
965                        }
966                    },
967                    "Post": {
968                        "type": "object",
969                        "properties": {
970                            "author": {
971                                "$ref": "#/components/schemas/NullableUser"
972                            }
973                        }
974                    }
975                }
976            }
977        }))
978        .expect("Failed to deserialize OpenAPI spec");
979
980        let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
981
982        // Находим модель Post
983        let post_model = models.iter().find(|m| m.name() == "Post");
984        assert!(post_model.is_some(), "Expected Post model to be generated");
985
986        if let Some(ModelType::Struct(post)) = post_model {
987            let author_field = post.fields.iter().find(|f| f.name == "author");
988            assert!(author_field.is_some(), "Expected author field");
989
990            // Проверяем что nullable правильно обработан для ссылочного типа
991            // (nullable берется из целевой схемы NullableUser)
992            let author = author_field.unwrap();
993            assert!(
994                author.is_nullable,
995                "Expected author field to be nullable (from referenced schema)"
996            );
997        } else {
998            panic!("Expected Post to be a Struct");
999        }
1000    }
1001
1002    #[test]
1003    fn test_allof_required_fields_merge() {
1004        let openapi_spec: OpenAPI = serde_json::from_value(json!({
1005            "openapi": "3.0.0",
1006            "info": { "title": "Test API", "version": "1.0.0" },
1007            "paths": {},
1008            "components": {
1009                "schemas": {
1010                    "BaseEntity": {
1011                        "type": "object",
1012                        "properties": {
1013                            "id": { "type": "string" },
1014                            "created": { "type": "string" }
1015                        },
1016                        "required": ["id"]
1017                    },
1018                    "Person": {
1019                        "allOf": [
1020                            { "$ref": "#/components/schemas/BaseEntity" },
1021                            {
1022                                "type": "object",
1023                                "properties": {
1024                                    "name": { "type": "string" },
1025                                    "age": { "type": "integer" }
1026                                },
1027                                "required": ["name"]
1028                            }
1029                        ]
1030                    }
1031                }
1032            }
1033        }))
1034        .expect("Failed to deserialize OpenAPI spec");
1035
1036        let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1037
1038        // Находим модель Person
1039        let person_model = models.iter().find(|m| m.name() == "Person");
1040        assert!(
1041            person_model.is_some(),
1042            "Expected Person model to be generated"
1043        );
1044
1045        if let Some(ModelType::Composition(person)) = person_model {
1046            // Проверяем что id (из BaseEntity) обязательное
1047            let id_field = person.all_fields.iter().find(|f| f.name == "id");
1048            assert!(id_field.is_some(), "Expected id field");
1049            assert!(
1050                id_field.unwrap().is_required,
1051                "Expected id to be required from BaseEntity"
1052            );
1053
1054            // Проверяем что name (из второго объекта) обязательное
1055            let name_field = person.all_fields.iter().find(|f| f.name == "name");
1056            assert!(name_field.is_some(), "Expected name field");
1057            assert!(
1058                name_field.unwrap().is_required,
1059                "Expected name to be required from inline object"
1060            );
1061
1062            // Проверяем что created и age не обязательные
1063            let created_field = person.all_fields.iter().find(|f| f.name == "created");
1064            assert!(created_field.is_some(), "Expected created field");
1065            assert!(
1066                !created_field.unwrap().is_required,
1067                "Expected created to be optional"
1068            );
1069
1070            let age_field = person.all_fields.iter().find(|f| f.name == "age");
1071            assert!(age_field.is_some(), "Expected age field");
1072            assert!(
1073                !age_field.unwrap().is_required,
1074                "Expected age to be optional"
1075            );
1076        } else {
1077            panic!("Expected Person to be a Composition");
1078        }
1079    }
1080
1081    #[test]
1082    fn test_x_rust_type_generates_type_alias() {
1083        let openapi_spec: OpenAPI = serde_json::from_value(json!({
1084            "openapi": "3.0.0",
1085            "info": { "title": "Test API", "version": "1.0.0" },
1086            "paths": {},
1087            "components": {
1088                "schemas": {
1089                    "User": {
1090                        "type": "object",
1091                        "x-rust-type": "crate::domain::User",
1092                        "description": "Custom domain user type",
1093                        "properties": {
1094                            "name": { "type": "string" },
1095                            "age": { "type": "integer" }
1096                        }
1097                    }
1098                }
1099            }
1100        }))
1101        .expect("Failed to deserialize OpenAPI spec");
1102
1103        let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1104
1105        // Проверяем что создан TypeAlias, а не Struct
1106        let user_model = models.iter().find(|m| m.name() == "User");
1107        assert!(user_model.is_some(), "Expected User model");
1108
1109        match user_model.unwrap() {
1110            ModelType::TypeAlias(alias) => {
1111                assert_eq!(alias.name, "User");
1112                assert_eq!(alias.target_type, "crate::domain::User");
1113                assert_eq!(
1114                    alias.description,
1115                    Some("Custom domain user type".to_string())
1116                );
1117            }
1118            _ => panic!("Expected TypeAlias, got different type"),
1119        }
1120    }
1121
1122    #[test]
1123    fn test_x_rust_type_works_with_enum() {
1124        let openapi_spec: OpenAPI = serde_json::from_value(json!({
1125            "openapi": "3.0.0",
1126            "info": { "title": "Test API", "version": "1.0.0" },
1127            "paths": {},
1128            "components": {
1129                "schemas": {
1130                    "Status": {
1131                        "type": "string",
1132                        "enum": ["active", "inactive"],
1133                        "x-rust-type": "crate::domain::Status"
1134                    }
1135                }
1136            }
1137        }))
1138        .expect("Failed to deserialize OpenAPI spec");
1139
1140        let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1141
1142        let status_model = models.iter().find(|m| m.name() == "Status");
1143        assert!(status_model.is_some(), "Expected Status model");
1144
1145        // Должен быть TypeAlias, не Enum
1146        assert!(
1147            matches!(status_model.unwrap(), ModelType::TypeAlias(_)),
1148            "Expected TypeAlias for enum with x-rust-type"
1149        );
1150    }
1151
1152    #[test]
1153    fn test_x_rust_type_works_with_oneof() {
1154        let openapi_spec: OpenAPI = serde_json::from_value(json!({
1155            "openapi": "3.0.0",
1156            "info": { "title": "Test API", "version": "1.0.0" },
1157            "paths": {},
1158            "components": {
1159                "schemas": {
1160                    "Payment": {
1161                        "oneOf": [
1162                            { "type": "object", "properties": { "card": { "type": "string" } } },
1163                            { "type": "object", "properties": { "cash": { "type": "number" } } }
1164                        ],
1165                        "x-rust-type": "payments::Payment"
1166                    }
1167                }
1168            }
1169        }))
1170        .expect("Failed to deserialize OpenAPI spec");
1171
1172        let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1173
1174        let payment_model = models.iter().find(|m| m.name() == "Payment");
1175        assert!(payment_model.is_some(), "Expected Payment model");
1176
1177        // Должен быть TypeAlias, не Union
1178        match payment_model.unwrap() {
1179            ModelType::TypeAlias(alias) => {
1180                assert_eq!(alias.target_type, "payments::Payment");
1181            }
1182            _ => panic!("Expected TypeAlias for oneOf with x-rust-type"),
1183        }
1184    }
1185
1186    #[test]
1187    fn test_x_rust_attrs_on_struct() {
1188        let openapi_spec: OpenAPI = serde_json::from_value(json!({
1189            "openapi": "3.0.0",
1190            "info": { "title": "Test API", "version": "1.0.0" },
1191            "paths": {},
1192            "components": {
1193                "schemas": {
1194                    "User": {
1195                        "type": "object",
1196                        "x-rust-attrs": [
1197                            "#[derive(Serialize, Deserialize)]",
1198                            "#[serde(rename_all = \"camelCase\")]"
1199                        ],
1200                        "properties": {
1201                            "name": { "type": "string" }
1202                        }
1203                    }
1204                }
1205            }
1206        }))
1207        .expect("Failed to deserialize OpenAPI spec");
1208
1209        let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1210
1211        let user_model = models.iter().find(|m| m.name() == "User");
1212        assert!(user_model.is_some(), "Expected User model");
1213
1214        match user_model.unwrap() {
1215            ModelType::Struct(model) => {
1216                assert!(model.custom_attrs.is_some(), "Expected custom_attrs");
1217                let attrs = model.custom_attrs.as_ref().unwrap();
1218                assert_eq!(attrs.len(), 2);
1219                assert_eq!(attrs[0], "#[derive(Serialize, Deserialize)]");
1220                assert_eq!(attrs[1], "#[serde(rename_all = \"camelCase\")]");
1221            }
1222            _ => panic!("Expected Struct model"),
1223        }
1224    }
1225
1226    #[test]
1227    fn test_x_rust_attrs_on_enum() {
1228        let openapi_spec: OpenAPI = serde_json::from_value(json!({
1229            "openapi": "3.0.0",
1230            "info": { "title": "Test API", "version": "1.0.0" },
1231            "paths": {},
1232            "components": {
1233                "schemas": {
1234                    "Status": {
1235                        "type": "string",
1236                        "enum": ["active", "inactive"],
1237                        "x-rust-attrs": ["#[serde(rename_all = \"UPPERCASE\")]"]
1238                    }
1239                }
1240            }
1241        }))
1242        .expect("Failed to deserialize OpenAPI spec");
1243
1244        let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1245
1246        let status_model = models.iter().find(|m| m.name() == "Status");
1247        assert!(status_model.is_some(), "Expected Status model");
1248
1249        match status_model.unwrap() {
1250            ModelType::Enum(enum_model) => {
1251                assert!(enum_model.custom_attrs.is_some());
1252                let attrs = enum_model.custom_attrs.as_ref().unwrap();
1253                assert_eq!(attrs.len(), 1);
1254                assert_eq!(attrs[0], "#[serde(rename_all = \"UPPERCASE\")]");
1255            }
1256            _ => panic!("Expected Enum model"),
1257        }
1258    }
1259
1260    #[test]
1261    fn test_x_rust_attrs_with_x_rust_type() {
1262        let openapi_spec: OpenAPI = serde_json::from_value(json!({
1263            "openapi": "3.0.0",
1264            "info": { "title": "Test API", "version": "1.0.0" },
1265            "paths": {},
1266            "components": {
1267                "schemas": {
1268                    "User": {
1269                        "type": "object",
1270                        "x-rust-type": "crate::domain::User",
1271                        "x-rust-attrs": ["#[cfg_attr(test, derive(Default))]"],
1272                        "properties": {
1273                            "name": { "type": "string" }
1274                        }
1275                    }
1276                }
1277            }
1278        }))
1279        .expect("Failed to deserialize OpenAPI spec");
1280
1281        let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1282
1283        let user_model = models.iter().find(|m| m.name() == "User");
1284        assert!(user_model.is_some(), "Expected User model");
1285
1286        // Должен быть TypeAlias с атрибутами
1287        match user_model.unwrap() {
1288            ModelType::TypeAlias(alias) => {
1289                assert_eq!(alias.target_type, "crate::domain::User");
1290                assert!(alias.custom_attrs.is_some());
1291                let attrs = alias.custom_attrs.as_ref().unwrap();
1292                assert_eq!(attrs.len(), 1);
1293                assert_eq!(attrs[0], "#[cfg_attr(test, derive(Default))]");
1294            }
1295            _ => panic!("Expected TypeAlias with custom attrs"),
1296        }
1297    }
1298
1299    #[test]
1300    fn test_x_rust_attrs_empty_array() {
1301        let openapi_spec: OpenAPI = serde_json::from_value(json!({
1302            "openapi": "3.0.0",
1303            "info": { "title": "Test API", "version": "1.0.0" },
1304            "paths": {},
1305            "components": {
1306                "schemas": {
1307                    "User": {
1308                        "type": "object",
1309                        "x-rust-attrs": [],
1310                        "properties": {
1311                            "name": { "type": "string" }
1312                        }
1313                    }
1314                }
1315            }
1316        }))
1317        .expect("Failed to deserialize OpenAPI spec");
1318
1319        let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1320
1321        let user_model = models.iter().find(|m| m.name() == "User");
1322        assert!(user_model.is_some());
1323
1324        match user_model.unwrap() {
1325            ModelType::Struct(model) => {
1326                // Пустой массив должен результировать в None
1327                assert!(model.custom_attrs.is_none());
1328            }
1329            _ => panic!("Expected Struct"),
1330        }
1331    }
1332}