openapi_model_generator/
parser.rs

1use crate::{
2    models::{
3        CompositionModel, Field, Model, ModelType, RequestModel, ResponseModel, UnionModel,
4        UnionType, UnionVariant,
5    },
6    Result,
7};
8use indexmap::IndexMap;
9use openapiv3::{
10    OpenAPI, ReferenceOr, Schema, SchemaKind, StringFormat, Type, VariantOrUnknownOrEmpty,
11};
12
13/// Information about a field extracted from OpenAPI schema
14#[derive(Debug)]
15struct FieldInfo {
16    field_type: String,
17    format: String,
18    is_nullable: bool,
19}
20
21/// Converts camelCase to PascalCase
22/// Example: "createRole" -> "CreateRole", "listRoles" -> "ListRoles", "listRoles-Input" -> "ListRolesInput"
23fn to_pascal_case(input: &str) -> String {
24    input
25        .split(&['-', '_'][..]) // split on '-' or '_'
26        .filter(|s| !s.is_empty())
27        .map(|s| {
28            let mut chars = s.chars();
29            match chars.next() {
30                Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
31                None => String::new(),
32            }
33        })
34        .collect::<String>()
35}
36
37pub fn parse_openapi(
38    openapi: &OpenAPI,
39) -> Result<(Vec<ModelType>, Vec<RequestModel>, Vec<ResponseModel>)> {
40    let mut models = Vec::new();
41    let mut requests = Vec::new();
42    let mut responses = Vec::new();
43
44    // Parse components/schemas
45    if let Some(components) = &openapi.components {
46        for (name, schema) in &components.schemas {
47            if let Some(model_type) = parse_schema_to_model_type(name, schema, &components.schemas)?
48            {
49                models.push(model_type);
50            }
51        }
52    }
53
54    // Parse paths
55    for (_path, path_item) in openapi.paths.iter() {
56        let path_item = match path_item {
57            ReferenceOr::Item(item) => item,
58            ReferenceOr::Reference { .. } => continue,
59        };
60
61        if let Some(op) = &path_item.get {
62            process_operation(op, &mut requests, &mut responses)?;
63        }
64        if let Some(op) = &path_item.post {
65            process_operation(op, &mut requests, &mut responses)?;
66        }
67        if let Some(op) = &path_item.put {
68            process_operation(op, &mut requests, &mut responses)?;
69        }
70        if let Some(op) = &path_item.delete {
71            process_operation(op, &mut requests, &mut responses)?;
72        }
73        if let Some(op) = &path_item.patch {
74            process_operation(op, &mut requests, &mut responses)?;
75        }
76    }
77
78    Ok((models, requests, responses))
79}
80
81fn process_operation(
82    operation: &openapiv3::Operation,
83    requests: &mut Vec<RequestModel>,
84    responses: &mut Vec<ResponseModel>,
85) -> Result<()> {
86    // Parse request body
87    if let Some(ReferenceOr::Item(request_body)) = &operation.request_body {
88        for (content_type, media_type) in &request_body.content {
89            if let Some(schema) = &media_type.schema {
90                let request = RequestModel {
91                    name: format!(
92                        "{}Request",
93                        to_pascal_case(operation.operation_id.as_deref().unwrap_or("Unknown"))
94                    ),
95                    content_type: content_type.clone(),
96                    schema: extract_type_and_format(schema)?.0,
97                    is_required: request_body.required,
98                };
99                requests.push(request);
100            }
101        }
102    }
103
104    // Parse responses
105    for (status, response_ref) in operation.responses.responses.iter() {
106        if let ReferenceOr::Item(response) = response_ref {
107            for (content_type, media_type) in &response.content {
108                if let Some(schema) = &media_type.schema {
109                    let response = ResponseModel {
110                        name: format!(
111                            "{}Response",
112                            to_pascal_case(operation.operation_id.as_deref().unwrap_or("Unknown"))
113                        ),
114                        status_code: status.to_string(),
115                        content_type: content_type.clone(),
116                        schema: extract_type_and_format(schema)?.0,
117                        description: Some(response.description.clone()),
118                    };
119                    responses.push(response);
120                }
121            }
122        }
123    }
124
125    Ok(())
126}
127
128fn parse_schema_to_model_type(
129    name: &str,
130    schema: &ReferenceOr<Schema>,
131    all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
132) -> Result<Option<ModelType>> {
133    match schema {
134        ReferenceOr::Reference { .. } => Ok(None),
135        ReferenceOr::Item(schema) => {
136            match &schema.schema_kind {
137                // regular objects
138                SchemaKind::Type(Type::Object(obj)) => {
139                    let mut fields = Vec::new();
140                    for (field_name, field_schema) in &obj.properties {
141                        let field_info = match field_schema {
142                            ReferenceOr::Item(boxed_schema) => {
143                                extract_field_info(&ReferenceOr::Item((**boxed_schema).clone()))?
144                            }
145                            ReferenceOr::Reference { reference } => {
146                                extract_field_info(&ReferenceOr::Reference {
147                                    reference: reference.clone(),
148                                })?
149                            }
150                        };
151
152                        let is_required = obj.required.contains(field_name);
153                        fields.push(Field {
154                            name: field_name.clone(),
155                            field_type: field_info.field_type,
156                            format: field_info.format,
157                            is_required,
158                            is_nullable: field_info.is_nullable,
159                        });
160                    }
161                    Ok(Some(ModelType::Struct(Model {
162                        name: to_pascal_case(name),
163                        fields,
164                    })))
165                }
166
167                // allOf
168                SchemaKind::AllOf { all_of } => {
169                    let all_fields = resolve_all_of_fields(name, all_of, all_schemas)?;
170                    Ok(Some(ModelType::Composition(CompositionModel {
171                        name: to_pascal_case(name),
172                        all_fields,
173                    })))
174                }
175
176                // oneOf
177                SchemaKind::OneOf { one_of } => {
178                    let variants = resolve_union_variants(one_of, all_schemas)?;
179                    Ok(Some(ModelType::Union(UnionModel {
180                        name: to_pascal_case(name),
181                        variants,
182                        union_type: UnionType::OneOf,
183                    })))
184                }
185
186                // anyOf
187                SchemaKind::AnyOf { any_of } => {
188                    let variants = resolve_union_variants(any_of, all_schemas)?;
189                    Ok(Some(ModelType::Union(UnionModel {
190                        name: to_pascal_case(name),
191                        variants,
192                        union_type: UnionType::AnyOf,
193                    })))
194                }
195
196                _ => Ok(None),
197            }
198        }
199    }
200}
201
202fn extract_type_and_format(schema: &ReferenceOr<Schema>) -> Result<(String, String)> {
203    match schema {
204        ReferenceOr::Reference { reference } => {
205            let type_name = reference.split('/').next_back().unwrap_or("Unknown");
206            Ok((to_pascal_case(type_name), "reference".to_string()))
207        }
208        ReferenceOr::Item(schema) => match &schema.schema_kind {
209            SchemaKind::Type(Type::String(string_type)) => match &string_type.format {
210                VariantOrUnknownOrEmpty::Item(fmt) => match fmt {
211                    StringFormat::DateTime => {
212                        Ok(("DateTime<Utc>".to_string(), "date-time".to_string()))
213                    }
214                    StringFormat::Date => Ok(("NaiveDate".to_string(), "date".to_string())),
215                    _ => Ok(("String".to_string(), format!("{fmt:?}"))),
216                },
217                VariantOrUnknownOrEmpty::Unknown(unknown_format) => {
218                    if unknown_format.to_lowercase() == "uuid" {
219                        Ok(("Uuid".to_string(), "uuid".to_string()))
220                    } else {
221                        Ok(("String".to_string(), unknown_format.clone()))
222                    }
223                }
224                _ => Ok(("String".to_string(), "string".to_string())),
225            },
226            SchemaKind::Type(Type::Integer(_)) => Ok(("i64".to_string(), "integer".to_string())),
227            SchemaKind::Type(Type::Number(_)) => Ok(("f64".to_string(), "number".to_string())),
228            SchemaKind::Type(Type::Boolean(_)) => Ok(("bool".to_string(), "boolean".to_string())),
229            SchemaKind::Type(Type::Array(arr)) => {
230                if let Some(items) = &arr.items {
231                    let items_ref: &ReferenceOr<Box<Schema>> = items;
232                    let (inner_type, _) = match items_ref {
233                        ReferenceOr::Item(boxed_schema) => {
234                            extract_type_and_format(&ReferenceOr::Item((**boxed_schema).clone()))?
235                        }
236                        ReferenceOr::Reference { reference } => {
237                            extract_type_and_format(&ReferenceOr::Reference {
238                                reference: reference.clone(),
239                            })?
240                        }
241                    };
242                    Ok((format!("Vec<{inner_type}>"), "array".to_string()))
243                } else {
244                    Ok(("Vec<serde_json::Value>".to_string(), "array".to_string()))
245                }
246            }
247            SchemaKind::Type(Type::Object(obj)) => {
248                if obj.properties.is_empty() {
249                    Ok(("()".to_string(), "object".to_string()))
250                } else {
251                    Ok(("serde_json::Value".to_string(), "object".to_string()))
252                }
253            }
254            _ => Ok(("serde_json::Value".to_string(), "unknown".to_string())),
255        },
256    }
257}
258
259/// Extracts field information including type, format, and nullable flag from OpenAPI schema
260fn extract_field_info(schema: &ReferenceOr<Schema>) -> Result<FieldInfo> {
261    let (field_type, format) = extract_type_and_format(schema)?;
262
263    let is_nullable = match schema {
264        ReferenceOr::Reference { .. } => false,
265        ReferenceOr::Item(schema) => schema.schema_data.nullable,
266    };
267
268    Ok(FieldInfo {
269        field_type,
270        format,
271        is_nullable,
272    })
273}
274
275fn resolve_all_of_fields(
276    _name: &str,
277    all_of: &[ReferenceOr<Schema>],
278    all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
279) -> Result<Vec<Field>> {
280    let mut all_fields = Vec::new();
281
282    for schema_ref in all_of {
283        match schema_ref {
284            ReferenceOr::Reference { reference } => {
285                if let Some(schema_name) = reference.strip_prefix("#/components/schemas/") {
286                    if let Some(referenced_schema) = all_schemas.get(schema_name) {
287                        let fields = extract_fields_from_schema(referenced_schema, all_schemas)?;
288                        all_fields.extend(fields);
289                    }
290                }
291            }
292            ReferenceOr::Item(_schema) => {
293                let fields = extract_fields_from_schema(schema_ref, all_schemas)?;
294                all_fields.extend(fields);
295            }
296        }
297    }
298
299    Ok(all_fields)
300}
301
302fn resolve_union_variants(
303    schemas: &[ReferenceOr<Schema>],
304    all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
305) -> Result<Vec<UnionVariant>> {
306    let mut variants = Vec::new();
307
308    for (index, schema_ref) in schemas.iter().enumerate() {
309        match schema_ref {
310            ReferenceOr::Reference { reference } => {
311                if let Some(schema_name) = reference.strip_prefix("#/components/schemas/") {
312                    if let Some(referenced_schema) = all_schemas.get(schema_name) {
313                        let fields = extract_fields_from_schema(referenced_schema, all_schemas)?;
314                        variants.push(UnionVariant {
315                            name: to_pascal_case(schema_name),
316                            fields,
317                        });
318                    }
319                }
320            }
321            ReferenceOr::Item(_schema) => {
322                let fields = extract_fields_from_schema(schema_ref, all_schemas)?;
323                let variant_name = format!("Variant{index}");
324                variants.push(UnionVariant {
325                    name: variant_name,
326                    fields,
327                });
328            }
329        }
330    }
331
332    Ok(variants)
333}
334
335fn extract_fields_from_schema(
336    schema_ref: &ReferenceOr<Schema>,
337    _all_schemas: &IndexMap<String, ReferenceOr<Schema>>,
338) -> Result<Vec<Field>> {
339    let mut fields = Vec::new();
340
341    match schema_ref {
342        ReferenceOr::Reference { .. } => Ok(fields),
343        ReferenceOr::Item(schema) => {
344            if let SchemaKind::Type(Type::Object(obj)) = &schema.schema_kind {
345                for (field_name, field_schema) in &obj.properties {
346                    let field_info = match field_schema {
347                        ReferenceOr::Item(boxed_schema) => {
348                            extract_field_info(&ReferenceOr::Item((**boxed_schema).clone()))?
349                        }
350                        ReferenceOr::Reference { reference } => {
351                            extract_field_info(&ReferenceOr::Reference {
352                                reference: reference.clone(),
353                            })?
354                        }
355                    };
356
357                    let is_required = obj.required.contains(field_name);
358                    fields.push(Field {
359                        name: field_name.clone(),
360                        field_type: field_info.field_type,
361                        format: field_info.format,
362                        is_required,
363                        is_nullable: field_info.is_nullable,
364                    });
365                }
366            }
367            Ok(fields)
368        }
369    }
370}