1use heck::{ToSnakeCase, ToUpperCamelCase};
2use indexmap::IndexMap;
3use proc_macro2::TokenStream;
4use quote::{format_ident, quote, ToTokens};
5use syn::Ident;
6
7use crate::openapi::r#type::OpenApiType;
8
9use super::{r#enum::Enum, ResolvedSchema};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum PrimitiveType {
13 Bool,
14 I32,
15 I64,
16 String,
17 Float,
18 DateTime,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum PropertyType {
23 Primitive(PrimitiveType),
24 Ref(String),
25 Enum(Enum),
26 Nested(Box<Object>),
27 Array(Box<PropertyType>),
28}
29
30impl PropertyType {
31 pub fn codegen(
32 &self,
33 namespace: &mut ObjectNamespace,
34 resolved: &ResolvedSchema,
35 ) -> Option<TokenStream> {
36 match self {
37 Self::Primitive(PrimitiveType::Bool) => Some(format_ident!("bool").into_token_stream()),
38 Self::Primitive(PrimitiveType::I32) => Some(format_ident!("i32").into_token_stream()),
39 Self::Primitive(PrimitiveType::I64) => Some(format_ident!("i64").into_token_stream()),
40 Self::Primitive(PrimitiveType::String) => {
41 Some(format_ident!("String").into_token_stream())
42 }
43 Self::Primitive(PrimitiveType::DateTime) => {
44 Some(quote! { chrono::DateTime<chrono::Utc> })
45 }
46 Self::Primitive(PrimitiveType::Float) => Some(format_ident!("f64").into_token_stream()),
47 Self::Ref(path) => {
48 let name = path.strip_prefix("#/components/schemas/")?;
49 let name = format_ident!("{name}");
50
51 Some(quote! { crate::models::#name })
52 }
53 Self::Enum(r#enum) => {
54 let code = r#enum.codegen(resolved)?;
55 namespace.push_element(code);
56
57 let ns = namespace.get_ident();
58 let name = format_ident!("{}", r#enum.name);
59
60 Some(quote! {
61 #ns::#name
62 })
63 }
64 Self::Array(array) => {
65 let inner_ty = array.codegen(namespace, resolved)?;
66
67 Some(quote! {
68 Vec<#inner_ty>
69 })
70 }
71 Self::Nested(nested) => {
72 let code = nested.codegen(resolved)?;
73 namespace.push_element(code);
74
75 let ns = namespace.get_ident();
76 let name = format_ident!("{}", nested.name);
77
78 Some(quote! {
79 #ns::#name
80 })
81 }
82 }
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct Property {
88 pub name: String,
89 pub description: Option<String>,
90 pub required: bool,
91 pub nullable: bool,
92 pub r#type: PropertyType,
93 pub deprecated: bool,
94}
95
96impl Property {
97 pub fn from_schema(
98 name: &str,
99 required: bool,
100 schema: &OpenApiType,
101 schemas: &IndexMap<&str, OpenApiType>,
102 ) -> Option<Self> {
103 let name = name.to_owned();
104 let description = schema.description.as_deref().map(ToOwned::to_owned);
105
106 match schema {
107 OpenApiType {
108 r#enum: Some(_), ..
109 } => Some(Self {
110 r#type: PropertyType::Enum(Enum::from_schema(
111 &name.clone().to_upper_camel_case(),
112 schema,
113 )?),
114 name,
115 description,
116 required,
117 deprecated: schema.deprecated,
118 nullable: false,
119 }),
120 OpenApiType {
121 one_of: Some(types),
122 ..
123 } => match types.as_slice() {
124 [left, OpenApiType {
125 r#type: Some("null"),
126 ..
127 }] => {
128 let mut inner = Self::from_schema(&name, required, left, schemas)?;
129 inner.nullable = true;
130 Some(inner)
131 }
132 [left @ .., OpenApiType {
133 r#type: Some("null"),
134 ..
135 }] => {
136 let rest = OpenApiType {
137 one_of: Some(left.to_owned()),
138 ..schema.clone()
139 };
140 let mut inner = Self::from_schema(&name, required, &rest, schemas)?;
141 inner.nullable = true;
142 Some(inner)
143 }
144 cases => {
145 let r#enum = Enum::from_one_of(&name.to_upper_camel_case(), cases)?;
146 Some(Self {
147 name,
148 description,
149 required,
150 nullable: false,
151 deprecated: schema.deprecated,
152 r#type: PropertyType::Enum(r#enum),
153 })
154 }
155 },
156 OpenApiType {
157 all_of: Some(types),
158 ..
159 } => {
160 let composite = Object::from_all_of(&name.to_upper_camel_case(), types, schemas)?;
161 Some(Self {
162 name,
163 description,
164 required,
165 nullable: false,
166 deprecated: schema.deprecated,
167 r#type: PropertyType::Nested(Box::new(composite)),
168 })
169 }
170 OpenApiType {
171 r#type: Some("object"),
172 ..
173 } => Some(Self {
174 r#type: PropertyType::Nested(Box::new(Object::from_schema_object(
175 &name.clone().to_upper_camel_case(),
176 schema,
177 schemas,
178 )?)),
179 name,
180 description,
181 required,
182 deprecated: schema.deprecated,
183 nullable: false,
184 }),
185 OpenApiType {
186 ref_path: Some(path),
187 ..
188 } => Some(Self {
189 name,
190 description,
191 r#type: PropertyType::Ref((*path).to_owned()),
192 required,
193 deprecated: schema.deprecated,
194 nullable: false,
195 }),
196 OpenApiType {
197 r#type: Some("array"),
198 items: Some(items),
199 ..
200 } => {
201 let inner = Self::from_schema(&name, required, items, schemas)?;
202
203 Some(Self {
204 name,
205 description,
206 required,
207 nullable: false,
208 deprecated: schema.deprecated,
209 r#type: PropertyType::Array(Box::new(inner.r#type)),
210 })
211 }
212 OpenApiType {
213 r#type: Some(_), ..
214 } => {
215 let prim = match (schema.r#type, schema.format) {
216 (Some("integer"), Some("int32")) => PrimitiveType::I32,
217 (Some("integer"), Some("int64")) => PrimitiveType::I64,
218 (Some("number"), _) | (_, Some("float")) => {
219 PrimitiveType::Float
220 }
221 (Some("string"), None) => PrimitiveType::String,
222 (Some("string"), Some("date")) => PrimitiveType::DateTime,
223 (Some("boolean"), None) => PrimitiveType::Bool,
224 _ => return None,
225 };
226
227 Some(Self {
228 name,
229 description,
230 required,
231 nullable: false,
232 deprecated: schema.deprecated,
233 r#type: PropertyType::Primitive(prim),
234 })
235 }
236 _ => None,
237 }
238 }
239
240 pub fn codegen(
241 &self,
242 namespace: &mut ObjectNamespace,
243 resolved: &ResolvedSchema,
244 ) -> Option<TokenStream> {
245 let desc = self.description.as_ref().map(|d| quote! { #[doc = #d]});
246
247 let name = &self.name;
248 let (name, serde_attr) = match name.as_str() {
249 "type" => (format_ident!("r#type"), None),
250 name if name != name.to_snake_case() => (
251 format_ident!("{}", name.to_snake_case()),
252 Some(quote! { #[serde(rename = #name)]}),
253 ),
254 _ => (format_ident!("{name}"), None),
255 };
256
257 let ty_inner = self.r#type.codegen(namespace, resolved)?;
258
259 let ty = if !self.required || self.nullable {
260 quote! { Option<#ty_inner> }
261 } else {
262 ty_inner
263 };
264
265 let deprecated = self.deprecated.then(|| {
266 let note = self.description.as_ref().map(|d| quote! { note = #d });
267
268 quote! {
269 #[deprecated(#note)]
270 }
271 });
272
273 Some(quote! {
274 #desc
275 #deprecated
276 #serde_attr
277 pub #name: #ty
278 })
279 }
280}
281
282#[derive(Debug, Clone, PartialEq, Eq, Default)]
283pub struct Object {
284 pub name: String,
285 pub description: Option<String>,
286 pub properties: Vec<Property>,
287}
288
289impl Object {
290 pub fn from_schema_object(
291 name: &str,
292 schema: &OpenApiType,
293 schemas: &IndexMap<&str, OpenApiType>,
294 ) -> Option<Self> {
295 let mut result = Object {
296 name: name.to_owned(),
297 description: schema.description.as_deref().map(ToOwned::to_owned),
298 ..Default::default()
299 };
300
301 let Some(props) = &schema.properties else {
302 return None;
303 };
304
305 let required = schema.required.clone().unwrap_or_default();
306
307 for (prop_name, prop) in props {
308 if ["itemDetails", "sci-fi", "non-attackers", "co-leader_id"].contains(prop_name) {
310 continue;
311 }
312
313 if *prop_name == "value" && name == "TornHof" {
316 continue;
317 }
318
319 result.properties.push(Property::from_schema(
320 prop_name,
321 required.contains(prop_name),
322 prop,
323 schemas,
324 )?);
325 }
326
327 Some(result)
328 }
329
330 pub fn from_all_of(
331 name: &str,
332 types: &[OpenApiType],
333 schemas: &IndexMap<&str, OpenApiType>,
334 ) -> Option<Self> {
335 let mut result = Self {
336 name: name.to_owned(),
337 ..Default::default()
338 };
339
340 for r#type in types {
341 let r#type = if let OpenApiType {
342 ref_path: Some(path),
343 ..
344 } = r#type
345 {
346 let name = path.strip_prefix("#/components/schemas/")?;
347 schemas.get(name)?
348 } else {
349 r#type
350 };
351 let obj = Self::from_schema_object(name, r#type, schemas)?;
352
353 result.description = result.description.or(obj.description);
354 result.properties.extend(obj.properties);
355 }
356
357 Some(result)
358 }
359
360 pub fn codegen(&self, resolved: &ResolvedSchema) -> Option<TokenStream> {
361 let doc = self.description.as_ref().map(|d| {
362 quote! {
363 #[doc = #d]
364 }
365 });
366
367 let mut namespace = ObjectNamespace {
368 object: self,
369 ident: None,
370 elements: Vec::default(),
371 };
372
373 let mut props = Vec::with_capacity(self.properties.len());
374 for prop in &self.properties {
375 props.push(prop.codegen(&mut namespace, resolved)?);
376 }
377
378 let name = format_ident!("{}", self.name);
379 let ns = namespace.codegen();
380
381 Some(quote! {
382 #ns
383
384 #doc
385 #[derive(Debug, Clone, PartialEq, serde::Deserialize)]
386 pub struct #name {
387 #(#props),*
388 }
389 })
390 }
391}
392
393pub struct ObjectNamespace<'o> {
394 object: &'o Object,
395 ident: Option<Ident>,
396 elements: Vec<TokenStream>,
397}
398
399impl ObjectNamespace<'_> {
400 pub fn get_ident(&mut self) -> Ident {
401 self.ident
402 .get_or_insert_with(|| {
403 let name = self.object.name.to_snake_case();
404 format_ident!("{name}")
405 })
406 .clone()
407 }
408
409 pub fn push_element(&mut self, el: TokenStream) {
410 self.elements.push(el);
411 }
412
413 pub fn codegen(mut self) -> Option<TokenStream> {
414 if self.elements.is_empty() {
415 None
416 } else {
417 let ident = self.get_ident();
418 let elements = self.elements;
419 Some(quote! {
420 pub mod #ident {
421 #(#elements)*
422 }
423 })
424 }
425 }
426}
427
428#[cfg(test)]
429mod test {
430 use super::*;
431
432 use crate::openapi::schema::test::get_schema;
433
434 #[test]
435 fn resolve_objects() {
436 let schema = get_schema();
437
438 let mut objects = 0;
439 let mut unresolved = vec![];
440
441 for (name, desc) in &schema.components.schemas {
442 if desc.r#type == Some("object") {
443 objects += 1;
444 if Object::from_schema_object(name, desc, &schema.components.schemas).is_none() {
445 unresolved.push(name);
446 }
447 }
448 }
449
450 if !unresolved.is_empty() {
451 panic!(
452 "Failed to resolve {}/{} objects. Could not resolve [{}]",
453 unresolved.len(),
454 objects,
455 unresolved
456 .into_iter()
457 .map(|u| format!("`{u}`"))
458 .collect::<Vec<_>>()
459 .join(", ")
460 )
461 }
462 }
463}