rematch/
lib.rs

1use proc_macro2::{Ident, Span, TokenStream};
2use proc_macro_error::{abort, proc_macro_error};
3use quote::{quote, ToTokens};
4use syn::Fields;
5
6#[proc_macro_attribute]
7#[proc_macro_error]
8pub fn rematch(
9    attr: proc_macro::TokenStream,
10    item: proc_macro::TokenStream,
11) -> proc_macro::TokenStream {
12    parse(attr.into(), item.into()).to_token_stream().into()
13}
14
15enum Parsed {
16    Enum {
17        item: syn::ItemEnum,
18        attrs_per_variant: Vec<Vec<Attribute>>,
19    },
20    Struct {
21        attr: Attribute,
22        item: syn::ItemStruct,
23    },
24}
25
26#[derive(Clone, Debug)]
27enum Attribute {
28    Regex(TokenStream),
29}
30
31fn generate_fields(caps_ident: &Ident, fields: &Fields) -> TokenStream {
32    match fields {
33        Fields::Named(named) => {
34            let fields = named
35                .named
36                .iter()
37                .enumerate()
38                .map(|(idx, field)| {
39                    let ident = field
40                        .ident
41                        .as_ref()
42                        .expect("named field should have a name");
43                    let ident_str = ident.to_string();
44                    let ty = &field.ty;
45                    quote! {
46                        #ident: #caps_ident.get(1 + #idx)
47                        .ok_or_else(|| anyhow::anyhow!("Getting group {} failed", 1 + #idx))?
48                        .as_str()
49                        .parse::<#ty>()
50                        .map_err(|e| anyhow::anyhow!("Field '{}' parsing error: {}", #ident_str, e))?,
51                    }
52                })
53                .collect::<TokenStream>();
54            quote! {
55                {
56                    #fields
57                }
58            }
59        }
60        Fields::Unnamed(unnamed) => {
61            let fields = unnamed
62                .unnamed
63                .iter()
64                .enumerate()
65                .map(|(idx, field)| {
66                    let ty = &field.ty;
67                    quote! {
68                        #caps_ident.get(1 + #idx)
69                        .ok_or_else(|| anyhow::anyhow!("Getting group {} failed", 1 + #idx))?
70                        .as_str()
71                        .parse::<#ty>()
72                        .map_err(|e| anyhow::anyhow!("Field {} parsing error: {}", #idx, e))?,
73                    }
74                })
75                .collect::<TokenStream>();
76            quote! {
77                (
78                    #fields
79                )
80            }
81        }
82        Fields::Unit => {
83            quote!()
84        }
85    }
86}
87
88fn generate_fields_matching(
89    re: &TokenStream,
90    re_ident: &Ident,
91    self_ident: TokenStream,
92    fields: &Fields,
93) -> TokenStream {
94    let caps_ident = Ident::new("caps", Span::call_site());
95    let fields = generate_fields(&caps_ident, fields);
96
97    quote! {
98        lazy_static::lazy_static! {
99            static ref #re_ident: regex::Regex = regex::Regex::new(#re).unwrap();
100        }
101
102        if let Some(#caps_ident) = #re_ident.captures(s) {
103            return Ok(#self_ident #fields);
104        }
105    }
106}
107
108fn generate_enum_matching<'a>(
109    variants: impl IntoIterator<Item = &'a syn::Variant>,
110    attrs_per_variant: &'a [Vec<Attribute>],
111) -> TokenStream {
112    variants
113        .into_iter()
114        .zip(attrs_per_variant.iter())
115        .flat_map(|(variant, attrs)| {
116            attrs.iter().enumerate().map(|(idx, attr)| {
117                let Attribute::Regex(re) = attr;
118                let re_ident =
119                    Ident::new(&format!("RE_{}_{}", variant.ident, idx), Span::call_site());
120                let variant_ident = &variant.ident;
121                let self_ident = quote! { Self::#variant_ident };
122                generate_fields_matching(re, &re_ident, self_ident, &variant.fields)
123            })
124        })
125        .collect()
126}
127
128impl ToTokens for Parsed {
129    fn to_tokens(&self, tokens: &mut TokenStream) {
130        let (name, matching) = match self {
131            Parsed::Enum {
132                item,
133                attrs_per_variant,
134            } => {
135                item.to_tokens(tokens);
136                (
137                    &item.ident,
138                    generate_enum_matching(&item.variants, attrs_per_variant),
139                )
140            }
141            Parsed::Struct {
142                attr: Attribute::Regex(re),
143                item,
144            } => {
145                item.to_tokens(tokens);
146                let re_ident = Ident::new("RE", Span::call_site());
147                (
148                    &item.ident,
149                    generate_fields_matching(re, &re_ident, quote! { Self }, &item.fields),
150                )
151            }
152        };
153        quote! {
154            impl std::str::FromStr for #name {
155                type Err = anyhow::Error;
156
157                fn from_str(s: &str) -> Result<Self, Self::Err> {
158                    #matching
159                    return Err(anyhow::anyhow!("Regex matching failed for: {:?}", s));
160                }
161            }
162        }
163        .to_tokens(tokens);
164    }
165}
166
167fn parse(attr: TokenStream, item: TokenStream) -> Parsed {
168    match syn::parse2::<syn::Item>(item) {
169        Ok(syn::Item::Enum(mut e)) => {
170            let mut attrs_per_variant = Vec::new();
171            for v in &mut e.variants {
172                let mut attrs = Vec::new();
173                v.attrs.retain(|attr| {
174                    if attr.path.get_ident().map(proc_macro2::Ident::to_string)
175                        == Some("rematch".to_owned())
176                    {
177                        attrs.push(parse_rematch_attrs(attr.tokens.clone()));
178                        false
179                    } else {
180                        true
181                    }
182                });
183                attrs_per_variant.push(attrs);
184            }
185            Parsed::Enum {
186                item: e,
187                attrs_per_variant,
188            }
189        }
190        Ok(syn::Item::Struct(s)) => Parsed::Struct {
191            attr: parse_rematch_attrs(attr),
192            item: s,
193        },
194        Ok(item) => abort!(item, "item is not an enum and not a struct"),
195        _ => panic!("rematch can't be used on this item"),    // FIXME: better error handling
196    }
197}
198
199fn parse_rematch_attrs(raw_attrs: TokenStream) -> Attribute {
200    match syn::parse2::<syn::Expr>(raw_attrs.clone()) {
201        Ok(syn::Expr::Paren(paren_expr)) => {
202            Attribute::Regex(paren_expr.expr.to_token_stream())
203        }
204        Ok(syn::Expr::Lit(lit_expr)) => {
205            Attribute::Regex(lit_expr.lit.to_token_stream())
206        }
207        Ok(expr) => {
208            abort!(expr, "Unknown attributes expression!");
209        }
210        _ => panic!("invalid attributes"),    // FIXME: better error handling
211    }
212}