use proc_macro::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
use syn::{Data, DeriveInput, Fields, Ident, LitStr, Path, parse_macro_input};
#[derive(Default)]
struct Attrs {
rename: Option<LitStr>,
json: bool,
crate_path: Option<Path>,
}
fn parse_attrs(attrs: &[syn::Attribute]) -> syn::Result<Attrs> {
let mut parsed = Attrs::default();
for attr in attrs.iter().filter(|a| a.path().is_ident("pylon")) {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("json") {
parsed.json = true;
Ok(())
} else if meta.path.is_ident("rename") {
parsed.rename = Some(meta.value()?.parse()?);
Ok(())
} else if meta.path.is_ident("crate_path") {
parsed.crate_path = Some(meta.value()?.parse()?);
Ok(())
} else {
Err(meta.error("unknown `pylon` attribute; expected `json`, `rename` or `crate_path`"))
}
})?;
}
Ok(parsed)
}
#[proc_macro_derive(Queryable, attributes(pylon))]
pub fn queryable(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand(input).unwrap_or_else(|error| error.to_compile_error()).into()
}
fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let container = parse_attrs(&input.attrs)?;
let crate_path = container
.crate_path
.clone()
.unwrap_or_else(|| syn::parse_quote!(::pylon_client));
let name = &input.ident;
let name_literal = name.to_string();
let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
let body = if container.json {
quote! { #crate_path::queryable::derive::from_json(value, #name_literal) }
} else {
match &input.data {
Data::Struct(data) => struct_body(&crate_path, &name_literal, &data.fields)?,
Data::Enum(data) => enum_body(&crate_path, name, &name_literal, data)?,
Data::Union(_) => {
return Err(syn::Error::new(
input.span(),
"Queryable cannot be derived for a union; use a struct or an enum",
));
}
}
};
Ok(quote! {
impl #impl_generics #crate_path::Queryable for #name #type_generics #where_clause {
fn decode(
value: &#crate_path::Value,
) -> ::core::result::Result<Self, #crate_path::DecodeError> {
#body
}
}
})
}
fn struct_body(crate_path: &Path, name_literal: &str, fields: &Fields) -> syn::Result<proc_macro2::TokenStream> {
let Fields::Named(named) = fields else {
return Err(syn::Error::new(
fields.span(),
"Queryable needs named fields — a query shape is a set of named pointers, \
so a tuple or unit struct has nothing to match them against",
));
};
let mut initializers = Vec::new();
for field in &named.named {
let attrs = parse_attrs(&field.attrs)?;
let ident = field.ident.as_ref().expect("named field has an identifier");
let shape_name = attrs
.rename
.as_ref()
.map(LitStr::value)
.unwrap_or_else(|| ident.to_string());
let helper = if attrs.json { quote!(json_field) } else { quote!(field) };
initializers.push(quote! {
#ident: #crate_path::queryable::derive::#helper(object, #name_literal, #shape_name)?
});
}
Ok(quote! {
let object = #crate_path::queryable::derive::object(value, #name_literal)?;
::core::result::Result::Ok(Self { #(#initializers),* })
})
}
fn enum_body(
crate_path: &Path,
name: &Ident,
name_literal: &str,
data: &syn::DataEnum,
) -> syn::Result<proc_macro2::TokenStream> {
let mut arms = Vec::new();
for variant in &data.variants {
if !matches!(variant.fields, Fields::Unit) {
return Err(syn::Error::new(
variant.span(),
"Queryable only supports unit variants — a schema enum's members carry no payload",
));
}
let attrs = parse_attrs(&variant.attrs)?;
if attrs.json {
return Err(syn::Error::new(
variant.span(),
"`json` is not valid on a variant; put it on the container or on a struct field",
));
}
let ident = &variant.ident;
let label = attrs
.rename
.as_ref()
.map(LitStr::value)
.unwrap_or_else(|| ident.to_string());
arms.push(quote! { #label => ::core::result::Result::Ok(#name::#ident) });
}
Ok(quote! {
match #crate_path::queryable::derive::enum_label(value, #name_literal)? {
#(#arms,)*
other => ::core::result::Result::Err(
#crate_path::queryable::derive::unknown_variant(#name_literal, other),
),
}
})
}