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 input
25 .split(&['-', '_'][..]) .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 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 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 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 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 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 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 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 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
259fn 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}