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