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