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            // Проверяем наличие x-rust-type - если есть, генерируем type alias
238            if let Some(rust_type) = schema.schema_data.extensions.get("x-rust-type") {
239                if let Some(type_str) = rust_type.as_str() {
240                    return Ok(vec![ModelType::TypeAlias(TypeAliasModel {
241                        name: to_pascal_case(name),
242                        target_type: type_str.to_string(),
243                        description: schema.schema_data.description.clone(),
244                        custom_attrs: extract_custom_attrs(schema),
245                    })]);
246                }
247            }
248
249            match &schema.schema_kind {
250                // regular objects
251                SchemaKind::Type(Type::Object(obj)) => {
252                    let mut fields = Vec::new();
253                    let mut inline_models = Vec::new();
254                    for (field_name, field_schema) in &obj.properties {
255                        let (field_info, inline_model) = match field_schema {
256                            ReferenceOr::Item(boxed_schema) => extract_field_info(
257                                field_name,
258                                &ReferenceOr::Item((**boxed_schema).clone()),
259                                all_schemas,
260                            )?,
261                            ReferenceOr::Reference { reference } => extract_field_info(
262                                field_name,
263                                &ReferenceOr::Reference {
264                                    reference: reference.clone(),
265                                },
266                                all_schemas,
267                            )?,
268                        };
269                        if let Some(inline_model) = inline_model {
270                            inline_models.push(inline_model);
271                        }
272                        let is_required = obj.required.contains(field_name);
273                        fields.push(Field {
274                            name: field_name.clone(),
275                            field_type: field_info.field_type,
276                            format: field_info.format,
277                            is_required,
278                            is_nullable: field_info.is_nullable,
279                        });
280                    }
281                    let mut models = inline_models;
282                    if !fields.is_empty() {
283                        models.push(ModelType::Struct(Model {
284                            name: to_pascal_case(name),
285                            fields,
286                            custom_attrs: extract_custom_attrs(schema),
287                        }));
288                    }
289                    Ok(models)
290                }
291
292                // allOf
293                SchemaKind::AllOf { all_of } => {
294                    let (all_fields, inline_models) =
295                        resolve_all_of_fields(name, all_of, all_schemas)?;
296                    let mut models = inline_models;
297
298                    if !all_fields.is_empty() {
299                        models.push(ModelType::Composition(CompositionModel {
300                            name: to_pascal_case(name),
301                            all_fields,
302                            custom_attrs: extract_custom_attrs(schema),
303                        }));
304                    }
305
306                    Ok(models)
307                }
308
309                // oneOf
310                SchemaKind::OneOf { one_of } => {
311                    let (variants, inline_models) =
312                        resolve_union_variants(name, one_of, all_schemas)?;
313                    let mut models = inline_models;
314
315                    models.push(ModelType::Union(UnionModel {
316                        name: to_pascal_case(name),
317                        variants,
318                        union_type: UnionType::OneOf,
319                        custom_attrs: extract_custom_attrs(schema),
320                    }));
321
322                    Ok(models)
323                }
324
325                // anyOf
326                SchemaKind::AnyOf { any_of } => {
327                    let (variants, inline_models) =
328                        resolve_union_variants(name, any_of, all_schemas)?;
329                    let mut models = inline_models;
330
331                    models.push(ModelType::Union(UnionModel {
332                        name: to_pascal_case(name),
333                        variants,
334                        union_type: UnionType::AnyOf,
335                        custom_attrs: extract_custom_attrs(schema),
336                    }));
337
338                    Ok(models)
339                }
340
341                // enum strings
342                SchemaKind::Type(Type::String(string_type)) => {
343                    if !string_type.enumeration.is_empty() {
344                        let variants: Vec<String> = string_type
345                            .enumeration
346                            .iter()
347                            .filter_map(|value| value.clone())
348                            .collect();
349
350                        if !variants.is_empty() {
351                            let models = vec![ModelType::Enum(EnumModel {
352                                name: to_pascal_case(name),
353                                variants,
354                                description: schema.schema_data.description.clone(),
355                                custom_attrs: extract_custom_attrs(schema),
356                            })];
357
358                            return Ok(models);
359                        }
360                    }
361                    Ok(Vec::new())
362                }
363
364                _ => Ok(Vec::new()),
365            }
366        }
367    }
368}
369
370fn extract_type_and_format(
371    schema: &ReferenceOr<Schema>,
372    all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
373) -> Result<(String, String)> {
374    match schema {
375        ReferenceOr::Reference { reference } => {
376            let type_name = reference.split('/').next_back().unwrap_or("Unknown");
377
378            if let Some(ReferenceOr::Item(schema)) = all_schemas.get(type_name) {
379                if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
380                    return Ok((to_pascal_case(type_name), "oneOf".to_string()));
381                }
382            }
383            Ok((to_pascal_case(type_name), "reference".to_string()))
384        }
385
386        ReferenceOr::Item(schema) => match &schema.schema_kind {
387            SchemaKind::Type(Type::String(string_type)) => match &string_type.format {
388                VariantOrUnknownOrEmpty::Item(fmt) => match fmt {
389                    StringFormat::DateTime => {
390                        Ok(("DateTime<Utc>".to_string(), "date-time".to_string()))
391                    }
392                    StringFormat::Date => Ok(("NaiveDate".to_string(), "date".to_string())),
393                    _ => Ok(("String".to_string(), format!("{fmt:?}"))),
394                },
395                VariantOrUnknownOrEmpty::Unknown(unknown_format) => {
396                    if unknown_format.to_lowercase() == "uuid" {
397                        Ok(("Uuid".to_string(), "uuid".to_string()))
398                    } else {
399                        Ok(("String".to_string(), unknown_format.clone()))
400                    }
401                }
402                _ => Ok(("String".to_string(), "string".to_string())),
403            },
404            SchemaKind::Type(Type::Integer(_)) => Ok(("i64".to_string(), "integer".to_string())),
405            SchemaKind::Type(Type::Number(_)) => Ok(("f64".to_string(), "number".to_string())),
406            SchemaKind::Type(Type::Boolean(_)) => Ok(("bool".to_string(), "boolean".to_string())),
407            SchemaKind::Type(Type::Array(arr)) => {
408                if let Some(items) = &arr.items {
409                    let items_ref: &ReferenceOr<Box<Schema>> = items;
410                    let (inner_type, format) = match items_ref {
411                        ReferenceOr::Item(boxed_schema) => extract_type_and_format(
412                            &ReferenceOr::Item((**boxed_schema).clone()),
413                            all_schemas,
414                        )?,
415                        ReferenceOr::Reference { reference } => {
416                            let type_name = reference.split('/').next_back().unwrap_or("Unknown");
417
418                            if let Some(ReferenceOr::Item(schema)) = all_schemas.get(type_name) {
419                                if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
420                                    (to_pascal_case(type_name), "oneOf".to_string())
421                                } else {
422                                    extract_type_and_format(
423                                        &ReferenceOr::Reference {
424                                            reference: reference.clone(),
425                                        },
426                                        all_schemas,
427                                    )?
428                                }
429                            } else {
430                                extract_type_and_format(
431                                    &ReferenceOr::Reference {
432                                        reference: reference.clone(),
433                                    },
434                                    all_schemas,
435                                )?
436                            }
437                        }
438                    };
439                    Ok((format!("Vec<{inner_type}>"), format))
440                } else {
441                    Ok(("Vec<serde_json::Value>".to_string(), "array".to_string()))
442                }
443            }
444            SchemaKind::Type(Type::Object(_obj)) => {
445                Ok(("serde_json::Value".to_string(), "object".to_string()))
446            }
447            _ => Ok(("serde_json::Value".to_string(), "unknown".to_string())),
448        },
449    }
450}
451
452/// Extracts field information including type, format, and nullable flag from OpenAPI schema
453fn extract_field_info(
454    field_name: &str,
455    schema: &ReferenceOr<Schema>,
456    all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
457) -> Result<(FieldInfo, Option<ModelType>)> {
458    let (mut field_type, format) = extract_type_and_format(schema, all_schemas)?;
459
460    let (is_nullable, en) = match schema {
461        ReferenceOr::Reference { reference } => {
462            let is_nullable =
463                if let Some(type_name) = reference.strip_prefix("#/components/schemas/") {
464                    all_schemas
465                        .get(type_name)
466                        .and_then(|s| match s {
467                            ReferenceOr::Item(schema) => Some(schema.schema_data.nullable),
468                            _ => None,
469                        })
470                        .unwrap_or(false)
471                } else {
472                    false
473                };
474            (is_nullable, None)
475        }
476
477        ReferenceOr::Item(schema) => {
478            let is_nullable = schema.schema_data.nullable;
479
480            let maybe_enum = match &schema.schema_kind {
481                SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
482                    let variants: Vec<String> =
483                        s.enumeration.iter().filter_map(|v| v.clone()).collect();
484                    field_type = to_pascal_case(field_name);
485                    Some(ModelType::Enum(EnumModel {
486                        name: to_pascal_case(field_name),
487                        variants,
488                        description: schema.schema_data.description.clone(),
489                        custom_attrs: extract_custom_attrs(schema),
490                    }))
491                }
492                SchemaKind::Type(Type::Object(_)) => {
493                    field_type = "serde_json::Value".to_string();
494                    None
495                }
496                _ => None,
497            };
498            (is_nullable, maybe_enum)
499        }
500    };
501
502    Ok((
503        FieldInfo {
504            field_type,
505            format,
506            is_nullable,
507        },
508        en,
509    ))
510}
511
512fn resolve_all_of_fields(
513    _name: &str,
514    all_of: &[ReferenceOr<Schema>],
515    all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
516) -> Result<(Vec<Field>, Vec<ModelType>)> {
517    let mut all_fields = Vec::new();
518    let mut models = Vec::new();
519    let mut all_required_fields = HashSet::new();
520
521    for schema_ref in all_of {
522        let schema_to_check = match schema_ref {
523            ReferenceOr::Reference { reference } => reference
524                .strip_prefix("#/components/schemas/")
525                .and_then(|schema_name| all_schemas.get(schema_name)),
526            ReferenceOr::Item(_) => Some(schema_ref),
527        };
528
529        if let Some(ReferenceOr::Item(schema)) = schema_to_check {
530            if let SchemaKind::Type(Type::Object(obj)) = &schema.schema_kind {
531                all_required_fields.extend(obj.required.iter().cloned());
532            }
533        }
534    }
535
536    // Теперь собираем поля из всех схем
537    for schema_ref in all_of {
538        match schema_ref {
539            ReferenceOr::Reference { reference } => {
540                if let Some(schema_name) = reference.strip_prefix("#/components/schemas/") {
541                    if let Some(referenced_schema) = all_schemas.get(schema_name) {
542                        let (fields, inline_models) =
543                            extract_fields_from_schema(referenced_schema, all_schemas)?;
544                        all_fields.extend(fields);
545                        models.extend(inline_models);
546                    }
547                }
548            }
549            ReferenceOr::Item(_schema) => {
550                let (fields, inline_models) = extract_fields_from_schema(schema_ref, all_schemas)?;
551                all_fields.extend(fields);
552                models.extend(inline_models);
553            }
554        }
555    }
556
557    // Обновляем is_required для полей на основе объединенного множества required
558    for field in &mut all_fields {
559        if all_required_fields.contains(&field.name) {
560            field.is_required = true;
561        }
562    }
563
564    Ok((all_fields, models))
565}
566
567fn resolve_union_variants(
568    name: &str,
569    schemas: &[ReferenceOr<Schema>],
570    all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
571) -> Result<(Vec<UnionVariant>, Vec<ModelType>)> {
572    use std::collections::BTreeSet;
573
574    let mut variants = Vec::new();
575    let mut models = Vec::new();
576    let mut enum_values: BTreeSet<String> = BTreeSet::new();
577    let mut is_all_simple_enum = true;
578
579    for schema_ref in schemas {
580        let resolved = match schema_ref {
581            ReferenceOr::Reference { reference } => reference
582                .strip_prefix("#/components/schemas/")
583                .and_then(|n| all_schemas.get(n)),
584            ReferenceOr::Item(_) => Some(schema_ref),
585        };
586
587        let Some(resolved_schema) = resolved else {
588            is_all_simple_enum = false;
589            continue;
590        };
591
592        match resolved_schema {
593            ReferenceOr::Item(schema) => match &schema.schema_kind {
594                SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
595                    enum_values.extend(s.enumeration.iter().filter_map(|v| v.as_ref().cloned()));
596                }
597                SchemaKind::Type(Type::Integer(n)) if !n.enumeration.is_empty() => {
598                    enum_values.extend(
599                        n.enumeration
600                            .iter()
601                            .filter_map(|v| v.map(|num| format!("Value{num}"))),
602                    );
603                }
604
605                _ => is_all_simple_enum = false,
606            },
607            ReferenceOr::Reference { reference } => {
608                if let Some(n) = reference.strip_prefix("#/components/schemas/") {
609                    if let Some(ReferenceOr::Item(inner)) = all_schemas.get(n) {
610                        if let SchemaKind::Type(Type::String(s)) = &inner.schema_kind {
611                            let values: Vec<String> = s
612                                .enumeration
613                                .iter()
614                                .filter_map(|v| v.as_ref().cloned())
615                                .collect();
616                            enum_values.extend(values);
617                        } else {
618                            is_all_simple_enum = false;
619                        }
620                    }
621                }
622            }
623        }
624    }
625    if is_all_simple_enum && !enum_values.is_empty() {
626        let enum_name = to_pascal_case(name);
627        let enum_model = ModelType::Enum(EnumModel {
628            name: enum_name.clone(),
629            variants: enum_values.iter().map(|v| to_pascal_case(v)).collect(),
630            description: None,
631            custom_attrs: None, // Collective enum from multiple schemas, no single source for attrs
632        });
633
634        return Ok((vec![], vec![enum_model]));
635    }
636
637    // fallback for usual union-schemas
638    for (index, schema_ref) in schemas.iter().enumerate() {
639        match schema_ref {
640            ReferenceOr::Reference { reference } => {
641                if let Some(schema_name) = reference.strip_prefix("#/components/schemas/") {
642                    if let Some(referenced_schema) = all_schemas.get(schema_name) {
643                        if let ReferenceOr::Item(schema) = referenced_schema {
644                            if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
645                                variants.push(UnionVariant {
646                                    name: to_pascal_case(schema_name),
647                                    fields: vec![],
648                                });
649                            } else {
650                                let (fields, inline_models) =
651                                    extract_fields_from_schema(referenced_schema, all_schemas)?;
652                                variants.push(UnionVariant {
653                                    name: to_pascal_case(schema_name),
654                                    fields,
655                                });
656                                models.extend(inline_models);
657                            }
658                        }
659                    }
660                }
661            }
662            ReferenceOr::Item(_) => {
663                let (fields, inline_models) = extract_fields_from_schema(schema_ref, all_schemas)?;
664                let variant_name = format!("Variant{index}");
665                variants.push(UnionVariant {
666                    name: variant_name,
667                    fields,
668                });
669                models.extend(inline_models);
670            }
671        }
672    }
673
674    Ok((variants, models))
675}
676
677fn extract_fields_from_schema(
678    schema_ref: &ReferenceOr<Schema>,
679    _all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
680) -> Result<(Vec<Field>, Vec<ModelType>)> {
681    let mut fields = Vec::new();
682    let mut inline_models = Vec::new();
683
684    match schema_ref {
685        ReferenceOr::Reference { .. } => Ok((fields, inline_models)),
686        ReferenceOr::Item(schema) => {
687            match &schema.schema_kind {
688                SchemaKind::Type(Type::Object(obj)) => {
689                    for (field_name, field_schema) in &obj.properties {
690                        let (field_info, inline_model) = match field_schema {
691                            ReferenceOr::Item(boxed_schema) => extract_field_info(
692                                field_name,
693                                &ReferenceOr::Item((**boxed_schema).clone()),
694                                _all_schemas,
695                            )?,
696                            ReferenceOr::Reference { reference } => extract_field_info(
697                                field_name,
698                                &ReferenceOr::Reference {
699                                    reference: reference.clone(),
700                                },
701                                _all_schemas,
702                            )?,
703                        };
704
705                        let is_nullable = field_info.is_nullable
706                            || field_name == "value"
707                            || field_name == "default_value";
708
709                        let field_type = field_info.field_type.clone();
710
711                        let is_required = obj.required.contains(field_name);
712                        fields.push(Field {
713                            name: field_name.clone(),
714                            field_type,
715                            format: field_info.format,
716                            is_required,
717                            is_nullable,
718                        });
719                        if let Some(inline_model) = inline_model {
720                            match &inline_model {
721                                ModelType::Struct(m) if m.fields.is_empty() => {}
722                                _ => inline_models.push(inline_model),
723                            }
724                        }
725                    }
726                }
727                SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
728                    let name = schema
729                        .schema_data
730                        .title
731                        .clone()
732                        .unwrap_or_else(|| "AnonymousStringEnum".to_string());
733
734                    let enum_model = ModelType::Enum(EnumModel {
735                        name,
736                        variants: s
737                            .enumeration
738                            .iter()
739                            .filter_map(|v| v.as_ref().map(|s| to_pascal_case(s)))
740                            .collect(),
741                        description: schema.schema_data.description.clone(),
742                        custom_attrs: extract_custom_attrs(schema),
743                    });
744
745                    inline_models.push(enum_model);
746                }
747                SchemaKind::Type(Type::Integer(n)) if !n.enumeration.is_empty() => {
748                    let name = schema
749                        .schema_data
750                        .title
751                        .clone()
752                        .unwrap_or_else(|| "AnonymousIntEnum".to_string());
753
754                    let enum_model = ModelType::Enum(EnumModel {
755                        name,
756                        variants: n
757                            .enumeration
758                            .iter()
759                            .filter_map(|v| v.map(|num| format!("Value{num}")))
760                            .collect(),
761                        description: schema.schema_data.description.clone(),
762                        custom_attrs: extract_custom_attrs(schema),
763                    });
764
765                    inline_models.push(enum_model);
766                }
767
768                _ => {}
769            }
770
771            Ok((fields, inline_models))
772        }
773    }
774}
775
776#[cfg(test)]
777mod tests {
778    use super::*;
779    use serde_json::json;
780
781    #[test]
782    fn test_parse_inline_request_body_generates_model() {
783        let openapi_spec: OpenAPI = serde_json::from_value(json!({
784            "openapi": "3.0.0",
785            "info": { "title": "Test API", "version": "1.0.0" },
786            "paths": {
787                "/items": {
788                    "post": {
789                        "operationId": "createItem",
790                        "requestBody": {
791                            "content": {
792                                "application/json": {
793                                    "schema": {
794                                        "type": "object",
795                                        "properties": {
796                                            "name": { "type": "string" },
797                                            "value": { "type": "integer" }
798                                        },
799                                        "required": ["name"]
800                                    }
801                                }
802                            }
803                        },
804                        "responses": { "200": { "description": "OK" } }
805                    }
806                }
807            }
808        }))
809        .expect("Failed to deserialize OpenAPI spec");
810
811        let (models, requests, _responses) =
812            parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
813
814        // 1. Проверяем, что модель запроса была создана
815        assert_eq!(requests.len(), 1);
816        let request_model = &requests[0];
817        assert_eq!(request_model.name, "CreateItemRequest");
818
819        // 2. Проверяем, что схема запроса ссылается на НОВУЮ модель, а не на Value
820        assert_eq!(request_model.schema, "CreateItemRequestBody");
821
822        // 3. Проверяем, что сама модель для тела запроса была сгенерирована
823        let inline_model = models.iter().find(|m| m.name() == "CreateItemRequestBody");
824        assert!(
825            inline_model.is_some(),
826            "Expected a model named 'CreateItemRequestBody' to be generated"
827        );
828
829        if let Some(ModelType::Struct(model)) = inline_model {
830            assert_eq!(model.fields.len(), 2);
831            assert_eq!(model.fields[0].name, "name");
832            assert_eq!(model.fields[0].field_type, "String");
833            assert!(model.fields[0].is_required);
834
835            assert_eq!(model.fields[1].name, "value");
836            assert_eq!(model.fields[1].field_type, "i64");
837            assert!(!model.fields[1].is_required);
838        } else {
839            panic!("Expected a Struct model for CreateItemRequestBody");
840        }
841    }
842
843    #[test]
844    fn test_parse_ref_request_body_works() {
845        let openapi_spec: OpenAPI = serde_json::from_value(json!({
846            "openapi": "3.0.0",
847            "info": { "title": "Test API", "version": "1.0.0" },
848            "components": {
849                "schemas": {
850                    "ItemData": {
851                        "type": "object",
852                        "properties": {
853                            "name": { "type": "string" }
854                        }
855                    }
856                },
857                "requestBodies": {
858                    "CreateItem": {
859                        "content": {
860                            "application/json": {
861                                "schema": { "$ref": "#/components/schemas/ItemData" }
862                            }
863                        }
864                    }
865                }
866            },
867            "paths": {
868                "/items": {
869                    "post": {
870                        "operationId": "createItem",
871                        "requestBody": { "$ref": "#/components/requestBodies/CreateItem" },
872                        "responses": { "200": { "description": "OK" } }
873                    }
874                }
875            }
876        }))
877        .expect("Failed to deserialize OpenAPI spec");
878
879        let (models, requests, _responses) =
880            parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
881
882        // Проверяем, что модель запроса была создана
883        assert_eq!(requests.len(), 1);
884        let request_model = &requests[0];
885        assert_eq!(request_model.name, "CreateItemRequest");
886
887        // Проверяем, что схема ссылается на существующую модель
888        assert_eq!(request_model.schema, "ItemData");
889
890        // Проверяем, что сама модель ItemData существует в списке моделей
891        assert!(models.iter().any(|m| m.name() == "ItemData"));
892    }
893
894    #[test]
895    fn test_parse_no_request_body() {
896        let openapi_spec: OpenAPI = serde_json::from_value(json!({
897            "openapi": "3.0.0",
898            "info": { "title": "Test API", "version": "1.0.0" },
899            "paths": {
900                "/items": {
901                    "get": {
902                        "operationId": "listItems",
903                        "responses": { "200": { "description": "OK" } }
904                    }
905                }
906            }
907        }))
908        .expect("Failed to deserialize OpenAPI spec");
909
910        let (_models, requests, _responses) =
911            parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
912
913        // Убеждаемся, что моделей запросов не создано
914        assert!(requests.is_empty());
915    }
916
917    #[test]
918    fn test_nullable_reference_field() {
919        // Тест проверяет что nullable корректно читается из целевой схемы при использовании $ref
920        let openapi_spec: OpenAPI = serde_json::from_value(json!({
921            "openapi": "3.0.0",
922            "info": { "title": "Test API", "version": "1.0.0" },
923            "paths": {},
924            "components": {
925                "schemas": {
926                    "NullableUser": {
927                        "type": "object",
928                        "nullable": true,
929                        "properties": {
930                            "name": { "type": "string" }
931                        }
932                    },
933                    "Post": {
934                        "type": "object",
935                        "properties": {
936                            "author": {
937                                "$ref": "#/components/schemas/NullableUser"
938                            }
939                        }
940                    }
941                }
942            }
943        }))
944        .expect("Failed to deserialize OpenAPI spec");
945
946        let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
947
948        // Находим модель Post
949        let post_model = models.iter().find(|m| m.name() == "Post");
950        assert!(post_model.is_some(), "Expected Post model to be generated");
951
952        if let Some(ModelType::Struct(post)) = post_model {
953            let author_field = post.fields.iter().find(|f| f.name == "author");
954            assert!(author_field.is_some(), "Expected author field");
955
956            // Проверяем что nullable правильно обработан для ссылочного типа
957            // (nullable берется из целевой схемы NullableUser)
958            let author = author_field.unwrap();
959            assert!(
960                author.is_nullable,
961                "Expected author field to be nullable (from referenced schema)"
962            );
963        } else {
964            panic!("Expected Post to be a Struct");
965        }
966    }
967
968    #[test]
969    fn test_allof_required_fields_merge() {
970        let openapi_spec: OpenAPI = serde_json::from_value(json!({
971            "openapi": "3.0.0",
972            "info": { "title": "Test API", "version": "1.0.0" },
973            "paths": {},
974            "components": {
975                "schemas": {
976                    "BaseEntity": {
977                        "type": "object",
978                        "properties": {
979                            "id": { "type": "string" },
980                            "created": { "type": "string" }
981                        },
982                        "required": ["id"]
983                    },
984                    "Person": {
985                        "allOf": [
986                            { "$ref": "#/components/schemas/BaseEntity" },
987                            {
988                                "type": "object",
989                                "properties": {
990                                    "name": { "type": "string" },
991                                    "age": { "type": "integer" }
992                                },
993                                "required": ["name"]
994                            }
995                        ]
996                    }
997                }
998            }
999        }))
1000        .expect("Failed to deserialize OpenAPI spec");
1001
1002        let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1003
1004        // Находим модель Person
1005        let person_model = models.iter().find(|m| m.name() == "Person");
1006        assert!(
1007            person_model.is_some(),
1008            "Expected Person model to be generated"
1009        );
1010
1011        if let Some(ModelType::Composition(person)) = person_model {
1012            // Проверяем что id (из BaseEntity) обязательное
1013            let id_field = person.all_fields.iter().find(|f| f.name == "id");
1014            assert!(id_field.is_some(), "Expected id field");
1015            assert!(
1016                id_field.unwrap().is_required,
1017                "Expected id to be required from BaseEntity"
1018            );
1019
1020            // Проверяем что name (из второго объекта) обязательное
1021            let name_field = person.all_fields.iter().find(|f| f.name == "name");
1022            assert!(name_field.is_some(), "Expected name field");
1023            assert!(
1024                name_field.unwrap().is_required,
1025                "Expected name to be required from inline object"
1026            );
1027
1028            // Проверяем что created и age не обязательные
1029            let created_field = person.all_fields.iter().find(|f| f.name == "created");
1030            assert!(created_field.is_some(), "Expected created field");
1031            assert!(
1032                !created_field.unwrap().is_required,
1033                "Expected created to be optional"
1034            );
1035
1036            let age_field = person.all_fields.iter().find(|f| f.name == "age");
1037            assert!(age_field.is_some(), "Expected age field");
1038            assert!(
1039                !age_field.unwrap().is_required,
1040                "Expected age to be optional"
1041            );
1042        } else {
1043            panic!("Expected Person to be a Composition");
1044        }
1045    }
1046
1047    #[test]
1048    fn test_x_rust_type_generates_type_alias() {
1049        let openapi_spec: OpenAPI = serde_json::from_value(json!({
1050            "openapi": "3.0.0",
1051            "info": { "title": "Test API", "version": "1.0.0" },
1052            "paths": {},
1053            "components": {
1054                "schemas": {
1055                    "User": {
1056                        "type": "object",
1057                        "x-rust-type": "crate::domain::User",
1058                        "description": "Custom domain user type",
1059                        "properties": {
1060                            "name": { "type": "string" },
1061                            "age": { "type": "integer" }
1062                        }
1063                    }
1064                }
1065            }
1066        }))
1067        .expect("Failed to deserialize OpenAPI spec");
1068
1069        let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1070
1071        // Проверяем что создан TypeAlias, а не Struct
1072        let user_model = models.iter().find(|m| m.name() == "User");
1073        assert!(user_model.is_some(), "Expected User model");
1074
1075        match user_model.unwrap() {
1076            ModelType::TypeAlias(alias) => {
1077                assert_eq!(alias.name, "User");
1078                assert_eq!(alias.target_type, "crate::domain::User");
1079                assert_eq!(
1080                    alias.description,
1081                    Some("Custom domain user type".to_string())
1082                );
1083            }
1084            _ => panic!("Expected TypeAlias, got different type"),
1085        }
1086    }
1087
1088    #[test]
1089    fn test_x_rust_type_works_with_enum() {
1090        let openapi_spec: OpenAPI = serde_json::from_value(json!({
1091            "openapi": "3.0.0",
1092            "info": { "title": "Test API", "version": "1.0.0" },
1093            "paths": {},
1094            "components": {
1095                "schemas": {
1096                    "Status": {
1097                        "type": "string",
1098                        "enum": ["active", "inactive"],
1099                        "x-rust-type": "crate::domain::Status"
1100                    }
1101                }
1102            }
1103        }))
1104        .expect("Failed to deserialize OpenAPI spec");
1105
1106        let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1107
1108        let status_model = models.iter().find(|m| m.name() == "Status");
1109        assert!(status_model.is_some(), "Expected Status model");
1110
1111        // Должен быть TypeAlias, не Enum
1112        assert!(
1113            matches!(status_model.unwrap(), ModelType::TypeAlias(_)),
1114            "Expected TypeAlias for enum with x-rust-type"
1115        );
1116    }
1117
1118    #[test]
1119    fn test_x_rust_type_works_with_oneof() {
1120        let openapi_spec: OpenAPI = serde_json::from_value(json!({
1121            "openapi": "3.0.0",
1122            "info": { "title": "Test API", "version": "1.0.0" },
1123            "paths": {},
1124            "components": {
1125                "schemas": {
1126                    "Payment": {
1127                        "oneOf": [
1128                            { "type": "object", "properties": { "card": { "type": "string" } } },
1129                            { "type": "object", "properties": { "cash": { "type": "number" } } }
1130                        ],
1131                        "x-rust-type": "payments::Payment"
1132                    }
1133                }
1134            }
1135        }))
1136        .expect("Failed to deserialize OpenAPI spec");
1137
1138        let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1139
1140        let payment_model = models.iter().find(|m| m.name() == "Payment");
1141        assert!(payment_model.is_some(), "Expected Payment model");
1142
1143        // Должен быть TypeAlias, не Union
1144        match payment_model.unwrap() {
1145            ModelType::TypeAlias(alias) => {
1146                assert_eq!(alias.target_type, "payments::Payment");
1147            }
1148            _ => panic!("Expected TypeAlias for oneOf with x-rust-type"),
1149        }
1150    }
1151
1152    #[test]
1153    fn test_x_rust_attrs_on_struct() {
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                    "User": {
1161                        "type": "object",
1162                        "x-rust-attrs": [
1163                            "#[derive(Serialize, Deserialize)]",
1164                            "#[serde(rename_all = \"camelCase\")]"
1165                        ],
1166                        "properties": {
1167                            "name": { "type": "string" }
1168                        }
1169                    }
1170                }
1171            }
1172        }))
1173        .expect("Failed to deserialize OpenAPI spec");
1174
1175        let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1176
1177        let user_model = models.iter().find(|m| m.name() == "User");
1178        assert!(user_model.is_some(), "Expected User model");
1179
1180        match user_model.unwrap() {
1181            ModelType::Struct(model) => {
1182                assert!(model.custom_attrs.is_some(), "Expected custom_attrs");
1183                let attrs = model.custom_attrs.as_ref().unwrap();
1184                assert_eq!(attrs.len(), 2);
1185                assert_eq!(attrs[0], "#[derive(Serialize, Deserialize)]");
1186                assert_eq!(attrs[1], "#[serde(rename_all = \"camelCase\")]");
1187            }
1188            _ => panic!("Expected Struct model"),
1189        }
1190    }
1191
1192    #[test]
1193    fn test_x_rust_attrs_on_enum() {
1194        let openapi_spec: OpenAPI = serde_json::from_value(json!({
1195            "openapi": "3.0.0",
1196            "info": { "title": "Test API", "version": "1.0.0" },
1197            "paths": {},
1198            "components": {
1199                "schemas": {
1200                    "Status": {
1201                        "type": "string",
1202                        "enum": ["active", "inactive"],
1203                        "x-rust-attrs": ["#[serde(rename_all = \"UPPERCASE\")]"]
1204                    }
1205                }
1206            }
1207        }))
1208        .expect("Failed to deserialize OpenAPI spec");
1209
1210        let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1211
1212        let status_model = models.iter().find(|m| m.name() == "Status");
1213        assert!(status_model.is_some(), "Expected Status model");
1214
1215        match status_model.unwrap() {
1216            ModelType::Enum(enum_model) => {
1217                assert!(enum_model.custom_attrs.is_some());
1218                let attrs = enum_model.custom_attrs.as_ref().unwrap();
1219                assert_eq!(attrs.len(), 1);
1220                assert_eq!(attrs[0], "#[serde(rename_all = \"UPPERCASE\")]");
1221            }
1222            _ => panic!("Expected Enum model"),
1223        }
1224    }
1225
1226    #[test]
1227    fn test_x_rust_attrs_with_x_rust_type() {
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                    "User": {
1235                        "type": "object",
1236                        "x-rust-type": "crate::domain::User",
1237                        "x-rust-attrs": ["#[cfg_attr(test, derive(Default))]"],
1238                        "properties": {
1239                            "name": { "type": "string" }
1240                        }
1241                    }
1242                }
1243            }
1244        }))
1245        .expect("Failed to deserialize OpenAPI spec");
1246
1247        let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1248
1249        let user_model = models.iter().find(|m| m.name() == "User");
1250        assert!(user_model.is_some(), "Expected User model");
1251
1252        // Должен быть TypeAlias с атрибутами
1253        match user_model.unwrap() {
1254            ModelType::TypeAlias(alias) => {
1255                assert_eq!(alias.target_type, "crate::domain::User");
1256                assert!(alias.custom_attrs.is_some());
1257                let attrs = alias.custom_attrs.as_ref().unwrap();
1258                assert_eq!(attrs.len(), 1);
1259                assert_eq!(attrs[0], "#[cfg_attr(test, derive(Default))]");
1260            }
1261            _ => panic!("Expected TypeAlias with custom attrs"),
1262        }
1263    }
1264
1265    #[test]
1266    fn test_x_rust_attrs_empty_array() {
1267        let openapi_spec: OpenAPI = serde_json::from_value(json!({
1268            "openapi": "3.0.0",
1269            "info": { "title": "Test API", "version": "1.0.0" },
1270            "paths": {},
1271            "components": {
1272                "schemas": {
1273                    "User": {
1274                        "type": "object",
1275                        "x-rust-attrs": [],
1276                        "properties": {
1277                            "name": { "type": "string" }
1278                        }
1279                    }
1280                }
1281            }
1282        }))
1283        .expect("Failed to deserialize OpenAPI spec");
1284
1285        let (models, _, _) = parse_openapi(&openapi_spec).expect("Failed to parse OpenAPI spec");
1286
1287        let user_model = models.iter().find(|m| m.name() == "User");
1288        assert!(user_model.is_some());
1289
1290        match user_model.unwrap() {
1291            ModelType::Struct(model) => {
1292                // Пустой массив должен результировать в None
1293                assert!(model.custom_attrs.is_none());
1294            }
1295            _ => panic!("Expected Struct"),
1296        }
1297    }
1298}