fp_bindgen/types/
enums.rs

1use super::{
2    structs::{Field, Struct, StructOptions},
3    Type, TypeIdent,
4};
5use crate::types::format_bounds;
6use crate::{casing::Casing, docs::get_doc_lines, primitives::Primitive, types::FieldAttrs};
7use quote::ToTokens;
8use std::{convert::TryFrom, str::FromStr};
9use syn::{
10    ext::IdentExt, parenthesized, parse::Parse, parse::ParseStream, Attribute, Error, GenericParam,
11    Ident, ItemEnum, LitStr, Result, Token, TypePath,
12};
13
14#[derive(Clone, Debug, Eq, Hash, PartialEq)]
15pub struct Enum {
16    pub ident: TypeIdent,
17    pub variants: Vec<Variant>,
18    pub doc_lines: Vec<String>,
19    pub options: EnumOptions,
20}
21
22pub(crate) fn parse_enum_item(item: ItemEnum) -> Enum {
23    let ident = TypeIdent {
24        name: item.ident.to_string(),
25        generic_args: item
26            .generics
27            .params
28            .iter()
29            .filter_map(|param| match param {
30                GenericParam::Type(ty) => {
31                    Some((TypeIdent::from(ty.ident.to_string()), format_bounds(ty)))
32                }
33                _ => None,
34            })
35            .collect(),
36        ..Default::default()
37    };
38    let options = EnumOptions::from_attrs(&item.attrs);
39    let variants = item
40        .variants
41        .iter()
42        .map(|variant| {
43            if variant.discriminant.is_some() {
44                panic!(
45                    "Discriminants in enum variants are not supported. Found: {:?}",
46                    item
47                );
48            }
49
50            // Variants with inline tags may result in unserializable types.
51            let has_inline_tag =
52                options.tag_prop_name.is_some() && options.content_prop_name.is_none();
53
54            let name = variant.ident.to_string();
55            let ty = if variant.fields.is_empty() {
56                Type::Unit
57            } else if variant.fields.iter().any(|field| field.ident.is_some()) {
58                let fields = variant
59                    .fields
60                    .iter()
61                    .map(|field| {
62                        let name = field
63                            .ident
64                            .as_ref()
65                            .unwrap_or_else(|| panic!("Unnamed field in variant of enum {}", ident))
66                            .to_string();
67                        if has_inline_tag && options.tag_prop_name.as_ref() == Some(&name) {
68                            panic!(
69                                "Enum {} cannot be serialized, because the variant `{}` has a \
70                                    field with the same name as the enum's `tag` attribute",
71                                ident, variant.ident
72                            );
73                        }
74
75                        Field {
76                            name: Some(name),
77                            ty: TypeIdent::try_from(&field.ty)
78                                .unwrap_or_else(|_| panic!("Invalid field type in enum {}", ident)),
79                            doc_lines: get_doc_lines(&field.attrs),
80                            attrs: FieldAttrs::from_attrs(&field.attrs),
81                        }
82                    })
83                    .collect();
84                Type::Struct(Struct {
85                    ident: TypeIdent::from(name.clone()),
86                    fields,
87                    doc_lines: Vec::new(),
88                    options: StructOptions::default(),
89                })
90            } else {
91                let item_types: Vec<_> = variant
92                    .fields
93                    .iter()
94                    .map(|field| {
95                        if has_inline_tag && is_path_to_primitive(&field.ty) {
96                            panic!(
97                                "Enum {} cannot be serialized, because the variant `{}` has a \
98                                    primitive unnamed field ({}) and the enum has no `content` \
99                                    attribute",
100                                ident,
101                                variant.ident,
102                                field.ty.to_token_stream()
103                            );
104                        }
105
106                        TypeIdent::try_from(&field.ty)
107                            .unwrap_or_else(|_| panic!("Invalid field type in enum {}", ident))
108                    })
109                    .collect();
110
111                if has_inline_tag && item_types.len() > 1 {
112                    panic!(
113                        "Enum {} cannot be serialized, because the variant `{}` contains multiple \
114                            unnamed fields and the enum has no `content` attribute",
115                        ident, variant.ident,
116                    );
117                }
118
119                Type::Tuple(item_types)
120            };
121            let doc_lines = get_doc_lines(&variant.attrs);
122            let attrs = VariantAttrs::from_attrs(&variant.attrs);
123
124            Variant {
125                name,
126                ty,
127                doc_lines,
128                attrs,
129            }
130        })
131        .collect();
132
133    Enum {
134        ident,
135        variants,
136        doc_lines: get_doc_lines(&item.attrs),
137        options,
138    }
139}
140
141#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
142pub struct EnumOptions {
143    pub variant_casing: Casing,
144    pub content_prop_name: Option<String>,
145    pub tag_prop_name: Option<String>,
146
147    /// If `true`, serialized variants are not tagged, and a match is found
148    /// by attempting to deserialize variants in order, where the first one to
149    /// successfully deserialize is used as the result.
150    pub untagged: bool,
151
152    /// Rust module path where the type can be found for the given generator.
153    /// If present, the generator can use this type instead of generating it.
154    ///
155    /// ## Example:
156    ///
157    /// ```rs
158    /// #[fp(rust_module = "my_crate")]
159    /// enum MyEnum { /* ... */ }
160    /// ```
161    ///
162    /// This will set `"my_crate"` as the rust_module, to
163    /// be used by the Rust plugin generator to generate a `use` statement such
164    /// as:
165    ///
166    /// ```rs
167    /// pub use my_crate::MyEnum;
168    /// ```
169    ///
170    /// Instead of generating the enum definition itself.
171    pub rust_module: Option<String>,
172}
173
174impl EnumOptions {
175    pub fn from_attrs(attrs: &[Attribute]) -> Self {
176        let mut opts = Self::default();
177        for attr in attrs {
178            if attr.path.is_ident("fp") || attr.path.is_ident("serde") {
179                opts.merge_with(
180                    &syn::parse2::<Self>(attr.tokens.clone()).expect("Could not parse attributes"),
181                );
182            }
183        }
184        opts
185    }
186
187    fn merge_with(&mut self, other: &EnumOptions) {
188        if other.variant_casing != Casing::default() {
189            self.variant_casing = other.variant_casing;
190        }
191        if other.content_prop_name.is_some() {
192            self.content_prop_name = other.content_prop_name.clone();
193        }
194        if other.tag_prop_name.is_some() {
195            self.tag_prop_name = other.tag_prop_name.clone();
196        }
197        if other.untagged {
198            self.untagged = true;
199        }
200        if let Some(other_rust_module) = &other.rust_module {
201            self.rust_module = Some(other_rust_module.clone());
202        }
203    }
204
205    pub fn to_serde_attrs(&self) -> Vec<String> {
206        let mut serde_attrs = vec![];
207        if self.untagged {
208            serde_attrs.push("untagged".to_owned());
209        } else if let Some(prop_name) = &self.tag_prop_name {
210            serde_attrs.push(format!("tag = \"{prop_name}\""));
211
212            if let Some(prop_name) = &self.content_prop_name {
213                serde_attrs.push(format!("content = \"{prop_name}\""));
214            }
215        }
216        if let Some(casing) = &self.variant_casing.as_maybe_str() {
217            serde_attrs.push(format!("rename_all = \"{casing}\""));
218        }
219        serde_attrs
220    }
221}
222
223impl Parse for EnumOptions {
224    fn parse(input: ParseStream) -> Result<Self> {
225        let content;
226        parenthesized!(content in input);
227
228        let parse_value = || -> Result<String> {
229            content.parse::<Token![=]>()?;
230            Ok(content
231                .parse::<LitStr>()?
232                .to_token_stream()
233                .to_string()
234                .trim_matches('"')
235                .to_owned())
236        };
237
238        let mut result = Self::default();
239        loop {
240            let key: Ident = content.call(IdentExt::parse_any)?;
241            match key.to_string().as_ref() {
242                "content" => result.content_prop_name = Some(parse_value()?),
243                "tag" => result.tag_prop_name = Some(parse_value()?),
244                "rename_all" => {
245                    result.variant_casing = Casing::try_from(parse_value()?.as_ref())
246                        .map_err(|err| Error::new(content.span(), err))?
247                }
248                "rust_module" => {
249                    result.rust_module = Some(parse_value()?);
250                }
251                "untagged" => result.untagged = true,
252                other => {
253                    return Err(Error::new(
254                        content.span(),
255                        format!("Unexpected attribute: {other}"),
256                    ))
257                }
258            }
259
260            if content.is_empty() {
261                break;
262            }
263
264            content.parse::<Token![,]>()?;
265        }
266
267        Ok(result)
268    }
269}
270
271#[derive(Clone, Debug, Eq, Hash, PartialEq)]
272pub struct Variant {
273    pub name: String,
274    pub ty: Type,
275    pub doc_lines: Vec<String>,
276    pub attrs: VariantAttrs,
277}
278
279#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
280pub struct VariantAttrs {
281    pub field_casing: Casing,
282
283    /// Optional name to use in the serialized format
284    /// (only used if different than the variant name itself).
285    ///
286    /// See also: <https://serde.rs/variant-attrs.html#rename>
287    pub rename: Option<String>,
288}
289
290impl VariantAttrs {
291    pub fn from_attrs(attrs: &[Attribute]) -> Self {
292        let mut opts = Self::default();
293        for attr in attrs {
294            if attr.path.is_ident("fp") || attr.path.is_ident("serde") {
295                opts.merge_with(
296                    &syn::parse2::<Self>(attr.tokens.clone())
297                        .expect("Could not parse variant attributes"),
298                );
299            }
300        }
301        opts
302    }
303
304    fn merge_with(&mut self, other: &Self) {
305        if other.field_casing != Casing::default() {
306            self.field_casing = other.field_casing;
307        }
308        if other.rename.is_some() {
309            self.rename = other.rename.clone();
310        }
311    }
312
313    pub fn to_serde_attrs(&self) -> Vec<String> {
314        let mut serde_attrs = vec![];
315        if let Some(rename) = self.rename.as_ref() {
316            serde_attrs.push(format!("rename = \"{rename}\""));
317        }
318        if let Some(casing) = &self.field_casing.as_maybe_str() {
319            serde_attrs.push(format!("rename_all = \"{casing}\""));
320        }
321        serde_attrs
322    }
323}
324
325impl Parse for VariantAttrs {
326    fn parse(input: ParseStream) -> Result<Self> {
327        let content;
328        parenthesized!(content in input);
329
330        let parse_value = || -> Result<String> {
331            content.parse::<Token![=]>()?;
332            Ok(content
333                .parse::<LitStr>()?
334                .to_token_stream()
335                .to_string()
336                .trim_matches('"')
337                .to_owned())
338        };
339
340        let mut result = Self::default();
341        loop {
342            let key: Ident = content.call(IdentExt::parse_any)?;
343            match key.to_string().as_ref() {
344                "rename" => result.rename = Some(parse_value()?),
345                "rename_all" => {
346                    result.field_casing = Casing::try_from(parse_value()?.as_ref())
347                        .map_err(|err| Error::new(content.span(), err))?
348                }
349                other => {
350                    return Err(Error::new(
351                        content.span(),
352                        format!("Unexpected variant attribute: {other}"),
353                    ))
354                }
355            }
356
357            if content.is_empty() {
358                break;
359            }
360
361            content.parse::<Token![,]>()?;
362        }
363
364        Ok(result)
365    }
366}
367
368fn is_path_to_primitive(ty: &syn::Type) -> bool {
369    matches!(
370        ty,
371        syn::Type::Path(TypePath { path, qself })
372            if qself.is_none()
373                && path
374                    .get_ident()
375                    .map(ToString::to_string)
376                    .map(|ident| ident == "String" || Primitive::from_str(&ident).is_ok())
377                    .unwrap_or(false)
378    )
379}