recap_derive/
lib.rs

1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use proc_macro2::Span;
5use quote::quote;
6use regex::Regex;
7use syn::{
8    parse_macro_input, Data::Struct, DataStruct, DeriveInput, Fields, Ident, Lit, Meta, NestedMeta,
9};
10
11#[proc_macro_derive(Recap, attributes(recap))]
12pub fn derive_recap(item: TokenStream) -> TokenStream {
13    let item = parse_macro_input!(item as DeriveInput);
14    let regex = extract_regex(&item).expect(
15        r#"Unable to resolve recap regex.
16            Make sure your structure has declared an attribute in the form:
17            #[derive(Deserialize, Recap)]
18            #[recap(regex ="your-pattern-here")]
19            struct YourStruct { ... }
20            "#,
21    );
22
23    validate(&item, &regex);
24
25    let item_ident = &item.ident;
26    let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();
27
28    let has_lifetimes = item.generics.lifetimes().count() > 0;
29    let impl_from_str = if !has_lifetimes {
30        quote! {
31            impl #impl_generics std::str::FromStr for #item_ident #ty_generics #where_clause {
32                type Err = recap::Error;
33                fn from_str(s: &str) -> Result<Self, Self::Err> {
34                    recap::lazy_static! {
35                        static ref RE: recap::Regex = recap::Regex::new(#regex)
36                            .expect("Failed to compile regex");
37                    }
38
39                    recap::from_captures(&RE, s)
40                }
41            }
42        }
43    } else {
44        quote! {}
45    };
46
47    let lifetimes = item.generics.lifetimes();
48    let also_lifetimes = item.generics.lifetimes();
49    let impl_inner = quote! {
50        impl #impl_generics std::convert::TryFrom<& #(#lifetimes)* str> for #item_ident #ty_generics #where_clause {
51            type Error = recap::Error;
52            fn try_from(s: & #(#also_lifetimes)* str) -> Result<Self, Self::Error> {
53                recap::lazy_static! {
54                    static ref RE: recap::Regex = recap::Regex::new(#regex)
55                        .expect("Failed to compile regex");
56                }
57
58                recap::from_captures(&RE, s)
59            }
60        }
61        #impl_from_str
62    };
63
64    let impl_matcher = quote! {
65        impl #impl_generics  #item_ident #ty_generics #where_clause {
66            /// Recap derived method. Returns true when some input text
67            /// matches the regex associated with this type
68            pub fn is_match(input: &str) -> bool {
69                recap::lazy_static! {
70                    static ref RE: recap::Regex = recap::Regex::new(#regex)
71                        .expect("Failed to compile regex");
72                }
73                RE.is_match(input)
74            }
75        }
76    };
77
78    let injector = Ident::new(
79        &format!("RECAP_IMPL_FOR_{}", item.ident.to_string()),
80        Span::call_site(),
81    );
82
83    let out = quote! {
84        const #injector: () = {
85            extern crate recap;
86            #impl_inner
87            #impl_matcher
88        };
89    };
90
91    out.into()
92}
93
94fn validate(
95    item: &DeriveInput,
96    regex: &str,
97) {
98    let regex = Regex::new(regex).unwrap_or_else(|err| {
99        panic!(
100            "Invalid regular expression provided for `{}`\n{}",
101            &item.ident, err
102        )
103    });
104    let caps = regex.capture_names().flatten().count();
105    let fields = match &item.data {
106        Struct(DataStruct {
107            fields: Fields::Named(fs),
108            ..
109        }) => fs.named.len(),
110        _ => panic!("Recap regex can only be applied to Structs with named fields"),
111    };
112    if caps != fields {
113        panic!(
114            "Recap could not derive a `FromStr` impl for `{}`.\n\t\t > Expected regex with {} named capture groups to align with struct fields but found {}",
115            item.ident, fields, caps
116        );
117    }
118}
119
120fn extract_regex(item: &DeriveInput) -> Option<String> {
121    item.attrs
122        .iter()
123        .flat_map(syn::Attribute::parse_meta)
124        .filter_map(|x| match x {
125            Meta::List(y) => Some(y),
126            _ => None,
127        })
128        .filter(|x| x.path.is_ident("recap"))
129        .flat_map(|x| x.nested.into_iter())
130        .filter_map(|x| match x {
131            NestedMeta::Meta(y) => Some(y),
132            _ => None,
133        })
134        .filter_map(|x| match x {
135            Meta::NameValue(y) => Some(y),
136            _ => None,
137        })
138        .find(|x| x.path.is_ident("regex"))
139        .and_then(|x| match x.lit {
140            Lit::Str(y) => Some(y.value()),
141            _ => None,
142        })
143}