gelx_macros/
lib.rs

1#![doc(html_logo_url = "https://raw.githubusercontent.com/ifiokjr/gelx/main/setup/assets/logo.png")]
2
3use gelx_core::GelxMetadata;
4use gelx_core::generate_query_token_stream;
5use gelx_core::get_descriptor_sync;
6use gelx_core::resolve_path;
7use proc_macro::TokenStream;
8use proc_macro2::Span;
9use quote::ToTokens;
10use syn::Token;
11use syn::parse::Parse;
12use syn::parse_macro_input;
13
14/// Generates a query module from a query string.
15///
16/// It supports inline code:
17///
18/// ```ignore
19/// use gelx_macros::gelx_raw;
20///
21/// gelx_raw!(get_users, query: "select User {**}");
22/// ```
23///
24/// It also supports file-based queries:
25///
26/// ```ignore
27/// use gelx_macros::gelx_raw;
28///
29/// gelx_raw!(insert_user, file: "../gelx/queries/insert_user.edgeql");
30/// ```
31#[proc_macro]
32pub fn gelx_raw(input: TokenStream) -> TokenStream {
33	parse_macro_input!(input as GelQueryInput)
34		.to_token_stream()
35		.into()
36}
37
38pub(crate) struct GelQueryInput {
39	pub(crate) module: syn::Ident,
40	pub(crate) query: String,
41}
42
43impl Parse for GelQueryInput {
44	fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
45		let module: syn::Ident = input.parse()?;
46
47		let query_content = if input.peek(Token![,]) {
48			input.parse::<Token![,]>()?;
49
50			let marker: syn::Ident = input.parse()?;
51			input.parse::<Token![:]>()?;
52			let raw_content: syn::LitStr = input.parse()?;
53
54			if marker == "file" {
55				QueryContent::File(raw_content.value(), raw_content.span())
56			} else if marker == "query" {
57				QueryContent::Query(raw_content.value())
58			} else {
59				let message = format!("unexpected marker token: {marker}");
60				return Err(syn::Error::new_spanned(marker, message));
61			}
62		} else {
63			QueryContent::File(format!("queries/{module}.edgeql"), module.span())
64		};
65
66		let query = query_content.resolve()?;
67		Ok(Self { module, query })
68	}
69}
70
71impl ToTokens for GelQueryInput {
72	fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
73		let metadata: GelxMetadata = std::env::var("GELX_METADATA_BASE64")
74			.ok()
75			.and_then(|value| GelxMetadata::try_from_base64(value).ok())
76			.unwrap_or_default();
77		let module_name = self.module.to_string();
78		let token_stream = get_descriptor_sync(&self.query, &metadata)
79			.and_then(|descriptor| {
80				generate_query_token_stream(&descriptor, &module_name, &self.query, &metadata, true)
81			})
82			.unwrap_or_else(|error| syn::Error::from(error).to_compile_error());
83
84		tokens.extend(token_stream);
85	}
86}
87
88#[derive(Debug)]
89pub(crate) enum QueryContent {
90	Query(String),
91	File(String, Span),
92}
93
94impl QueryContent {
95	pub fn resolve(self) -> syn::Result<String> {
96		match self {
97			QueryContent::Query(query) => Ok(query),
98			QueryContent::File(relative_path, span) => {
99				let path = resolve_path(relative_path, span)?;
100
101				std::fs::read_to_string(&path)
102					.map_err(|error| {
103						syn::Error::new(
104							span,
105							format!("failed to read query file at {}: {}", path.display(), error),
106						)
107					})
108					.map(|value| value.trim().to_string())
109			}
110		}
111	}
112}