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
13fn 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 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 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 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 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 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 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 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 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}