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