Skip to main content

sui_graphql_macros/
query.rs

1//! The `graphql_query!` function-style macro: compile-time validation and
2//! canonical formatting of GraphQL queries and mutations against the embedded
3//! Sui schema.
4//!
5//! Validation is delegated to `apollo-compiler`, which performs full GraphQL
6//! spec validation (selection set against schema, argument types, fragment
7//! shapes, variable usage, etc.). On success, the macro emits a `&'static str`
8//! literal containing the canonically formatted query. On failure, every
9//! diagnostic becomes a `syn::Error` anchored at the input literal's span.
10//!
11//! The macro is intentionally decoupled from any consumer crate: it produces
12//! a plain string literal so callers can wrap it in their own type. The
13//! `sui-graphql` crate ships a `macro_rules!` wrapper that nests this macro
14//! inside a `ValidatedQuery` constructor, but this proc macro itself has no
15//! dependency on or knowledge of that type.
16
17use std::sync::LazyLock;
18
19use apollo_compiler::ExecutableDocument;
20use apollo_compiler::Schema;
21use apollo_compiler::validation::Valid;
22use proc_macro::TokenStream;
23use proc_macro2::TokenStream as TokenStream2;
24use quote::quote;
25use syn::LitStr;
26
27/// Diagnostic label for the embedded SDL. apollo-compiler embeds this string
28/// into its error messages (e.g. `error at <sui schema>:42:5`); it does not
29/// open any file. The actual SDL bytes come from [`crate::schema::SCHEMA_SDL`].
30const SCHEMA_DIAGNOSTIC_LABEL: &str = "<sui schema>";
31
32/// Diagnostic label for the user's query string passed to `graphql!`. Same
33/// idea as [`SCHEMA_DIAGNOSTIC_LABEL`]: a string that appears in apollo's
34/// error messages, not a file path.
35const QUERY_DIAGNOSTIC_LABEL: &str = "<graphql! input>";
36
37/// `SCHEMA_SDL` parsed and validated into a form that can type-check incoming
38/// queries.
39///
40/// Cached for the lifetime of a single `cargo build`: parsing the full SDL is
41/// non-trivial, and proc macros are loaded once per build, so we parse on
42/// first use and reuse the result for every later `graphql!` call.
43///
44/// `Valid<Schema>` is apollo-compiler's marker that the schema itself is
45/// internally consistent; `ExecutableDocument::parse_and_validate` requires
46/// that proof. The `Result<_, String>` shape is what `LazyLock` needs (the
47/// cell value must be `Sync` and shareable across reads); if SDL parsing ever
48/// fails the cached message is reported at every call site.
49static VALIDATED_SCHEMA: LazyLock<Result<Valid<Schema>, String>> = LazyLock::new(|| {
50    Schema::parse_and_validate(crate::schema::SCHEMA_SDL, SCHEMA_DIAGNOSTIC_LABEL)
51        .map_err(|e| format!("Failed to parse Sui GraphQL schema: {e}"))
52});
53
54pub fn expand(input: TokenStream) -> TokenStream {
55    match expand_impl(input) {
56        Ok(tokens) => tokens.into(),
57        Err(err) => {
58            // Block-wrap with a `&str` tail; `compile_error!{...}` doesn't
59            // parse on its own in expression position.
60            let compile_error = err.to_compile_error();
61            quote!({ #compile_error "" }).into()
62        }
63    }
64}
65
66fn expand_impl(input: TokenStream) -> Result<TokenStream2, syn::Error> {
67    let lit: LitStr = syn::parse(input)?;
68    let source = lit.value();
69
70    let schema = VALIDATED_SCHEMA
71        .as_ref()
72        .map_err(|e| syn::Error::new(proc_macro2::Span::call_site(), e.clone()))?;
73
74    match ExecutableDocument::parse_and_validate(schema, source.as_str(), QUERY_DIAGNOSTIC_LABEL) {
75        Ok(valid) => {
76            let formatted = valid.to_string();
77            Ok(quote!(#formatted))
78        }
79        Err(with_errors) => {
80            let mut combined: Option<syn::Error> = None;
81            for diag in with_errors.errors.iter() {
82                let msg = match diag.line_column_range() {
83                    Some(range) => format!(
84                        "GraphQL [line {}, col {}]: {}",
85                        range.start.line, range.start.column, diag.error
86                    ),
87                    None => format!("GraphQL: {}", diag.error),
88                };
89                let err = syn::Error::new(proc_macro2::Span::call_site(), msg);
90                match &mut combined {
91                    Some(existing) => existing.combine(err),
92                    None => combined = Some(err),
93                }
94            }
95            Err(combined.unwrap_or_else(|| {
96                syn::Error::new(
97                    proc_macro2::Span::call_site(),
98                    "GraphQL validation failed with no diagnostics",
99                )
100            }))
101        }
102    }
103}