cynic_codegen/inline_fragments_derive/
input.rs1use {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}