torn_api_codegen/model/
object.rs

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