cynic_codegen/inline_fragments_derive/
input.rs

1use {darling::util::SpannedValue, proc_macro2::Span};
2
3use crate::{error::Errors, schema::SchemaInput};
4
5#[derive(darling::FromDeriveInput)]
6#[darling(attributes(cynic), supports(enum_newtype, enum_unit))]
7pub struct InlineFragmentsDeriveInput {
8    pub(super) ident: proc_macro2::Ident,
9    pub(super) data: darling::ast::Data<SpannedValue<InlineFragmentsDeriveVariant>, ()>,
10    pub(super) generics: syn::Generics,
11
12    #[darling(default)]
13    schema: Option<SpannedValue<String>>,
14    #[darling(default)]
15    schema_path: Option<SpannedValue<String>>,
16
17    #[darling(default)]
18    pub(super) exhaustive: Option<SpannedValue<bool>>,
19
20    #[darling(default, rename = "schema_module")]
21    schema_module_: Option<syn::Path>,
22
23    #[darling(default)]
24    pub graphql_type: Option<SpannedValue<String>>,
25
26    #[darling(default)]
27    variables: Option<syn::Path>,
28}
29
30impl InlineFragmentsDeriveInput {
31    pub fn schema_module(&self) -> syn::Path {
32        if let Some(schema_module) = &self.schema_module_ {
33            return schema_module.clone();
34        }
35        syn::parse2(quote::quote! { schema }).unwrap()
36    }
37
38    pub fn graphql_type_name(&self) -> String {
39        self.graphql_type
40            .as_ref()
41            .map(|sp| sp.to_string())
42            .unwrap_or_else(|| self.ident.to_string())
43    }
44
45    pub fn graphql_type_span(&self) -> Span {
46        self.graphql_type
47            .as_ref()
48            .map(|val| val.span())
49            .unwrap_or_else(|| self.ident.span())
50    }
51
52    pub fn variables(&self) -> Option<syn::Path> {
53        self.variables.clone()
54    }
55
56    pub(super) fn validate(&self, mode: ValidationMode) -> Result<(), Errors> {
57        let data_ref = self.data.as_ref().take_enum().unwrap();
58
59        let fallbacks = data_ref.iter().filter(|v| *v.fallback).collect::<Vec<_>>();
60        let mut errors = Errors::default();
61
62        if fallbacks.is_empty() {
63            errors.push(syn::Error::new(proc_macro2::Span::call_site(), "InlineFragments derives require a fallback.  Add a unit variant and mark it with `#[cynic(fallback)]`"));
64        }
65
66        if fallbacks.len() > 1 {
67            errors.extend(
68                fallbacks
69                    .into_iter()
70                    .map(|f| {
71                        syn::Error::new(
72                            f.span(),
73                            "InlineFragments only support a single fallback, but this enum has many",
74                        )
75                    })
76                    .collect::<Vec<_>>(),
77            );
78        }
79
80        errors.extend(
81            data_ref
82                .iter()
83                .filter_map(|v| v.validate(mode, v.span()).err()),
84        );
85
86        match (mode, &self.exhaustive) {
87            (ValidationMode::Interface, Some(exhaustive)) if **exhaustive => {
88                errors.push(syn::Error::new(
89                    exhaustive.span(),
90                    "exhaustiveness checking is only supported on graphql unions",
91                ));
92            }
93            _ => {}
94        }
95
96        errors.into_result(())
97    }
98
99    pub fn schema_input(&self) -> Result<SchemaInput, syn::Error> {
100        match (&self.schema, &self.schema_path) {
101            (None, None) => SchemaInput::default().map_err(|e| e.into_syn_error(Span::call_site())),
102            (None, Some(path)) => SchemaInput::from_schema_path(path.as_ref())
103                .map_err(|e| e.into_syn_error(path.span())),
104            (Some(name), None) => SchemaInput::from_schema_name(name.as_ref())
105                .map_err(|e| e.into_syn_error(name.span())),
106            (Some(_), Some(path)) => Err(syn::Error::new(
107                path.span(),
108                "Only one of schema_path & schema can be provided",
109            )),
110        }
111    }
112}
113
114#[derive(darling::FromVariant)]
115#[darling(attributes(cynic))]
116pub(super) struct InlineFragmentsDeriveVariant {
117    pub(super) ident: proc_macro2::Ident,
118    pub fields: darling::ast::Fields<InlineFragmentsDeriveField>,
119
120    #[darling(default)]
121    pub(super) fallback: SpannedValue<bool>,
122}
123
124#[derive(darling::FromField)]
125#[darling(attributes(cynic))]
126pub(super) struct InlineFragmentsDeriveField {
127    pub ty: syn::Type,
128}
129
130#[derive(Clone, Copy, Debug)]
131pub(super) enum ValidationMode {
132    Interface,
133    Union,
134}
135
136impl InlineFragmentsDeriveVariant {
137    fn validate(&self, mode: ValidationMode, span: proc_macro2::Span) -> Result<(), Errors> {
138        use {
139            darling::ast::Style::{Struct, Tuple, Unit},
140            ValidationMode::{Interface, Union},
141        };
142
143        if *self.fallback {
144            match (mode, self.fields.style, self.fields.len()) {
145                (_, Unit, _) => Ok(()),
146                (Interface | Union, Tuple, 1) => Ok(()),
147                (_, Struct, _) => Err(syn::Error::new(
148                    span,
149                    "The InlineFragments derive doesn't currently support struct variants",
150                )
151                .into()),
152                (Interface, Tuple, _) => Err(syn::Error::new(
153                    span,
154                    "InlineFragments fallbacks on an interface must be a unit or newtype variant",
155                )
156                .into()),
157                (Union, Tuple, _) => Err(syn::Error::new(
158                    span,
159                    "InlineFragments fallbacks on a union must be a unit or newtype variant",
160                )
161                .into()),
162            }
163        } else {
164            match (self.fields.style, self.fields.len()) {
165                (Tuple, 1) => Ok(()),
166                (Struct, _) => Err(syn::Error::new(
167                    span,
168                    "The InlineFragments derive doesn't currently support struct variants",
169                )
170                .into()),
171                (_, _) => Err(syn::Error::new(
172                    span,
173                    "Variants on the InlineFragments derive should have one field",
174                )
175                .into()),
176            }
177        }
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use {darling::FromDeriveInput, syn::parse_quote};
184
185    use super::*;
186
187    #[test]
188    fn test_interface_validation() {
189        let input = InlineFragmentsDeriveInput::from_derive_input(&parse_quote! {
190            #[cynic(schema_path = "whatever")]
191            enum TestStruct {
192                AnIncorrectUnitVariant,
193                AVariantThatIsFine(SomeStruct),
194            }
195        })
196        .unwrap();
197
198        insta::assert_display_snapshot!(input.validate(ValidationMode::Interface).unwrap_err(), @r###"
199        InlineFragments derives require a fallback.  Add a unit variant and mark it with `#[cynic(fallback)]`
200        Variants on the InlineFragments derive should have one field
201        "###);
202    }
203
204    #[test]
205    fn test_union_validation() {
206        let input = InlineFragmentsDeriveInput::from_derive_input(&parse_quote! {
207            #[cynic(schema_path = "whatever")]
208            enum TestStruct {
209                AnIncorrectUnitVariant,
210                AVariantThatIsFine(SomeStruct),
211            }
212        })
213        .unwrap();
214
215        insta::assert_display_snapshot!(input.validate(ValidationMode::Union).unwrap_err(), @r###"
216        InlineFragments derives require a fallback.  Add a unit variant and mark it with `#[cynic(fallback)]`
217        Variants on the InlineFragments derive should have one field
218        "###);
219    }
220
221    #[test]
222    fn test_multiple_fallback_validation() {
223        let input = InlineFragmentsDeriveInput::from_derive_input(&parse_quote! {
224            #[cynic(schema_path = "whatever")]
225            enum TestStruct {
226                #[cynic(fallback)]
227                FirstFallback,
228                #[cynic(fallback)]
229                SecondFallback,
230            }
231        })
232        .unwrap();
233
234        insta::assert_display_snapshot!(input.validate(ValidationMode::Union).unwrap_err(), @r###"
235        InlineFragments only support a single fallback, but this enum has many
236        InlineFragments only support a single fallback, but this enum has many
237        "###);
238    }
239
240    #[test]
241    fn test_interface_fallback_validation_happy_path() {
242        let input = InlineFragmentsDeriveInput::from_derive_input(&parse_quote! {
243            #[cynic(schema_path = "whatever")]
244            enum TestStruct {
245                #[cynic(fallback)]
246                FirstFallback,
247            }
248        })
249        .unwrap();
250
251        input.validate(ValidationMode::Interface).unwrap();
252
253        let input = InlineFragmentsDeriveInput::from_derive_input(&parse_quote! {
254            #[cynic(schema_path = "whatever")]
255            enum TestStruct {
256                #[cynic(fallback)]
257                FirstFallback(SomeStruct),
258            }
259        })
260        .unwrap();
261
262        input.validate(ValidationMode::Interface).unwrap();
263    }
264
265    #[test]
266    fn test_union_fallback_validation_happy_path() {
267        let input = InlineFragmentsDeriveInput::from_derive_input(&parse_quote! {
268            #[cynic(schema_path = "whatever")]
269            enum TestStruct {
270                #[cynic(fallback)]
271                FirstFallback,
272            }
273        })
274        .unwrap();
275
276        input.validate(ValidationMode::Union).unwrap();
277    }
278
279    #[test]
280    fn test_union_fallback_validation_with_newtype() {
281        let input = InlineFragmentsDeriveInput::from_derive_input(&parse_quote! {
282            #[cynic(schema_path = "whatever")]
283            enum TestStruct {
284                #[cynic(fallback)]
285                FirstFallback(String),
286            }
287        })
288        .unwrap();
289
290        input.validate(ValidationMode::Union).unwrap();
291    }
292}