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