1use proc_macro::TokenStream;
33use quote::quote;
34use syn::spanned::Spanned;
35use syn::{Data, DeriveInput, Fields, Ident, LitStr, Path, parse_macro_input};
36
37#[derive(Default)]
39struct Attrs {
40 rename: Option<LitStr>,
41 json: bool,
42 crate_path: Option<Path>,
43}
44
45fn parse_attrs(attrs: &[syn::Attribute]) -> syn::Result<Attrs> {
46 let mut parsed = Attrs::default();
47 for attr in attrs.iter().filter(|a| a.path().is_ident("pylon")) {
48 attr.parse_nested_meta(|meta| {
49 if meta.path.is_ident("json") {
50 parsed.json = true;
51 Ok(())
52 } else if meta.path.is_ident("rename") {
53 parsed.rename = Some(meta.value()?.parse()?);
54 Ok(())
55 } else if meta.path.is_ident("crate_path") {
56 parsed.crate_path = Some(meta.value()?.parse()?);
57 Ok(())
58 } else {
59 Err(meta.error("unknown `pylon` attribute; expected `json`, `rename` or `crate_path`"))
60 }
61 })?;
62 }
63 Ok(parsed)
64}
65
66#[proc_macro_derive(Queryable, attributes(pylon))]
67pub fn queryable(input: TokenStream) -> TokenStream {
68 let input = parse_macro_input!(input as DeriveInput);
69 expand(input).unwrap_or_else(|error| error.to_compile_error()).into()
70}
71
72fn expand(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
73 let container = parse_attrs(&input.attrs)?;
74 let crate_path = container
75 .crate_path
76 .clone()
77 .unwrap_or_else(|| syn::parse_quote!(::pylon_client));
78 let name = &input.ident;
79 let name_literal = name.to_string();
80 let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
81
82 let body = if container.json {
86 quote! { #crate_path::queryable::derive::from_json(value, #name_literal) }
87 } else {
88 match &input.data {
89 Data::Struct(data) => struct_body(&crate_path, &name_literal, &data.fields)?,
90 Data::Enum(data) => enum_body(&crate_path, name, &name_literal, data)?,
91 Data::Union(_) => {
92 return Err(syn::Error::new(
93 input.span(),
94 "Queryable cannot be derived for a union; use a struct or an enum",
95 ));
96 }
97 }
98 };
99
100 Ok(quote! {
101 impl #impl_generics #crate_path::Queryable for #name #type_generics #where_clause {
102 fn decode(
103 value: &#crate_path::Value,
104 ) -> ::core::result::Result<Self, #crate_path::DecodeError> {
105 #body
106 }
107 }
108 })
109}
110
111fn struct_body(crate_path: &Path, name_literal: &str, fields: &Fields) -> syn::Result<proc_macro2::TokenStream> {
112 let Fields::Named(named) = fields else {
113 return Err(syn::Error::new(
114 fields.span(),
115 "Queryable needs named fields — a query shape is a set of named pointers, \
116 so a tuple or unit struct has nothing to match them against",
117 ));
118 };
119
120 let mut initializers = Vec::new();
121 for field in &named.named {
122 let attrs = parse_attrs(&field.attrs)?;
123 let ident = field.ident.as_ref().expect("named field has an identifier");
125 let shape_name = attrs
126 .rename
127 .as_ref()
128 .map(LitStr::value)
129 .unwrap_or_else(|| ident.to_string());
130 let helper = if attrs.json { quote!(json_field) } else { quote!(field) };
131 initializers.push(quote! {
132 #ident: #crate_path::queryable::derive::#helper(object, #name_literal, #shape_name)?
133 });
134 }
135
136 Ok(quote! {
137 let object = #crate_path::queryable::derive::object(value, #name_literal)?;
138 ::core::result::Result::Ok(Self { #(#initializers),* })
139 })
140}
141
142fn enum_body(
143 crate_path: &Path,
144 name: &Ident,
145 name_literal: &str,
146 data: &syn::DataEnum,
147) -> syn::Result<proc_macro2::TokenStream> {
148 let mut arms = Vec::new();
149 for variant in &data.variants {
150 if !matches!(variant.fields, Fields::Unit) {
151 return Err(syn::Error::new(
152 variant.span(),
153 "Queryable only supports unit variants — a schema enum's members carry no payload",
154 ));
155 }
156 let attrs = parse_attrs(&variant.attrs)?;
157 if attrs.json {
158 return Err(syn::Error::new(
159 variant.span(),
160 "`json` is not valid on a variant; put it on the container or on a struct field",
161 ));
162 }
163 let ident = &variant.ident;
164 let label = attrs
165 .rename
166 .as_ref()
167 .map(LitStr::value)
168 .unwrap_or_else(|| ident.to_string());
169 arms.push(quote! { #label => ::core::result::Result::Ok(#name::#ident) });
170 }
171
172 Ok(quote! {
173 match #crate_path::queryable::derive::enum_label(value, #name_literal)? {
174 #(#arms,)*
175 other => ::core::result::Result::Err(
176 #crate_path::queryable::derive::unknown_variant(#name_literal, other),
177 ),
178 }
179 })
180}