cynic_codegen/query_variables_derive/
mod.rs

1use {
2    proc_macro2::TokenStream,
3    quote::{format_ident, quote, quote_spanned},
4    syn::visit_mut::{self, VisitMut},
5};
6
7mod input;
8
9use crate::{generics_for_serde, variables_fields_ident};
10
11use self::input::QueryVariablesDeriveInput;
12
13pub fn query_variables_derive(ast: &syn::DeriveInput) -> Result<TokenStream, syn::Error> {
14    use darling::FromDeriveInput;
15
16    match QueryVariablesDeriveInput::from_derive_input(ast) {
17        Ok(input) => query_variables_derive_impl(input),
18        Err(e) => Ok(e.write_errors()),
19    }
20}
21
22pub fn query_variables_derive_impl(
23    input: QueryVariablesDeriveInput,
24) -> Result<TokenStream, syn::Error> {
25    let ident = &input.ident;
26
27    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
28    let generics_with_ser = generics_for_serde::with_serialize_bounds(&input.generics);
29    let (impl_generics_with_ser, _, where_clause_with_ser) = generics_with_ser.split_for_impl();
30
31    let vis = &input.vis;
32    let schema_module = &input.schema_module();
33    let fields_struct_ident = variables_fields_ident(ident);
34
35    let input_fields = input.data.take_struct().unwrap().fields;
36
37    let mut field_funcs = Vec::new();
38    let mut variables = Vec::new();
39    let mut field_inserts = Vec::new();
40    let mut coercion_checks = Vec::new();
41    let mut field_output_types = Vec::new();
42
43    for (field_idx, f) in input_fields.into_iter().enumerate() {
44        let name = f.ident.as_ref().unwrap();
45        let ty = &f.ty;
46        let mut ty_for_fields_struct = match f.graphql_type {
47            None => ty.clone(),
48            Some(ref graphql_type) => {
49                // This enables to support generics. Normally we have every Variable type that
50                // implements `CoercesTo<schema::SchemaType>`. Here that is also
51                // the case, but:
52                // - The XxxVariablesFields struct needs to not be generic, as it needs to be
53                //   referenced explicitly by the `QueryFragment` type (to have access to the
54                //   functions that enable to typecheck for the presence of variables), and we
55                //   like the QueryFragment itself to not be generic on the variables type to
56                //   generate the query.
57                // - Normally we use the type of the fields of the XxxVariables directly in
58                //   these return types of the functions of XxxVariablesFields, and that type
59                //   implements `CoercesTo<schema::CorrespondingType>` (as derived by
60                //   `InputObject`, or specified by cynic if it's a literal)
61                // - Now we want to still typecheck that our concrete (specialized) types have
62                //   all their fields implement `CoercesTo` the correct schema type, but we also
63                //   want to not be generic in the `Fields` struct. This is achieved by:
64                //   - Creating a proxy type that will be used in the XxxVariablesFields struct
65                //   - Implementing `CoercesTo<SchemaType>` for it (so that the regular
66                //     typecheck passes)
67                //   - Adding a separate typecheck in the concrete type for the
68                //     `CoercesTo<schema_type>` (so that we still check for correctness)
69
70                let graphql_type_full_path = match graphql_type.get_ident() {
71                    Some(ident) => {
72                        // They are probably trying to refer to the single type in the schema
73                        // (Also this is necessary for backwards compatibility, now that we
74                        // add the branch below with the ability to have any path)
75                        let span = ident.span();
76                        quote_spanned! {span =>
77                            #schema_module::#ident
78                        }
79                    }
80                    None => quote! { #graphql_type },
81                };
82
83                let new_type_that_coerces_to_schema_type =
84                    format_ident!("CoercionProxyForField{field_idx}");
85                field_output_types.push(quote! {
86                    #vis struct #new_type_that_coerces_to_schema_type;
87                    cynic::impl_coercions!(#new_type_that_coerces_to_schema_type [] [], #graphql_type_full_path);
88                });
89                coercion_checks.push(quote! {
90                    cynic::assert_impl!(#ty [#impl_generics] [#where_clause]: cynic::coercions::CoercesTo<#graphql_type_full_path>);
91                });
92
93                // Turn that from an ident into a type
94                syn::parse_quote! { #new_type_that_coerces_to_schema_type }
95            }
96        };
97        TurnLifetimesToStatic.visit_type_mut(&mut ty_for_fields_struct);
98        let name_str =
99            proc_macro2::Literal::string(&f.graphql_ident(input.rename_all).graphql_name());
100
101        field_funcs.push(quote! {
102            #vis fn #name() -> cynic::variables::VariableDefinition<Self, #ty_for_fields_struct> {
103                cynic::variables::VariableDefinition::new(#name_str)
104            }
105        });
106
107        variables.push(quote! {
108            (#name_str, <#ty as #schema_module::variable::Variable>::TYPE)
109        });
110
111        match f.skip_serializing_if {
112            Some(skip_check_fn) => {
113                let skip_check_fn = &*skip_check_fn;
114                field_inserts.push(quote! {
115                    if !#skip_check_fn(&self.#name) {
116                        map_serializer.serialize_entry(#name_str, &self.#name)?;
117                    }
118                })
119            }
120            None => field_inserts.push(quote! {
121                map_serializer.serialize_entry(#name_str, &self.#name)?;
122            }),
123        }
124    }
125
126    let map_len = field_inserts.len();
127
128    let ident_span = ident.span();
129    let fields_struct = quote_spanned! { ident_span =>
130        #vis struct #fields_struct_ident;
131
132        impl cynic::QueryVariablesFields for #fields_struct_ident {}
133
134        impl cynic::queries::VariableMatch<#fields_struct_ident> for #fields_struct_ident {}
135
136        const _: () = {
137            #(
138                #field_output_types
139            )*
140
141            impl #fields_struct_ident {
142                #(
143                    #field_funcs
144                )*
145            }
146        };
147    };
148
149    Ok(quote! {
150
151        #[automatically_derived]
152        impl #impl_generics cynic::QueryVariables for #ident #ty_generics #where_clause {
153            type Fields = #fields_struct_ident;
154            const VARIABLES: &'static [(&'static str, cynic::variables::VariableType)]
155                = &[#(#variables),*];
156        }
157
158        #[automatically_derived]
159        impl #impl_generics_with_ser cynic::serde::Serialize for #ident #ty_generics #where_clause_with_ser {
160            fn serialize<__S>(&self, serializer: __S) -> Result<__S::Ok, __S::Error>
161            where
162                __S: cynic::serde::Serializer,
163            {
164                use cynic::serde::ser::SerializeMap;
165                #(#coercion_checks)*
166
167                let mut map_serializer = serializer.serialize_map(Some(#map_len))?;
168
169                #(#field_inserts)*
170
171                map_serializer.end()
172            }
173        }
174
175        #fields_struct
176    })
177}
178
179struct TurnLifetimesToStatic;
180impl VisitMut for TurnLifetimesToStatic {
181    fn visit_lifetime_mut(&mut self, i: &mut syn::Lifetime) {
182        i.ident = format_ident!("static");
183        visit_mut::visit_lifetime_mut(self, i)
184    }
185}