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