openapi_model_generator/
parser.rs

1use crate::{
2    models::{
3        CompositionModel, EnumModel, Field, Model, ModelType, RequestModel, ResponseModel,
4        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
38pub fn parse_openapi(
39    openapi: &OpenAPI,
40) -> Result<(Vec<ModelType>, Vec<RequestModel>, Vec<ResponseModel>)> {
41    let mut models = Vec::new();
42    let mut requests = Vec::new();
43    let mut responses = Vec::new();
44
45    let mut added_models = HashSet::new();
46
47    // Parse components/schemas
48    if let Some(components) = &openapi.components {
49        for (name, schema) in &components.schemas {
50            let model_types = parse_schema_to_model_type(name, schema, &components.schemas)?;
51            for model_type in model_types {
52                if added_models.insert(model_type.name().to_string()) {
53                    models.push(model_type);
54                }
55            }
56        }
57
58        // Parse paths
59        for (_path, path_item) in openapi.paths.iter() {
60            let path_item = match path_item {
61                ReferenceOr::Item(item) => item,
62                ReferenceOr::Reference { .. } => continue,
63            };
64
65            if let Some(op) = &path_item.get {
66                process_operation(op, &mut requests, &mut responses, &components.schemas)?;
67            }
68            if let Some(op) = &path_item.post {
69                process_operation(op, &mut requests, &mut responses, &components.schemas)?;
70            }
71            if let Some(op) = &path_item.put {
72                process_operation(op, &mut requests, &mut responses, &components.schemas)?;
73            }
74            if let Some(op) = &path_item.delete {
75                process_operation(op, &mut requests, &mut responses, &components.schemas)?;
76            }
77            if let Some(op) = &path_item.patch {
78                process_operation(op, &mut requests, &mut responses, &components.schemas)?;
79            }
80        }
81    }
82
83    Ok((models, requests, responses))
84}
85
86fn process_operation(
87    operation: &openapiv3::Operation,
88    requests: &mut Vec<RequestModel>,
89    responses: &mut Vec<ResponseModel>,
90    all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
91) -> Result<()> {
92    // Parse request body
93    if let Some(ReferenceOr::Item(request_body)) = &operation.request_body {
94        for (content_type, media_type) in &request_body.content {
95            if let Some(schema) = &media_type.schema {
96                let request = RequestModel {
97                    name: format!(
98                        "{}Request",
99                        to_pascal_case(operation.operation_id.as_deref().unwrap_or("Unknown"))
100                    ),
101                    content_type: content_type.clone(),
102                    schema: extract_type_and_format(schema, all_schemas)?.0,
103                    is_required: request_body.required,
104                };
105                requests.push(request);
106            }
107        }
108    }
109
110    // Parse responses
111    for (status, response_ref) in operation.responses.responses.iter() {
112        if let ReferenceOr::Item(response) = response_ref {
113            for (content_type, media_type) in &response.content {
114                if let Some(schema) = &media_type.schema {
115                    let response = ResponseModel {
116                        name: format!(
117                            "{}Response",
118                            to_pascal_case(operation.operation_id.as_deref().unwrap_or("Unknown"))
119                        ),
120                        status_code: status.to_string(),
121                        content_type: content_type.clone(),
122                        schema: extract_type_and_format(schema, all_schemas)?.0,
123                        description: Some(response.description.clone()),
124                    };
125                    responses.push(response);
126                }
127            }
128        }
129    }
130    Ok(())
131}
132
133fn parse_schema_to_model_type(
134    name: &str,
135    schema: &ReferenceOr<Schema>,
136    all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
137) -> Result<Vec<ModelType>> {
138    match schema {
139        ReferenceOr::Reference { .. } => Ok(Vec::new()),
140        ReferenceOr::Item(schema) => {
141            match &schema.schema_kind {
142                // regular objects
143                SchemaKind::Type(Type::Object(obj)) => {
144                    let mut fields = Vec::new();
145                    let mut inline_models = Vec::new();
146                    for (field_name, field_schema) in &obj.properties {
147                        let (field_info, inline_model) = match field_schema {
148                            ReferenceOr::Item(boxed_schema) => extract_field_info(
149                                field_name,
150                                &ReferenceOr::Item((**boxed_schema).clone()),
151                                all_schemas,
152                            )?,
153                            ReferenceOr::Reference { reference } => extract_field_info(
154                                field_name,
155                                &ReferenceOr::Reference {
156                                    reference: reference.clone(),
157                                },
158                                all_schemas,
159                            )?,
160                        };
161                        if let Some(inline_model) = inline_model {
162                            inline_models.push(inline_model);
163                        }
164                        let is_required = obj.required.contains(field_name);
165                        fields.push(Field {
166                            name: field_name.clone(),
167                            field_type: field_info.field_type,
168                            format: field_info.format,
169                            is_required,
170                            is_nullable: field_info.is_nullable,
171                        });
172                    }
173                    let mut models = inline_models;
174                    if !fields.is_empty() {
175                        models.push(ModelType::Struct(Model {
176                            name: to_pascal_case(name),
177                            fields,
178                        }));
179                    }
180                    Ok(models)
181                }
182
183                // allOf
184                SchemaKind::AllOf { all_of } => {
185                    let (all_fields, inline_models) =
186                        resolve_all_of_fields(name, all_of, all_schemas)?;
187                    let mut models = inline_models;
188
189                    if !all_fields.is_empty() {
190                        models.push(ModelType::Composition(CompositionModel {
191                            name: to_pascal_case(name),
192                            all_fields,
193                        }));
194                    }
195
196                    Ok(models)
197                }
198
199                // oneOf
200                SchemaKind::OneOf { one_of } => {
201                    let (variants, inline_models) =
202                        resolve_union_variants(name, one_of, all_schemas)?;
203                    let mut models = inline_models;
204
205                    models.push(ModelType::Union(UnionModel {
206                        name: to_pascal_case(name),
207                        variants,
208                        union_type: UnionType::OneOf,
209                    }));
210
211                    Ok(models)
212                }
213
214                // anyOf
215                SchemaKind::AnyOf { any_of } => {
216                    let (variants, inline_models) =
217                        resolve_union_variants(name, any_of, all_schemas)?;
218                    let mut models = inline_models;
219
220                    models.push(ModelType::Union(UnionModel {
221                        name: to_pascal_case(name),
222                        variants,
223                        union_type: UnionType::AnyOf,
224                    }));
225
226                    Ok(models)
227                }
228
229                // enum strings
230                SchemaKind::Type(Type::String(string_type)) => {
231                    if !string_type.enumeration.is_empty() {
232                        let variants: Vec<String> = string_type
233                            .enumeration
234                            .iter()
235                            .filter_map(|value| value.clone())
236                            .collect();
237
238                        if !variants.is_empty() {
239                            let models = vec![ModelType::Enum(EnumModel {
240                                name: to_pascal_case(name),
241                                variants,
242                                description: schema.schema_data.description.clone(),
243                            })];
244
245                            return Ok(models);
246                        }
247                    }
248                    Ok(Vec::new())
249                }
250
251                _ => Ok(Vec::new()),
252            }
253        }
254    }
255}
256
257fn extract_type_and_format(
258    schema: &ReferenceOr<Schema>,
259    all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
260) -> Result<(String, String)> {
261    match schema {
262        ReferenceOr::Reference { reference } => {
263            let type_name = reference.split('/').next_back().unwrap_or("Unknown");
264
265            if let Some(ReferenceOr::Item(schema)) = all_schemas.get(type_name) {
266                if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
267                    return Ok((to_pascal_case(type_name), "oneOf".to_string()));
268                }
269            }
270            Ok((to_pascal_case(type_name), "reference".to_string()))
271        }
272
273        ReferenceOr::Item(schema) => match &schema.schema_kind {
274            SchemaKind::Type(Type::String(string_type)) => match &string_type.format {
275                VariantOrUnknownOrEmpty::Item(fmt) => match fmt {
276                    StringFormat::DateTime => {
277                        Ok(("DateTime<Utc>".to_string(), "date-time".to_string()))
278                    }
279                    StringFormat::Date => Ok(("NaiveDate".to_string(), "date".to_string())),
280                    _ => Ok(("String".to_string(), format!("{fmt:?}"))),
281                },
282                VariantOrUnknownOrEmpty::Unknown(unknown_format) => {
283                    if unknown_format.to_lowercase() == "uuid" {
284                        Ok(("Uuid".to_string(), "uuid".to_string()))
285                    } else {
286                        Ok(("String".to_string(), unknown_format.clone()))
287                    }
288                }
289                _ => Ok(("String".to_string(), "string".to_string())),
290            },
291            SchemaKind::Type(Type::Integer(_)) => Ok(("i64".to_string(), "integer".to_string())),
292            SchemaKind::Type(Type::Number(_)) => Ok(("f64".to_string(), "number".to_string())),
293            SchemaKind::Type(Type::Boolean(_)) => Ok(("bool".to_string(), "boolean".to_string())),
294            SchemaKind::Type(Type::Array(arr)) => {
295                if let Some(items) = &arr.items {
296                    let items_ref: &ReferenceOr<Box<Schema>> = items;
297                    let (inner_type, format) = match items_ref {
298                        ReferenceOr::Item(boxed_schema) => extract_type_and_format(
299                            &ReferenceOr::Item((**boxed_schema).clone()),
300                            all_schemas,
301                        )?,
302                        ReferenceOr::Reference { reference } => {
303                            let type_name = reference.split('/').next_back().unwrap_or("Unknown");
304
305                            if let Some(ReferenceOr::Item(schema)) = all_schemas.get(type_name) {
306                                if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
307                                    (to_pascal_case(type_name), "oneOf".to_string())
308                                } else {
309                                    extract_type_and_format(
310                                        &ReferenceOr::Reference {
311                                            reference: reference.clone(),
312                                        },
313                                        all_schemas,
314                                    )?
315                                }
316                            } else {
317                                extract_type_and_format(
318                                    &ReferenceOr::Reference {
319                                        reference: reference.clone(),
320                                    },
321                                    all_schemas,
322                                )?
323                            }
324                        }
325                    };
326                    Ok((format!("Vec<{inner_type}>"), format))
327                } else {
328                    Ok(("Vec<serde_json::Value>".to_string(), "array".to_string()))
329                }
330            }
331            SchemaKind::Type(Type::Object(_obj)) => {
332                Ok(("serde_json::Value".to_string(), "object".to_string()))
333            }
334            _ => Ok(("serde_json::Value".to_string(), "unknown".to_string())),
335        },
336    }
337}
338
339/// Extracts field information including type, format, and nullable flag from OpenAPI schema
340fn extract_field_info(
341    field_name: &str,
342    schema: &ReferenceOr<Schema>,
343    all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
344) -> Result<(FieldInfo, Option<ModelType>)> {
345    let (mut field_type, format) = extract_type_and_format(schema, all_schemas)?;
346
347    let (is_nullable, en) = match schema {
348        ReferenceOr::Reference { .. } => (false, None),
349
350        ReferenceOr::Item(schema) => {
351            let is_nullable = schema.schema_data.nullable;
352
353            let maybe_enum = match &schema.schema_kind {
354                SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
355                    let variants: Vec<String> =
356                        s.enumeration.iter().filter_map(|v| v.clone()).collect();
357                    field_type = to_pascal_case(field_name);
358                    Some(ModelType::Enum(EnumModel {
359                        name: to_pascal_case(field_name),
360                        variants,
361                        description: schema.schema_data.description.clone(),
362                    }))
363                }
364                SchemaKind::Type(Type::Object(_)) => {
365                    field_type = "serde_json::Value".to_string();
366                    None
367                }
368                _ => None,
369            };
370            (is_nullable, maybe_enum)
371        }
372    };
373
374    Ok((
375        FieldInfo {
376            field_type,
377            format,
378            is_nullable,
379        },
380        en,
381    ))
382}
383
384fn resolve_all_of_fields(
385    _name: &str,
386    all_of: &[ReferenceOr<Schema>],
387    all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
388) -> Result<(Vec<Field>, Vec<ModelType>)> {
389    let mut all_fields = Vec::new();
390    let mut models = Vec::new();
391
392    for schema_ref in all_of {
393        match schema_ref {
394            ReferenceOr::Reference { reference } => {
395                if let Some(schema_name) = reference.strip_prefix("#/components/schemas/") {
396                    if let Some(referenced_schema) = all_schemas.get(schema_name) {
397                        let (fields, inline_models) =
398                            extract_fields_from_schema(referenced_schema, all_schemas)?;
399                        all_fields.extend(fields);
400                        models.extend(inline_models);
401                    }
402                }
403            }
404            ReferenceOr::Item(_schema) => {
405                let (fields, inline_models) = extract_fields_from_schema(schema_ref, all_schemas)?;
406                all_fields.extend(fields);
407                models.extend(inline_models);
408            }
409        }
410    }
411    Ok((all_fields, models))
412}
413
414fn resolve_union_variants(
415    name: &str,
416    schemas: &[ReferenceOr<Schema>],
417    all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
418) -> Result<(Vec<UnionVariant>, Vec<ModelType>)> {
419    use std::collections::BTreeSet;
420
421    let mut variants = Vec::new();
422    let mut models = Vec::new();
423    let mut enum_values: BTreeSet<String> = BTreeSet::new();
424    let mut is_all_simple_enum = true;
425
426    for schema_ref in schemas {
427        let resolved = match schema_ref {
428            ReferenceOr::Reference { reference } => reference
429                .strip_prefix("#/components/schemas/")
430                .and_then(|n| all_schemas.get(n)),
431            ReferenceOr::Item(_) => Some(schema_ref),
432        };
433
434        let Some(resolved_schema) = resolved else {
435            is_all_simple_enum = false;
436            continue;
437        };
438
439        match resolved_schema {
440            ReferenceOr::Item(schema) => match &schema.schema_kind {
441                SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
442                    enum_values.extend(s.enumeration.iter().filter_map(|v| v.as_ref().cloned()));
443                }
444                SchemaKind::Type(Type::Integer(n)) if !n.enumeration.is_empty() => {
445                    enum_values.extend(
446                        n.enumeration
447                            .iter()
448                            .filter_map(|v| v.map(|num| format!("Value{num}"))),
449                    );
450                }
451
452                _ => is_all_simple_enum = false,
453            },
454            ReferenceOr::Reference { reference } => {
455                if let Some(n) = reference.strip_prefix("#/components/schemas/") {
456                    if let Some(ReferenceOr::Item(inner)) = all_schemas.get(n) {
457                        if let SchemaKind::Type(Type::String(s)) = &inner.schema_kind {
458                            let values: Vec<String> = s
459                                .enumeration
460                                .iter()
461                                .filter_map(|v| v.as_ref().cloned())
462                                .collect();
463                            enum_values.extend(values);
464                        } else {
465                            is_all_simple_enum = false;
466                        }
467                    }
468                }
469            }
470        }
471    }
472    if is_all_simple_enum && !enum_values.is_empty() {
473        let enum_name = to_pascal_case(name);
474        let enum_model = ModelType::Enum(EnumModel {
475            name: enum_name.clone(),
476            variants: enum_values.iter().map(|v| to_pascal_case(v)).collect(),
477            description: None,
478        });
479
480        return Ok((vec![], vec![enum_model]));
481    }
482
483    // fallback for usual union-schemas
484    for (index, schema_ref) in schemas.iter().enumerate() {
485        match schema_ref {
486            ReferenceOr::Reference { reference } => {
487                if let Some(schema_name) = reference.strip_prefix("#/components/schemas/") {
488                    if let Some(referenced_schema) = all_schemas.get(schema_name) {
489                        if let ReferenceOr::Item(schema) = referenced_schema {
490                            if matches!(schema.schema_kind, SchemaKind::OneOf { .. }) {
491                                variants.push(UnionVariant {
492                                    name: to_pascal_case(schema_name),
493                                    fields: vec![],
494                                });
495                            } else {
496                                let (fields, inline_models) =
497                                    extract_fields_from_schema(referenced_schema, all_schemas)?;
498                                variants.push(UnionVariant {
499                                    name: to_pascal_case(schema_name),
500                                    fields,
501                                });
502                                models.extend(inline_models);
503                            }
504                        }
505                    }
506                }
507            }
508            ReferenceOr::Item(_) => {
509                let (fields, inline_models) = extract_fields_from_schema(schema_ref, all_schemas)?;
510                let variant_name = format!("Variant{index}");
511                variants.push(UnionVariant {
512                    name: variant_name,
513                    fields,
514                });
515                models.extend(inline_models);
516            }
517        }
518    }
519
520    Ok((variants, models))
521}
522
523fn extract_fields_from_schema(
524    schema_ref: &ReferenceOr<Schema>,
525    _all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
526) -> Result<(Vec<Field>, Vec<ModelType>)> {
527    let mut fields = Vec::new();
528    let mut inline_models = Vec::new();
529
530    match schema_ref {
531        ReferenceOr::Reference { .. } => Ok((fields, inline_models)),
532        ReferenceOr::Item(schema) => {
533            match &schema.schema_kind {
534                SchemaKind::Type(Type::Object(obj)) => {
535                    for (field_name, field_schema) in &obj.properties {
536                        let (field_info, inline_model) = match field_schema {
537                            ReferenceOr::Item(boxed_schema) => extract_field_info(
538                                field_name,
539                                &ReferenceOr::Item((**boxed_schema).clone()),
540                                _all_schemas,
541                            )?,
542                            ReferenceOr::Reference { reference } => extract_field_info(
543                                field_name,
544                                &ReferenceOr::Reference {
545                                    reference: reference.clone(),
546                                },
547                                _all_schemas,
548                            )?,
549                        };
550
551                        let is_nullable = field_info.is_nullable
552                            || field_name == "value"
553                            || field_name == "default_value";
554
555                        let field_type = field_info.field_type.clone();
556
557                        let is_required = obj.required.contains(field_name);
558                        fields.push(Field {
559                            name: field_name.clone(),
560                            field_type,
561                            format: field_info.format,
562                            is_required,
563                            is_nullable,
564                        });
565                        if let Some(inline_model) = inline_model {
566                            match &inline_model {
567                                ModelType::Struct(m) if m.fields.is_empty() => {}
568                                _ => inline_models.push(inline_model),
569                            }
570                        }
571                    }
572                }
573                SchemaKind::Type(Type::String(s)) if !s.enumeration.is_empty() => {
574                    let name = schema
575                        .schema_data
576                        .title
577                        .clone()
578                        .unwrap_or_else(|| "AnonymousStringEnum".to_string());
579
580                    let enum_model = ModelType::Enum(EnumModel {
581                        name,
582                        variants: s
583                            .enumeration
584                            .iter()
585                            .filter_map(|v| v.as_ref().map(|s| to_pascal_case(s)))
586                            .collect(),
587                        description: schema.schema_data.description.clone(),
588                    });
589
590                    inline_models.push(enum_model);
591                }
592                SchemaKind::Type(Type::Integer(n)) if !n.enumeration.is_empty() => {
593                    let name = schema
594                        .schema_data
595                        .title
596                        .clone()
597                        .unwrap_or_else(|| "AnonymousIntEnum".to_string());
598
599                    let enum_model = ModelType::Enum(EnumModel {
600                        name,
601                        variants: n
602                            .enumeration
603                            .iter()
604                            .filter_map(|v| v.map(|num| format!("Value{num}")))
605                            .collect(),
606                        description: schema.schema_data.description.clone(),
607                    });
608
609                    inline_models.push(enum_model);
610                }
611
612                _ => {}
613            }
614
615            Ok((fields, inline_models))
616        }
617    }
618}