1use proc_macro::TokenStream;
25use quote::quote;
26use syn::{parse_macro_input, Data, DeriveInput, Error, Fields, Lit, Meta};
27
28#[proc_macro_derive(RecordKey, attributes(key, key_prefix, link_address))]
63pub fn derive_record_key(input: TokenStream) -> TokenStream {
64 let input = parse_macro_input!(input as DeriveInput);
65
66 match derive_record_key_impl(input) {
67 Ok(tokens) => tokens.into(),
68 Err(err) => err.to_compile_error().into(),
69 }
70}
71
72fn derive_record_key_impl(input: DeriveInput) -> Result<proc_macro2::TokenStream, Error> {
73 let name = &input.ident;
74
75 let data_enum = match &input.data {
77 Data::Enum(data) => data,
78 _ => {
79 return Err(Error::new_spanned(
80 &input,
81 "RecordKey can only be derived for enums",
82 ));
83 }
84 };
85
86 let prefix = get_key_prefix(&input.attrs)?;
88
89 struct VariantData {
91 name: syn::Ident,
92 key: String,
93 link: Option<String>,
94 }
95
96 let mut variants = Vec::new();
97 let mut seen_keys = std::collections::HashSet::new();
98
99 for variant in &data_enum.variants {
100 match &variant.fields {
102 Fields::Unit => {}
103 _ => {
104 return Err(Error::new_spanned(
105 variant,
106 "RecordKey variants must be unit variants (no fields)",
107 ));
108 }
109 }
110
111 let variant_name = &variant.ident;
112 let key = get_variant_key(&variant.attrs, variant_name)?;
113 let link = get_optional_attr(&variant.attrs, "link_address")?;
114
115 let full_key = if let Some(ref p) = prefix {
117 format!("{}{}", p, key)
118 } else {
119 key
120 };
121
122 if !seen_keys.insert(full_key.clone()) {
124 return Err(Error::new_spanned(
125 variant,
126 format!(
127 "Duplicate key \"{}\" - each variant must have a unique key",
128 full_key
129 ),
130 ));
131 }
132
133 variants.push(VariantData {
134 name: variant_name.clone(),
135 key: full_key,
136 link,
137 });
138 }
139
140 let has_link = variants.iter().any(|v| v.link.is_some());
142
143 let as_str_arms = variants.iter().map(|v| {
145 let variant_name = &v.name;
146 let key = &v.key;
147 quote! {
148 Self::#variant_name => #key
149 }
150 });
151
152 let link_impl = if has_link {
154 let arms = variants.iter().map(|v| {
155 let variant_name = &v.name;
156 match &v.link {
157 Some(url) => quote! { Self::#variant_name => Some(#url) },
158 None => quote! { Self::#variant_name => None },
159 }
160 });
161 quote! {
162 #[inline]
163 fn link_address(&self) -> Option<&str> {
164 match self {
165 #(#arms),*
166 }
167 }
168 }
169 } else {
170 quote! {}
171 };
172
173 let expanded = quote! {
175 impl aimdb_core::RecordKey for #name {
176 #[inline]
177 fn as_str(&self) -> &str {
178 match self {
179 #(#as_str_arms),*
180 }
181 }
182
183 #link_impl
184 }
185
186 impl core::borrow::Borrow<str> for #name {
187 #[inline]
188 fn borrow(&self) -> &str {
189 <Self as aimdb_core::RecordKey>::as_str(self)
190 }
191 }
192
193 impl core::hash::Hash for #name {
196 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
197 <Self as aimdb_core::RecordKey>::as_str(self).hash(state);
198 }
199 }
200 };
201
202 Ok(expanded)
203}
204
205fn get_key_prefix(attrs: &[syn::Attribute]) -> Result<Option<String>, Error> {
207 get_optional_attr(attrs, "key_prefix")
208}
209
210fn get_optional_attr(attrs: &[syn::Attribute], attr_name: &str) -> Result<Option<String>, Error> {
212 for attr in attrs {
213 if attr.path().is_ident(attr_name) {
214 let meta = &attr.meta;
215 if let Meta::NameValue(nv) = meta {
216 if let syn::Expr::Lit(expr_lit) = &nv.value {
217 if let Lit::Str(lit_str) = &expr_lit.lit {
218 return Ok(Some(lit_str.value()));
219 }
220 }
221 }
222 return Err(Error::new_spanned(
223 attr,
224 format!("Expected #[{} = \"...\"]", attr_name),
225 ));
226 }
227 }
228 Ok(None)
229}
230
231fn get_variant_key(attrs: &[syn::Attribute], variant_name: &syn::Ident) -> Result<String, Error> {
233 for attr in attrs {
234 if attr.path().is_ident("key") {
235 let meta = &attr.meta;
236 if let Meta::NameValue(nv) = meta {
237 if let syn::Expr::Lit(expr_lit) = &nv.value {
238 if let Lit::Str(lit_str) = &expr_lit.lit {
239 return Ok(lit_str.value());
240 }
241 }
242 }
243 return Err(Error::new_spanned(attr, "Expected #[key = \"...\"]"));
244 }
245 }
246
247 Err(Error::new_spanned(
248 variant_name,
249 format!(
250 "Missing #[key = \"...\"] attribute on variant `{}`",
251 variant_name
252 ),
253 ))
254}
255
256#[cfg(test)]
257mod tests {
258 }