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, ®ex);
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 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}