torn_api_codegen/model/
object.rs

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") */ _) | (_, 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            // HACK: This will cause a duplicate key otherwise
309            if ["itemDetails", "sci-fi", "non-attackers", "co-leader_id"].contains(prop_name) {
310                continue;
311            }
312
313            // TODO: implement custom enum for this (depends on overrides being added)
314            // Maybe this is an issue with the schema instead?
315            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}