sui_graphql_macros/
query.rs1use std::path::PathBuf;
17use std::sync::LazyLock;
18
19use bluejay_parser::Error as BluejayError;
20use bluejay_parser::ast::Parse as _;
21use bluejay_parser::ast::definition::DefinitionDocument;
22use bluejay_parser::ast::definition::SchemaDefinition;
23use bluejay_parser::ast::executable::ExecutableDocument;
24use bluejay_parser::error::Location;
25use bluejay_printer::executable::ExecutableDocumentPrinter;
26use bluejay_validator::definition::BuiltinRulesValidator as SchemaValidator;
27use bluejay_validator::executable::Cache;
28use bluejay_validator::executable::document::BuiltinRulesValidator as ExecutableValidator;
29use proc_macro::TokenStream;
30use proc_macro2::TokenStream as TokenStream2;
31use quote::quote;
32use syn::LitStr;
33use syn::Token;
34use syn::parse::Parse;
35use syn::parse::ParseStream;
36use syn::parse::Parser;
37use syn::punctuated::Punctuated;
38
39const SCHEMA_DIAGNOSTIC_LABEL: &str = "<sui schema>";
43
44static SCHEMA_DOCUMENT: LazyLock<Result<DefinitionDocument<'static>, String>> =
51 LazyLock::new(|| {
52 DefinitionDocument::parse(crate::schema::SCHEMA_SDL)
53 .map_err(|errors| format_schema_errors("parse", errors))
54 });
55
56static VALIDATED_SCHEMA: LazyLock<Result<SchemaDefinition<'static>, String>> =
58 LazyLock::new(|| {
59 let document = SCHEMA_DOCUMENT.as_ref().map_err(Clone::clone)?;
60 let schema = SchemaDefinition::try_from(document)
61 .map_err(|errors| format_schema_errors("resolve", errors))?;
62 let errors = SchemaValidator::validate(&schema).collect::<Vec<_>>();
63
64 if errors.is_empty() {
65 Ok(schema)
66 } else {
67 Err(format_schema_errors("validate", errors))
68 }
69 });
70
71enum Source {
73 Inline(LitStr),
74 File(LitStr),
75}
76
77impl Parse for Source {
78 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
79 if input.peek(Token![@]) {
80 input.parse::<Token![@]>()?;
81 input.parse().map(Self::File)
82 } else {
83 input.parse().map(Self::Inline)
84 }
85 }
86}
87
88pub fn expand(input: TokenStream) -> TokenStream {
89 match expand_impl(input) {
90 Ok(tokens) => tokens.into(),
91 Err(err) => {
92 let compile_error = err.to_compile_error();
95 quote!({ #compile_error "" }).into()
96 }
97 }
98}
99
100fn combine_query_errors<E>(source: &str, errors: impl IntoIterator<Item = E>) -> Option<syn::Error>
101where
102 E: Into<BluejayError>,
103{
104 let mut combined: Option<syn::Error> = None;
105
106 for error in errors {
107 let error = error.into();
108 let message = error.message().to_owned();
109 let graph_errors = BluejayError::into_graphql_errors(source, [error]);
110 let graph_error = graph_errors.first();
111
112 let message = match graph_error {
116 Some(error) if message == "Parse error" => error.message.to_string(),
117 _ => message,
118 };
119
120 let message = if let Some(Location { line, col }) =
121 graph_error.and_then(|error| error.locations.first())
122 {
123 format!("GraphQL [line {}, col {}]: {message}", line, col)
124 } else {
125 format!("GraphQL: {message}")
126 };
127
128 let error = syn::Error::new(proc_macro2::Span::call_site(), message);
129 if let Some(existing) = &mut combined {
130 existing.combine(error);
131 } else {
132 combined = Some(error);
133 }
134 }
135
136 combined
137}
138
139fn expand_impl(input: TokenStream) -> Result<TokenStream2, syn::Error> {
140 let sources = Punctuated::<Source, Token![,]>::parse_terminated.parse(input)?;
141 if sources.is_empty() {
142 return Err(syn::Error::new(
143 proc_macro2::Span::call_site(),
144 "expected at least one GraphQL source",
145 ));
146 }
147
148 let mut query = String::new();
153 let mut dependencies = Vec::new();
154 for source in sources {
155 match source {
156 Source::Inline(literal) => query.push_str(&literal.value()),
157 Source::File(literal) => {
158 let path = source_relative(&literal)?;
159 let contents = std::fs::read_to_string(&path).map_err(|error| {
160 syn::Error::new(
161 literal.span(),
162 format!(
163 "failed to read GraphQL source from '{}': {error}",
164 path.display()
165 ),
166 )
167 })?;
168
169 query.push_str(&contents);
170 query.push('\n');
171
172 dependencies.push(literal);
173 }
174 }
175 }
176
177 let schema = VALIDATED_SCHEMA
178 .as_ref()
179 .map_err(|error| syn::Error::new(proc_macro2::Span::call_site(), error.clone()))?;
180
181 let document = ExecutableDocument::parse(query.as_str()).map_err(|errors| {
182 combine_query_errors(query.as_str(), errors).unwrap_or_else(|| {
183 syn::Error::new(
184 proc_macro2::Span::call_site(),
185 "GraphQL parsing failed with no diagnostics",
186 )
187 })
188 })?;
189
190 let cache = Cache::new(&document, schema);
191 if let Some(error) = combine_query_errors(
192 query.as_str(),
193 ExecutableValidator::validate(&document, schema, &cache),
194 ) {
195 return Err(error);
196 }
197
198 let formatted = ExecutableDocumentPrinter::to_string(&document);
199 Ok(quote!({
202 #(const _: &str = ::core::include_str!(#dependencies);)*
203 #formatted
204 }))
205}
206
207fn format_schema_errors<E>(action: &str, errors: impl IntoIterator<Item = E>) -> String
208where
209 E: Into<BluejayError>,
210{
211 let formatted = BluejayError::format_errors(
212 crate::schema::SCHEMA_SDL,
213 Some(SCHEMA_DIAGNOSTIC_LABEL),
214 errors,
215 );
216
217 format!("Failed to {action} Sui GraphQL schema:\n{formatted}")
218}
219
220fn source_relative(literal: &LitStr) -> Result<PathBuf, syn::Error> {
222 let path = PathBuf::from(literal.value());
223 if path.is_absolute() {
224 return Ok(path);
225 }
226
227 let source = literal.span().local_file();
228 let Some(source_dir) = source.as_ref().and_then(|file| file.parent()) else {
229 return Err(syn::Error::new(
230 literal.span(),
231 "cannot resolve GraphQL path relative to source file",
232 ));
233 };
234
235 Ok(source_dir.join(path))
236}