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}