graphql_extract/
lib.rs

1//! Macro to extract data from deeply nested types representing GraphQL results
2//!
3//! # Suggested workflow
4//!
5//! 1. Generate query types using [cynic] and its [generator]
6//! 1. Use [insta] to define an inline snapshot test so that the query string is visible in the
7//!    module that defines the query types
8//! 1. Define an `extract` function that takes the root query type and returns the data of interest
9//! 1. Inside `extract`, use [`extract!`](crate::extract!) as `extract!(data => { ... })`
10//! 1. Inside the curly braces, past the query string from the snapshot test above
11//! 1. Change all node names from `camelCase` to `snake_case`
12//! 1. Add `?` after the nodes that are nullable
13//! 1. Add `[]` after the nodes that are iterable
14//!
15//! # Examples
16//!
17//! The following omits the `derive`s for [cynic] traits that are usually implemented for GraphQL
18//! queries. This is so that we can focus on the nesting of the structures and how the macro helps
19//! to 'extract' the leaves.
20//!
21//! ```no_run
22//! struct Query {
23//!     object: Option<Object>,
24//! }
25//!
26//! struct Object {
27//!     dynamic_field: Option<DynamicField>,
28//! }
29//!
30//! struct DynamicField {
31//!     value: Option<DynamicFieldValue>,
32//! }
33//!
34//! enum DynamicFieldValue {
35//!     MoveValue(MoveValue),
36//!     Unknown,
37//! }
38//!
39//! struct MoveValue {
40//!     type_: MoveType,
41//!     bcs: Option<String>,
42//! }
43//!
44//! struct MoveType {
45//!     repr: String,
46//! }
47//!
48//! fn extract(data: Option<Query>) -> Result<(MoveType, String), &'static str> {
49//!     use graphql_extract::extract;
50//!     use DynamicFieldValue::MoveValue;
51//!
52//!     // Leafs become available as variables
53//!     extract!(data => {
54//!         object? {
55//!             dynamic_field? {
56//!                 value? {
57//!                     // `MoveValue` is the enum variant name we're interested in
58//!                     ... on MoveValue {
59//!                         type_
60//!                         bcs?
61//!                     }
62//!                 }
63//!             }
64//!         }
65//!     });
66//!     Ok((type_, bcs))
67//! }
68//! ```
69//!
70//! ```no_run
71//! struct Query {
72//!     address: Option<Address2>,
73//!     object: Option<Object>,
74//! }
75//!
76//! struct Address2 {
77//!     address: String,
78//! }
79//!
80//! struct Object {
81//!     version: u64,
82//!     dynamic_field: Option<DynamicField>,
83//!     dynamic_fields: DynamicFieldConnection,
84//! }
85//!
86//! struct DynamicFieldConnection {
87//!     nodes: Vec<DynamicField>,
88//! }
89//!
90//! struct DynamicField {
91//!     value: Option<DynamicFieldValue>,
92//! }
93//!
94//! enum DynamicFieldValue {
95//!     MoveValue(MoveValue),
96//!     Unknown,
97//! }
98//!
99//! struct MoveValue {
100//!     type_: MoveType,
101//!     bcs: String,
102//! }
103//!
104//! struct MoveType {
105//!     repr: String,
106//! }
107//!
108//! type Item = Result<(MoveType, String), &'static str>;
109//!
110//! fn extract(data: Option<Query>) -> Result<(u64, impl Iterator<Item = Item>), &'static str> {
111//!     use graphql_extract::extract;
112//!     use DynamicFieldValue::MoveValue;
113//!
114//!     extract!(data => {
115//!         object? {
116//!             version
117//!             dynamic_fields {
118//!                 // `nodes` becomes a variable in the namespace. It implements `Iterator`
119//!                 nodes[] {
120//!                     // Everything underneath an iterator node works the same, except it 'maps'
121//!                     // the items of the iterator (check the `Item` type alias above)
122//!                     value? {
123//!                         ... on MoveValue {
124//!                             type_
125//!                             bcs
126//!                         }
127//!                     }
128//!                 }
129//!             }
130//!         }
131//!     });
132//!     Ok((version, nodes))
133//! }
134//! ```
135//!
136//! A caveat to the above is that nested `iterator[]` nodes aren't handled yet. They'll likely be
137//! forbidden in the future.
138//!
139//! [cynic]: https://cynic-rs.dev/
140//! [generator]: https://generator.cynic-rs.dev/
141//! [insta]: https://insta.rs/
142
143use proc_macro2::{Span, TokenStream};
144use quote::{ToTokens as _, quote};
145use syn::parse::{Parse, ParseStream};
146use syn::spanned::Spanned as _;
147use syn::token::{self, Brace};
148use syn::{Error, Ident, Token, braced, bracketed, parse_macro_input, parse_quote};
149
150/// See the top-level [`crate`] doc for a description.
151#[proc_macro]
152pub fn extract(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
153    let root = parse_macro_input!(input as Root);
154    let stmt = root.generate_extract();
155    stmt.into()
156}
157
158struct Root {
159    expr: syn::Expr,
160    nested: Nested,
161}
162
163struct Node {
164    ident: Ident,
165    alias: Option<Ident>,
166    optional: bool,
167    iterable: bool,
168    nested: Option<Nested>,
169}
170
171enum Nested {
172    Nodes(Vec<Node>),
173    Variant(Variant),
174}
175
176struct Variant {
177    path: syn::Path,
178    nodes: Vec<Node>,
179}
180
181//=================================================================================================
182// Parsing
183//=================================================================================================
184
185impl Parse for Root {
186    fn parse(input: ParseStream) -> syn::Result<Self> {
187        let expr = input.parse()?;
188        let _: Token![=>] = input.parse()?;
189        let nested = input.parse()?;
190        Ok(Self { expr, nested })
191    }
192}
193
194impl Parse for Node {
195    fn parse(input: ParseStream) -> syn::Result<Self> {
196        let mut self_ = Self {
197            ident: input.parse()?,
198            alias: None,
199            optional: false,
200            iterable: false,
201            nested: None,
202        };
203
204        // Caller is allowed to set an alias like `alias: node`
205        let lookahead = input.lookahead1();
206        if lookahead.peek(Token![:]) {
207            let _: Token![:] = input.parse()?;
208            self_.alias = Some(self_.ident);
209            self_.ident = input.parse()?;
210        }
211
212        while !input.is_empty() {
213            let lookahead = input.lookahead1();
214            if lookahead.peek(Ident) {
215                break; // There's another field to be parsed
216            } else if lookahead.peek(Token![?]) {
217                let question: Token![?] = input.parse()?;
218                if self_.optional {
219                    return Err(Error::new_spanned(
220                        question,
221                        "Can't have two `?` for the same node",
222                    ));
223                }
224                self_.optional = true;
225            } else if lookahead.peek(token::Bracket) {
226                let content;
227                let bracket = bracketed!(content in input);
228                if self_.iterable {
229                    return Err(Error::new(
230                        bracket.span.span(),
231                        "Can't have two `[]` for the same node",
232                    ));
233                }
234                if !content.is_empty() {
235                    return Err(Error::new(
236                        bracket.span.span(),
237                        "Only empty brackets allowed",
238                    ));
239                }
240                self_.iterable = true;
241            } else if lookahead.peek(token::Brace) {
242                let nested = input.parse()?;
243                self_.nested = Some(nested);
244                break; // Everything after the closing brace is ignored
245            } else {
246                return Err(lookahead.error());
247            }
248        }
249
250        Ok(self_)
251    }
252}
253
254impl Node {
255    fn within_braces(brace: Brace, content: ParseStream) -> syn::Result<Vec<Self>> {
256        let mut nodes = vec![];
257        while !content.is_empty() {
258            let lookahead = content.lookahead1();
259            if lookahead.peek(Token![...]) {
260                return Err(Error::new(
261                    brace.span.span(),
262                    "Nodes can't be mixed with '... on Variant' matches",
263                ));
264            }
265            nodes.push(content.parse()?);
266        }
267        if nodes.is_empty() {
268            return Err(Error::new(
269                brace.span.span(),
270                "Empty braces. Must have at least one node",
271            ));
272        }
273        Ok(nodes)
274    }
275}
276
277impl Parse for Nested {
278    fn parse(input: ParseStream) -> syn::Result<Self> {
279        let content;
280        let brace = braced!(content in input);
281
282        let lookahead = content.lookahead1();
283        Ok(if lookahead.peek(Token![...]) {
284            let var = Self::Variant(content.parse()?);
285            if !content.is_empty() {
286                return Err(Error::new(
287                    brace.span.span(),
288                    "Only a single '... on Variant' match is supported within the same braces",
289                ));
290            }
291            var
292        } else {
293            Self::Nodes(Node::within_braces(brace, &content)?)
294        })
295    }
296}
297
298impl Parse for Variant {
299    fn parse(input: ParseStream) -> syn::Result<Self> {
300        input.parse::<Token![...]>()?;
301        let on: Ident = input.parse()?;
302        if on != "on" {
303            return Err(Error::new(on.span(), "Expected 'on'"));
304        }
305        let path = input.parse()?;
306        let content;
307        let brace = braced!(content in input);
308        Ok(Self {
309            path,
310            nodes: Node::within_braces(brace, &content)?,
311        })
312    }
313}
314
315//=================================================================================================
316// Generation
317//=================================================================================================
318
319impl Root {
320    fn generate_extract(self) -> TokenStream {
321        let Self { expr, nested, .. } = self;
322        let data = Ident::new("data", Span::mixed_site());
323        let err = data.to_string() + " is null";
324        let (pats, tokens): (Vec<_>, Vec<_>) =
325            nested.generate_extract(data.clone(), data.to_string());
326        quote! {
327            let #data = ( #expr ).ok_or::<&'static str>(#err)?;
328            let ( #(#pats),* ) = {
329                #(#tokens)*
330                ( #(#pats),* )
331            };
332        }
333    }
334}
335
336impl Node {
337    fn generate_extract(self, data: Ident, path: String) -> (syn::Pat, TokenStream) {
338        let Self {
339            ident,
340            alias,
341            optional,
342            iterable,
343            nested,
344        } = self;
345        let field = &ident;
346        let ident = alias.as_ref().unwrap_or(&ident);
347
348        let path = path + " -> " + ident.to_string().as_str();
349
350        let assign = if optional {
351            let err = path.clone() + " is null";
352            quote!(let #ident = #data.#field.ok_or::<&'static str>(#err)?;)
353        } else {
354            quote!(let #ident = #data.#field;)
355        };
356
357        let Some(inner) = nested else {
358            return (parse_quote!(#ident), assign);
359        };
360
361        let (pats, tokens) = inner.generate_extract(ident.clone(), path);
362        let (pat, tokens_);
363        // TODO: consider
364        // - verifying that no nested `[]` exist
365        // - detecting any `?` in the subtree and setting the return type accordingly
366        if iterable {
367            pat = parse_quote!(#ident);
368            tokens_ = quote! {
369                #assign
370                let #ident = #ident.into_iter().map(|#ident| -> Result<_, &'static str> {
371                    #(#tokens)*
372                    Ok(( #(#pats),* ))
373                });
374            };
375        } else {
376            pat = parse_quote!( (#(#pats),*) );
377            tokens_ = quote! {
378                #assign
379                let ( #(#pats),* ) = {
380                    #(#tokens)*
381                    ( #(#pats),* )
382                };
383            };
384        }
385        (pat, tokens_)
386    }
387}
388
389impl Nested {
390    fn generate_extract(self, data: Ident, path: String) -> (Vec<syn::Pat>, Vec<TokenStream>) {
391        match self {
392            Self::Nodes(nodes) => nodes
393                .into_iter()
394                .map(|n| n.generate_extract(data.clone(), path.clone()))
395                .unzip(),
396            Self::Variant(Variant { path: var, nodes }) => {
397                let path = path + " ... on " + var.to_token_stream().to_string().as_str();
398                let err = path.clone() + " is null";
399                let val = Ident::new("val", Span::mixed_site());
400                let assign = quote! {
401                    let #var(#val) = #data else {
402                        return Err(#err);
403                    };
404                };
405
406                let mut tokens_ = vec![assign];
407                let (pats, tokens): (Vec<_>, Vec<_>) = nodes
408                    .into_iter()
409                    .map(|n| n.generate_extract(val.clone(), path.clone()))
410                    .unzip();
411                tokens_.extend(tokens);
412                (pats, tokens_)
413            }
414        }
415    }
416}