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}