cynic_codegen/enum_derive/
input.rs

1use darling::util::SpannedValue;
2use proc_macro2::Span;
3
4use crate::{
5    error::Errors,
6    idents::{RenamableFieldIdent, RenameAll},
7    schema::SchemaInput,
8};
9
10#[derive(darling::FromDeriveInput)]
11#[darling(attributes(cynic), supports(enum_unit, enum_newtype))]
12pub struct EnumDeriveInput {
13    pub(super) ident: proc_macro2::Ident,
14    pub(super) data: darling::ast::Data<SpannedValue<EnumDeriveVariant>, ()>,
15
16    #[darling(default)]
17    schema: Option<SpannedValue<String>>,
18    #[darling(default)]
19    schema_path: Option<SpannedValue<String>>,
20
21    #[darling(default, rename = "schema_module")]
22    schema_module_: Option<syn::Path>,
23
24    #[darling(default)]
25    pub graphql_type: Option<SpannedValue<String>>,
26
27    #[darling(default)]
28    pub(super) rename_all: Option<RenameAll>,
29
30    #[darling(default)]
31    pub(super) non_exhaustive: bool,
32}
33
34impl EnumDeriveInput {
35    pub fn schema_module(&self) -> syn::Path {
36        if let Some(schema_module) = &self.schema_module_ {
37            return schema_module.clone();
38        }
39        syn::parse2(quote::quote! { schema }).unwrap()
40    }
41}
42
43#[derive(Debug, darling::FromVariant)]
44#[darling(attributes(cynic))]
45pub struct EnumDeriveVariant {
46    pub(super) ident: proc_macro2::Ident,
47
48    #[darling(default)]
49    pub(super) rename: Option<SpannedValue<String>>,
50
51    #[darling(default)]
52    pub(super) fallback: SpannedValue<bool>,
53
54    pub(super) fields: darling::ast::Fields<()>,
55}
56
57impl EnumDeriveInput {
58    pub fn graphql_type_name(&self) -> String {
59        self.graphql_type
60            .as_ref()
61            .map(|sp| sp.to_string())
62            .unwrap_or_else(|| self.ident.to_string())
63    }
64
65    pub fn graphql_type_span(&self) -> Span {
66        self.graphql_type
67            .as_ref()
68            .map(|val| val.span())
69            .unwrap_or_else(|| self.ident.span())
70    }
71
72    pub fn schema_input(&self) -> Result<SchemaInput, syn::Error> {
73        match (&self.schema, &self.schema_path) {
74            (None, None) => SchemaInput::default().map_err(|e| e.into_syn_error(Span::call_site())),
75            (None, Some(path)) => SchemaInput::from_schema_path(path.as_ref())
76                .map_err(|e| e.into_syn_error(path.span())),
77            (Some(name), None) => SchemaInput::from_schema_name(name.as_ref())
78                .map_err(|e| e.into_syn_error(name.span())),
79            (Some(_), Some(path)) => Err(syn::Error::new(
80                path.span(),
81                "Only one of schema_path & schema can be provided",
82            )),
83        }
84    }
85
86    pub(super) fn validate(&self) -> Result<(), Errors> {
87        let data_ref = self.data.as_ref().take_enum().unwrap();
88        let fallbacks = data_ref.iter().filter(|v| *v.fallback).collect::<Vec<_>>();
89        let mut errors = Errors::default();
90
91        if fallbacks.len() > 1 {
92            errors.extend(
93                fallbacks
94                    .into_iter()
95                    .map(|f| {
96                        syn::Error::new(
97                            f.span(),
98                            "Enums only support a single fallback, but this enum has many",
99                        )
100                    })
101                    .collect::<Vec<_>>(),
102            );
103        }
104
105        errors.extend(data_ref.iter().filter_map(|v| v.validate(v.span()).err()));
106
107        errors.into_result(())
108    }
109}
110
111impl EnumDeriveVariant {
112    pub(super) fn graphql_ident(&self, rename_rule: RenameAll) -> RenamableFieldIdent {
113        let mut ident = RenamableFieldIdent::from(self.ident.clone());
114        match &self.rename {
115            Some(rename) => {
116                let span = rename.span();
117                let rename = (**rename).clone();
118                ident.set_rename(rename, span)
119            }
120            None => {
121                ident.rename_with(rename_rule, self.ident.span());
122            }
123        }
124        ident
125    }
126
127    fn validate(&self, span: proc_macro2::Span) -> Result<(), Errors> {
128        use darling::ast::Style::*;
129
130        if *self.fallback {
131            match (self.fields.style, self.fields.len()) {
132                (Unit, _) => Ok(()),
133                (Struct, _) => Err(syn::Error::new(
134                    span,
135                    "Enum derive doesn't support struct variants as a fallback",
136                )
137                .into()),
138                (Tuple, 1) => Ok(()),
139                (Tuple, _) => Err(syn::Error::new(
140                    span,
141                    "Enum derive fallbacks can only have a single field",
142                )
143                .into()),
144            }
145            // TODO: make sure we only have a string somewhere
146        } else {
147            match self.fields.style {
148                Unit => Ok(()),
149                _ => Err(syn::Error::new(span, "GraphQL Enums can't have fields").into()),
150            }
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use darling::FromDeriveInput;
158    use syn::parse_quote;
159
160    use super::*;
161
162    #[test]
163    fn test_enum_with_no_fallbacks() {
164        let input = EnumDeriveInput::from_derive_input(&parse_quote! {
165            enum TestEnum {
166                Foo,
167                Bar
168            }
169        })
170        .unwrap();
171
172        input.validate().expect("no errors");
173    }
174
175    #[test]
176    fn test_enum_with_empty_fallback() {
177        let input = EnumDeriveInput::from_derive_input(&parse_quote! {
178            enum TestEnum {
179                Foo,
180                #[cynic(fallback)]
181                Bar
182            }
183        })
184        .unwrap();
185
186        input.validate().expect("no errors");
187    }
188
189    #[test]
190    fn test_enum_with_string_fallback() {
191        let input = EnumDeriveInput::from_derive_input(&parse_quote! {
192            enum TestEnum {
193                Foo,
194                #[cynic(fallback)]
195                Bar(String)
196            }
197        })
198        .unwrap();
199
200        input.validate().expect("no errors");
201    }
202}