1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
2
3use proc_macro::TokenStream;
4use quote::{ToTokens, quote};
5use syn::{
6 Error, FnArg, Result, Token, Variant, parenthesized, parse::Parser, punctuated::Punctuated,
7 spanned::Spanned,
8};
9
10const FUNC_ATTR: &str = "func";
11const ASSOC_ATTR: &str = "assoc";
12
13#[proc_macro_derive(Assoc, attributes(func, assoc))]
14pub fn derive_assoc(input: TokenStream) -> TokenStream {
15 impl_macro(&syn::parse(input).expect("Failed to parse macro input"))
16 .unwrap_or_else(syn::Error::into_compile_error)
18 .into()
19}
20
21fn impl_macro(ast: &syn::DeriveInput) -> Result<proc_macro2::TokenStream> {
22 let name = &ast.ident;
23 let generics = &ast.generics;
24 let generic_params = &generics.params;
25 let fns = ast
26 .attrs
27 .iter()
28 .filter(|attr| attr.path().is_ident(FUNC_ATTR))
29 .map(|attr| syn::parse2::<DeriveFuncs>(attr.meta.to_token_stream()))
30 .collect::<Result<Vec<DeriveFuncs>>>()?;
31 let variants: Vec<&Variant> = if let syn::Data::Enum(data) = &ast.data {
32 data.variants.iter().collect()
33 } else {
34 panic!("#[derive(Assoc)] only applicable to enums")
35 };
36 let functions: Vec<proc_macro2::TokenStream> = fns
37 .into_iter()
38 .flat_map(|DeriveFuncs(funcs)| {
39 funcs
40 .iter()
41 .map(|func| build_function(&variants, func, funcs.clone()))
42 .collect::<Vec<_>>()
43 })
44 .collect::<Result<Vec<proc_macro2::TokenStream>>>()?;
45 Ok(quote! {
46 #[allow(clippy::used_underscore_binding)]
47 impl <#generic_params> #name #generics
48 {
49 #(#functions)*
50 }
51 })
52}
53
54fn build_function(
55 variants: &[&Variant],
56 func: &DeriveFunc,
57 associated_funcs: Vec<DeriveFunc>,
58) -> Result<proc_macro2::TokenStream> {
59 let vis = &func.vis;
60 let sig = &func.sig;
61 let has_self = match func.sig.inputs.first() {
63 Some(FnArg::Receiver(_)) => true,
64 Some(FnArg::Typed(pat_type)) => {
65 let pat = &pat_type.pat;
66 quote!(#pat).to_string().trim() == "self"
67 }
68 None => false,
69 };
70 let is_option = if let syn::ReturnType::Type(_, ty) = &func.sig.output {
71 let s = quote!(#ty).to_string();
72 let trimmed = s.trim();
73 trimmed.starts_with("Option") && trimmed.len() > 6 && trimmed[6..].trim().starts_with("<")
74 } else {
75 false
76 };
77 let mut arms = variants
78 .iter()
79 .map(|variant| {
80 build_variant_arm(
81 variant,
82 &func.sig.ident,
83 associated_funcs.iter().map(|func| func.sig.ident.clone()),
84 is_option,
85 has_self,
86 &func.def,
87 )
88 })
89 .collect::<Result<Vec<(proc_macro2::TokenStream, Wildcard)>>>()?;
90 if is_option
91 && !arms
92 .iter()
93 .any(|(_, wildcard)| matches!(wildcard, Wildcard::True))
94 {
95 arms.push((quote!(_ => None,), Wildcard::True))
96 }
97 if !has_self {
99 arms.sort_by(|(_, wildcard1), (_, wildcard2)| wildcard1.cmp(wildcard2));
100 }
101 let arms = arms.into_iter().map(|(toks, _)| toks);
102 let match_on = if has_self {
103 quote!(self)
104 } else if func.sig.inputs.is_empty() {
105 return Err(syn::Error::new(func.span, "Missing parameter"));
106 } else {
107 let mut result = quote!();
108 for input in &func.sig.inputs {
109 match input {
110 FnArg::Receiver(_) => {
111 result = quote!(self);
112 break;
113 }
114 FnArg::Typed(pat_type) => {
115 let pat = &pat_type.pat;
116 result = if result.is_empty() {
117 quote!(#pat)
118 } else {
119 quote!(#result, #pat)
120 };
121 }
122 }
123 }
124 if func.sig.inputs.len() > 1 {
125 result = quote!((#result));
126 }
127 result
128 };
129 Ok(quote! {
130 #vis #sig
131 {
132 match #match_on
133 {
134 #(#arms)*
135 }
136 }
137 })
138}
139
140fn build_variant_arm(
141 variant: &Variant,
142 func: &syn::Ident,
143 mut assoc_funcs: impl Iterator<Item = syn::Ident>,
144 is_option: bool,
145 has_self: bool,
146 def: &Option<proc_macro2::TokenStream>,
147) -> Result<(proc_macro2::TokenStream, Wildcard)> {
148 let assocs = Association::get_variant_assocs(variant, !has_self).filter(|assoc| {
150 assoc.func == *func || assoc_funcs.any(|assoc_func| assoc_func == assoc.func)
151 });
152 if has_self {
153 build_fwd_assoc(assocs, variant, is_option, func, def)
154 } else {
155 build_rev_assoc(assocs, variant, is_option)
156 }
157}
158
159fn build_fwd_assoc(
160 assocs: impl Iterator<Item = Association>,
161 variant: &Variant,
162 is_option: bool,
163 func_ident: &syn::Ident,
164 def: &Option<proc_macro2::TokenStream>,
165) -> Result<(proc_macro2::TokenStream, Wildcard)> {
166 let var_ident = &variant.ident;
167 let fields = match &variant.fields {
168 syn::Fields::Named(fields) => {
169 let named = fields
170 .named
171 .iter()
172 .map(|f| {
173 let ident = &f.ident;
174 let val: &Option<proc_macro2::Ident> = &f.ident.as_ref().map(|s| {
175 proc_macro2::Ident::new(&("_".to_string() + &s.to_string()), f.span())
176 });
177 quote!(#ident: #val)
178 })
179 .collect::<Vec<proc_macro2::TokenStream>>();
180 quote!({#(#named),*})
181 }
182 syn::Fields::Unnamed(fields) => {
183 let unnamed = fields
184 .unnamed
185 .iter()
186 .enumerate()
187 .map(|(i, f)| {
188 let ident =
189 proc_macro2::Ident::new(&("_".to_string() + &i.to_string()), f.span());
190 quote!(#ident)
191 })
192 .collect::<Vec<proc_macro2::TokenStream>>();
193 quote!((#(#unnamed),*))
194 }
195 _ => quote!(),
196 };
197 let assocs = assocs
198 .filter_map(|assoc| {
199 if let AssociationType::Forward(expr) = assoc.assoc {
200 Some(Ok(expr))
201 } else {
202 None
203 }
204 })
205 .collect::<Result<Vec<syn::Expr>>>()?;
206 match assocs.len() {
207 0 => {
208 if let Some(tokens) = def {
209 Ok(quote! { Self::#var_ident #fields => #tokens, })
210 } else if is_option {
211 Ok(quote! { Self::#var_ident #fields => None, })
212 } else {
213 Err(Error::new_spanned(
214 variant,
215 format!("Missing `assoc` attribute for {}", func_ident),
216 ))
217 }
218 }
219 1 => {
220 let val = &assocs[0];
221 if is_option {
222 if quote!(#val).to_string().trim() == "None" {
223 Ok(quote! { Self::#var_ident #fields => #val, })
224 } else {
225 Ok(quote! { Self::#var_ident #fields => Some(#val), })
226 }
227 } else {
228 Ok(quote! { Self::#var_ident #fields => #val, })
229 }
230 }
231 _ => Err(Error::new_spanned(
232 variant,
233 format!("Too many `assoc` attributes for {}", func_ident),
234 )),
235 }
236 .map(|toks| (toks, Wildcard::None))
237}
238
239fn build_rev_assoc(
240 assocs: impl Iterator<Item = Association>,
241 variant: &Variant,
242 is_option: bool,
243) -> Result<(proc_macro2::TokenStream, Wildcard)> {
244 let var_ident = &variant.ident;
245 let assocs = assocs
246 .filter_map(|assoc| {
247 if let AssociationType::Reverse(pat) = assoc.assoc {
248 Some(Ok(pat))
249 } else {
250 None
251 }
252 })
253 .collect::<Result<Vec<syn::Pat>>>()?;
254 let mut concrete_pats: Vec<proc_macro2::TokenStream> = Vec::new();
255 let mut wildcard_pat: Option<proc_macro2::TokenStream> = None;
256 let mut wildcard_status = Wildcard::False;
257 for pat in assocs.iter() {
258 if !matches!(variant.fields, syn::Fields::Unit) {
259 return Err(Error::new_spanned(
260 variant,
261 "Reverse associations not allowed for tuple or struct-like variants",
262 ));
263 }
264 let arm = if is_option {
265 quote!(#pat => Some(Self::#var_ident),)
266 } else {
267 quote!(#pat => Self::#var_ident,)
268 };
269 if matches!(pat, syn::Pat::Wild(_)) {
270 if wildcard_pat.is_some() {
271 return Err(syn::Error::new_spanned(
272 pat,
273 "Only 1 wildcard allowed per reverse association",
274 ));
275 }
276 wildcard_status = Wildcard::True;
277 wildcard_pat = Some(arm);
278 } else {
279 concrete_pats.push(arm);
280 }
281 }
282 if let Some(wildcard_pat) = wildcard_pat {
283 concrete_pats.push(wildcard_pat)
284 }
285 Ok((quote!(#(#concrete_pats) *), wildcard_status))
286}
287
288#[derive(Clone)]
292struct DeriveFunc {
293 vis: syn::Visibility,
294 sig: syn::Signature,
295 span: proc_macro2::Span,
296 def: Option<proc_macro2::TokenStream>,
297}
298
299struct Association {
302 func: syn::Ident,
303 assoc: AssociationType,
304}
305
306enum AssociationType {
307 Forward(syn::Expr),
308 Reverse(syn::Pat),
309}
310
311#[derive(PartialEq, Eq, PartialOrd, Ord)]
316enum Wildcard {
317 False = 0,
318 None = 1,
319 True = 2,
320}
321
322impl syn::parse::Parse for DeriveFunc {
323 fn parse(input: syn::parse::ParseStream) -> Result<Self> {
325 let vis = input.parse::<syn::Visibility>()?;
326 let sig = input.parse::<syn::Signature>()?;
327 let def = if let Ok(block) = input.parse::<syn::Block>() {
328 Some(proc_macro2::TokenStream::from(ToTokens::into_token_stream(
329 block,
330 )))
331 } else {
332 None
333 };
334 Ok(DeriveFunc {
335 vis,
336 sig,
337 span: input.span(),
338 def,
339 })
340 }
341}
342
343struct DeriveFuncs(Vec<DeriveFunc>);
344impl syn::parse::Parse for DeriveFuncs {
345 fn parse(input: syn::parse::ParseStream) -> Result<Self> {
347 input.step(|cursor| {
348 if let Some((_, next)) = cursor.token_tree() {
349 Ok(((), next))
350 } else {
351 Err(cursor.error("Missing function signature"))
352 }
353 })?;
354 let content;
355 parenthesized!(content in input);
356 Ok(Self(
357 content
358 .parse_terminated(DeriveFunc::parse, Token!(,))
359 .map(|parsed| parsed.into_iter().collect())?,
360 ))
361 }
362}
363
364struct ForwardAssocTokens(syn::Ident, syn::Expr);
366impl syn::parse::Parse for ForwardAssocTokens {
367 fn parse(input: syn::parse::ParseStream) -> Result<Self> {
368 let ident = input.parse()?;
369 input.parse::<syn::Token!(=)>()?;
370 let expr = input.parse()?;
371 Ok(Self(ident, expr))
372 }
373}
374
375struct ReverseAssocTokens(syn::Ident, syn::Pat);
377impl syn::parse::Parse for ReverseAssocTokens {
378 fn parse(input: syn::parse::ParseStream) -> Result<Self> {
379 let ident = input.parse()?;
380 input.parse::<syn::Token!(=)>()?;
381 let pat = syn::Pat::parse_multi_with_leading_vert(input)?;
382 Ok(Self(ident, pat))
383 }
384}
385
386impl From<ForwardAssocTokens> for Association {
387 fn from(val: ForwardAssocTokens) -> Self {
388 Association {
389 func: val.0,
390 assoc: AssociationType::Forward(val.1),
391 }
392 }
393}
394
395impl From<ReverseAssocTokens> for Association {
396 fn from(val: ReverseAssocTokens) -> Self {
397 Association {
398 func: val.0,
399 assoc: AssociationType::Reverse(val.1),
400 }
401 }
402}
403
404impl Association {
405 fn get_variant_assocs(variant: &Variant, is_reverse: bool) -> impl Iterator<Item = Self> + '_ {
406 variant
407 .attrs
408 .iter()
409 .filter(|assoc_attr| assoc_attr.path().is_ident(ASSOC_ATTR))
410 .filter_map(move |attr| {
411 if let syn::Meta::List(meta_list) = &attr.meta {
412 if is_reverse {
413 let parser = Punctuated::<ReverseAssocTokens, Token![,]>::parse_terminated;
414 parser
415 .parse2(meta_list.tokens.clone())
416 .map(|tokens| {
417 tokens
418 .into_iter()
419 .map(|tokens| tokens.into())
420 .collect::<Vec<Self>>()
421 })
422 .ok()
423 } else {
424 let parser = Punctuated::<ForwardAssocTokens, Token![,]>::parse_terminated;
425 parser
426 .parse2(meta_list.tokens.clone())
427 .map(|tokens| {
428 tokens
429 .into_iter()
430 .map(|tokens| tokens.into())
431 .collect::<Vec<Self>>()
432 })
433 .ok()
434 }
435 } else {
436 None
437 }
438 })
439 .flatten()
440 }
441}