torn_api_codegen/model/
enum.rs

1use heck::ToUpperCamelCase;
2use proc_macro2::TokenStream;
3use quote::{format_ident, quote};
4
5use crate::openapi::{
6    parameter::OpenApiParameterSchema,
7    r#type::{OpenApiType, OpenApiVariants},
8};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum EnumRepr {
12    U8,
13    U32,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum EnumVariantTupleValue {
18    Ref(String),
19}
20
21impl EnumVariantTupleValue {
22    pub fn from_schema(schema: &OpenApiType) -> Option<Self> {
23        if let OpenApiType {
24            ref_path: Some(path),
25            ..
26        } = schema
27        {
28            Some(Self::Ref((*path).to_owned()))
29        } else {
30            None
31        }
32    }
33
34    pub fn name(&self) -> Option<&str> {
35        let Self::Ref(path) = self;
36
37        path.strip_prefix("#/components/schemas/")
38    }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum EnumVariantValue {
43    Repr(u32),
44    String { rename: Option<String> },
45    Tuple(Vec<EnumVariantTupleValue>),
46}
47
48impl Default for EnumVariantValue {
49    fn default() -> Self {
50        Self::String { rename: None }
51    }
52}
53
54impl EnumVariantValue {
55    pub fn codegen_display(&self, name: &str) -> Option<TokenStream> {
56        match self {
57            Self::Repr(i) => Some(quote! { write!(f, "{}", #i) }),
58            Self::String { rename } => {
59                let name = rename.as_deref().unwrap_or(name);
60                Some(quote! { write!(f, #name) })
61            }
62            _ => None,
63        }
64    }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Default)]
68pub struct EnumVariant {
69    pub name: String,
70    pub description: Option<String>,
71    pub value: EnumVariantValue,
72}
73
74impl EnumVariant {
75    pub fn codegen(&self) -> Option<TokenStream> {
76        let doc = self.description.as_ref().map(|d| {
77            quote! {
78                #[doc = #d]
79            }
80        });
81
82        let name = format_ident!("{}", self.name);
83
84        match &self.value {
85            EnumVariantValue::Repr(repr) => Some(quote! {
86                #doc
87                #name = #repr
88            }),
89            EnumVariantValue::String { rename } => {
90                let serde_attr = rename.as_ref().map(|r| {
91                    quote! {
92                        #[serde(rename = #r)]
93                    }
94                });
95
96                Some(quote! {
97                    #doc
98                    #serde_attr
99                    #name
100                })
101            }
102            EnumVariantValue::Tuple(values) => {
103                let mut val_tys = Vec::with_capacity(values.len());
104
105                for value in values {
106                    let ty_name = value.name()?;
107                    let ty_name = format_ident!("{ty_name}");
108
109                    val_tys.push(quote! {
110                        crate::models::#ty_name
111                    });
112                }
113
114                Some(quote! {
115                    #name(#(#val_tys),*)
116                })
117            }
118        }
119    }
120
121    pub fn codegen_display(&self) -> Option<TokenStream> {
122        let rhs = self.value.codegen_display(&self.name)?;
123        let name = format_ident!("{}", self.name);
124
125        Some(quote! {
126            Self::#name => #rhs
127        })
128    }
129}
130
131#[derive(Debug, Clone, PartialEq, Eq, Default)]
132pub struct Enum {
133    pub name: String,
134    pub description: Option<String>,
135    pub repr: Option<EnumRepr>,
136    pub copy: bool,
137    pub display: bool,
138    pub untagged: bool,
139    pub variants: Vec<EnumVariant>,
140}
141
142impl Enum {
143    pub fn from_schema(name: &str, schema: &OpenApiType) -> Option<Self> {
144        let mut result = Enum {
145            name: name.to_owned(),
146            description: schema.description.as_deref().map(ToOwned::to_owned),
147            copy: true,
148            ..Default::default()
149        };
150
151        match &schema.r#enum {
152            Some(OpenApiVariants::Int(int_variants)) => {
153                result.repr = Some(EnumRepr::U32);
154                result.display = true;
155                result.variants = int_variants
156                    .iter()
157                    .copied()
158                    .map(|i| EnumVariant {
159                        name: format!("Variant{i}"),
160                        value: EnumVariantValue::Repr(i as u32),
161                        ..Default::default()
162                    })
163                    .collect();
164            }
165            Some(OpenApiVariants::Str(str_variants)) => {
166                result.display = true;
167                result.variants = str_variants
168                    .iter()
169                    .copied()
170                    .map(|s| {
171                        let transformed = s.replace('&', "And").to_upper_camel_case();
172                        EnumVariant {
173                            value: EnumVariantValue::String {
174                                rename: (transformed != s).then(|| s.to_owned()),
175                            },
176                            name: transformed,
177                            ..Default::default()
178                        }
179                    })
180                    .collect();
181            }
182            None => return None,
183        }
184
185        Some(result)
186    }
187
188    pub fn from_parameter_schema(name: &str, schema: &OpenApiParameterSchema) -> Option<Self> {
189        let mut result = Self {
190            name: name.to_owned(),
191            copy: true,
192            display: true,
193            ..Default::default()
194        };
195
196        for var in schema.r#enum.as_ref()? {
197            let transformed = var.to_upper_camel_case();
198            result.variants.push(EnumVariant {
199                value: EnumVariantValue::String {
200                    rename: (transformed != *var).then(|| transformed.clone()),
201                },
202                name: transformed,
203                ..Default::default()
204            });
205        }
206
207        Some(result)
208    }
209
210    pub fn from_one_of(name: &str, schemas: &[OpenApiType]) -> Option<Self> {
211        let mut result = Self {
212            name: name.to_owned(),
213            untagged: true,
214            ..Default::default()
215        };
216
217        for schema in schemas {
218            let value = EnumVariantTupleValue::from_schema(schema)?;
219            let name = value.name()?.to_owned();
220
221            result.variants.push(EnumVariant {
222                name,
223                value: EnumVariantValue::Tuple(vec![value]),
224                ..Default::default()
225            });
226        }
227
228        Some(result)
229    }
230
231    pub fn codegen(&self) -> Option<TokenStream> {
232        let repr = self.repr.map(|r| match r {
233            EnumRepr::U8 => quote! { #[repr(u8)]},
234            EnumRepr::U32 => quote! { #[repr(u32)]},
235        });
236        let name = format_ident!("{}", self.name);
237        let desc = self.description.as_ref().map(|d| {
238            quote! {
239                #repr
240                #[doc = #d]
241            }
242        });
243
244        let mut display = Vec::with_capacity(self.variants.len());
245        let mut variants = Vec::with_capacity(self.variants.len());
246        for variant in &self.variants {
247            variants.push(variant.codegen()?);
248
249            if self.display {
250                display.push(variant.codegen_display()?);
251            }
252        }
253
254        let mut derives = vec![];
255
256        if self.copy {
257            derives.extend_from_slice(&["Copy", "Hash"]);
258        }
259
260        let derives = derives.into_iter().map(|d| format_ident!("{d}"));
261
262        let serde_attr = self.untagged.then(|| {
263            quote! {
264                #[serde(untagged)]
265            }
266        });
267
268        let display = self.display.then(|| {
269            quote! {
270                impl std::fmt::Display for #name {
271                    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
272                        match self {
273                            #(#display),*
274                        }
275                    }
276                }
277            }
278        });
279
280        Some(quote! {
281            #desc
282            #[derive(Debug, Clone, PartialEq, serde::Deserialize, #(#derives),*)]
283            #serde_attr
284            pub enum #name {
285                #(#variants),*
286            }
287            #display
288        })
289    }
290}
291
292#[cfg(test)]
293mod test {
294    use crate::openapi::schema::OpenApiSchema;
295
296    use super::*;
297
298    #[test]
299    fn codegen() {
300        let schema = OpenApiSchema::read().unwrap();
301
302        let revive_setting = schema.components.schemas.get("ReviveSetting").unwrap();
303
304        let r#enum = Enum::from_schema("ReviveSetting", revive_setting).unwrap();
305
306        let code = r#enum.codegen().unwrap();
307
308        panic!("{code}");
309    }
310}