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") */ _) => PrimitiveType::Float,
211                    (Some("string"), None) => PrimitiveType::String,
212                    (Some("boolean"), None) => PrimitiveType::Bool,
213                    _ => return None,
214                };
215
216                Some(Self {
217                    name,
218                    description,
219                    required,
220                    nullable: false,
221                    deprecated: schema.deprecated,
222                    r#type: PropertyType::Primitive(prim),
223                })
224            }
225            _ => None,
226        }
227    }
228
229    pub fn codegen(&self, namespace: &mut ObjectNamespace) -> Option<TokenStream> {
230        let desc = self.description.as_ref().map(|d| quote! { #[doc = #d]});
231
232        let name = &self.name;
233        let (name, serde_attr) = match name.as_str() {
234            "type" => (format_ident!("r#type"), None),
235            name if name != name.to_snake_case() => (
236                format_ident!("{}", name.to_snake_case()),
237                Some(quote! { #[serde(rename = #name)]}),
238            ),
239            _ => (format_ident!("{name}"), None),
240        };
241
242        let ty_inner = self.r#type.codegen(namespace)?;
243
244        let ty = if !self.required || self.nullable {
245            quote! { Option<#ty_inner> }
246        } else {
247            ty_inner
248        };
249
250        let deprecated = self.deprecated.then(|| {
251            let note = self.description.as_ref().map(|d| quote! { note = #d });
252
253            quote! {
254                #[deprecated(#note)]
255            }
256        });
257
258        Some(quote! {
259            #desc
260            #deprecated
261            #serde_attr
262            pub #name: #ty
263        })
264    }
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Default)]
268pub struct Object {
269    pub name: String,
270    pub description: Option<String>,
271    pub properties: Vec<Property>,
272}
273
274impl Object {
275    pub fn from_schema_object(
276        name: &str,
277        schema: &OpenApiType,
278        schemas: &IndexMap<&str, OpenApiType>,
279    ) -> Option<Self> {
280        let mut result = Object {
281            name: name.to_owned(),
282            description: schema.description.as_deref().map(ToOwned::to_owned),
283            ..Default::default()
284        };
285
286        let Some(props) = &schema.properties else {
287            return None;
288        };
289
290        let required = schema.required.clone().unwrap_or_default();
291
292        for (prop_name, prop) in props {
293            // HACK: This will cause a duplicate key otherwise
294            if ["itemDetails", "sci-fi", "non-attackers", "co-leader_id"].contains(prop_name) {
295                continue;
296            }
297
298            // TODO: implement custom enum for this (depends on overrides being added)
299            if *prop_name == "value" && name == "TornHof" {
300                continue;
301            }
302
303            result.properties.push(Property::from_schema(
304                prop_name,
305                required.contains(prop_name),
306                prop,
307                schemas,
308            )?);
309        }
310
311        Some(result)
312    }
313
314    pub fn from_all_of(
315        name: &str,
316        types: &[OpenApiType],
317        schemas: &IndexMap<&str, OpenApiType>,
318    ) -> Option<Self> {
319        let mut result = Self {
320            name: name.to_owned(),
321            ..Default::default()
322        };
323
324        for r#type in types {
325            let r#type = if let OpenApiType {
326                ref_path: Some(path),
327                ..
328            } = r#type
329            {
330                let name = path.strip_prefix("#/components/schemas/")?;
331                schemas.get(name)?
332            } else {
333                r#type
334            };
335            let obj = Self::from_schema_object(name, r#type, schemas)?;
336
337            result.description = result.description.or(obj.description);
338            result.properties.extend(obj.properties);
339        }
340
341        Some(result)
342    }
343
344    pub fn codegen(&self) -> Option<TokenStream> {
345        let doc = self.description.as_ref().map(|d| {
346            quote! {
347                #[doc = #d]
348            }
349        });
350
351        let mut namespace = ObjectNamespace {
352            object: self,
353            ident: None,
354            elements: Vec::default(),
355        };
356
357        let mut props = Vec::with_capacity(self.properties.len());
358        for prop in &self.properties {
359            props.push(prop.codegen(&mut namespace)?);
360        }
361
362        let name = format_ident!("{}", self.name);
363        let ns = namespace.codegen();
364
365        Some(quote! {
366            #ns
367
368            #doc
369            #[derive(Debug, Clone, PartialEq, serde::Deserialize)]
370            pub struct #name {
371                #(#props),*
372            }
373        })
374    }
375}
376
377pub struct ObjectNamespace<'o> {
378    object: &'o Object,
379    ident: Option<Ident>,
380    elements: Vec<TokenStream>,
381}
382
383impl ObjectNamespace<'_> {
384    pub fn get_ident(&mut self) -> Ident {
385        self.ident
386            .get_or_insert_with(|| {
387                let name = self.object.name.to_snake_case();
388                format_ident!("{name}")
389            })
390            .clone()
391    }
392
393    pub fn push_element(&mut self, el: TokenStream) {
394        self.elements.push(el);
395    }
396
397    pub fn codegen(mut self) -> Option<TokenStream> {
398        if self.elements.is_empty() {
399            None
400        } else {
401            let ident = self.get_ident();
402            let elements = self.elements;
403            Some(quote! {
404                pub mod #ident {
405                    #(#elements)*
406                }
407            })
408        }
409    }
410}
411
412#[cfg(test)]
413mod test {
414    use super::*;
415
416    use crate::openapi::schema::OpenApiSchema;
417
418    #[test]
419    fn resolve_object() {
420        let schema = OpenApiSchema::read().unwrap();
421
422        let attack = schema.components.schemas.get("FactionUpgrades").unwrap();
423
424        let resolved =
425            Object::from_schema_object("FactionUpgrades", attack, &schema.components.schemas)
426                .unwrap();
427        let _code = resolved.codegen().unwrap();
428    }
429
430    #[test]
431    fn resolve_objects() {
432        let schema = OpenApiSchema::read().unwrap();
433
434        let mut objects = 0;
435        let mut unresolved = vec![];
436
437        for (name, desc) in &schema.components.schemas {
438            if desc.r#type == Some("object") {
439                objects += 1;
440                if Object::from_schema_object(name, desc, &schema.components.schemas).is_none() {
441                    unresolved.push(name);
442                }
443            }
444        }
445
446        if !unresolved.is_empty() {
447            panic!(
448                "Failed to resolve {}/{} objects. Could not resolve [{}]",
449                unresolved.len(),
450                objects,
451                unresolved
452                    .into_iter()
453                    .map(|u| format!("`{u}`"))
454                    .collect::<Vec<_>>()
455                    .join(", ")
456            )
457        }
458    }
459}