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#[derive(Debug)]
15struct FieldInfo {
16 field_type: String,
17 format: String,
18 is_nullable: bool,
19}
20
21fn 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 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 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 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 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 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 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 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 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
265fn 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}