1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
extern crate proc_macro;

use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use regex::Regex;
use std::convert::identity;
use syn::{
    parse_macro_input, Data::Struct, DataStruct, DeriveInput, Fields, Ident, Lit, Meta, NestedMeta,
};

#[proc_macro_derive(Recap, attributes(recap))]
pub fn derive_recap(item: TokenStream) -> TokenStream {
    let item = parse_macro_input!(item as DeriveInput);
    let regex = extract_regex(&item).expect(
        r#"Unable to resolve recap regex.
            Make sure your structure has declared an attribute in the form:
            #[derive(Deserialize, Recap)]
            #[recap(regex ="your-pattern-here")]
            struct YourStruct { ... }
            "#,
    );

    validate(&item, &regex);

    let item_ident = &item.ident;
    let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();

    let impl_inner = quote! {
        impl #impl_generics std::str::FromStr for #item_ident #ty_generics #where_clause {
            type Err = recap::Error;
            fn from_str(s: &str) -> Result<Self, Self::Err> {
                recap::lazy_static! {
                    static ref RE: recap::Regex = recap::Regex::new(#regex)
                        .expect("Failed to compile regex");
                }

                Ok(recap::from_captures(&RE, s)?)
            }
        }
    };

    let injector = Ident::new(
        &format!("IMPL_FROMSTR_FOR_{}", item.ident.to_string()),
        Span::call_site(),
    );

    let out = quote! {
        const #injector: () = {
            extern crate recap;
            #impl_inner
        };
    };

    out.into()
}

fn validate(
    item: &DeriveInput,
    regex: &str,
) {
    let regex = Regex::new(&regex).unwrap_or_else(|err| {
        panic!(
            "Invalid regular expression provided for `{}`\n{}",
            &item.ident, err
        )
    });
    let caps = regex.capture_names().filter_map(identity).count();
    let fields = match &item.data {
        Struct(DataStruct {
            fields: Fields::Named(fs),
            ..
        }) => fs.named.len(),
        _ => panic!("Recap regex can only be applied to Structs with named fields"),
    };
    if caps != fields {
        panic!(
            "Recap could not derive a `FromStr` impl for `{}`.\n\t\t > Expected regex with {} named capture groups to align with struct fields but found {}",
            item.ident, fields, caps
        );
    }
}

fn extract_regex(item: &DeriveInput) -> Option<String> {
    item.attrs
        .iter()
        .flat_map(syn::Attribute::parse_meta)
        .filter_map(|x| match x {
            Meta::List(y) => Some(y),
            _ => None,
        })
        .filter(|x| x.ident == "recap")
        .flat_map(|x| x.nested.into_iter())
        .filter_map(|x| match x {
            NestedMeta::Meta(y) => Some(y),
            _ => None,
        })
        .filter_map(|x| match x {
            Meta::NameValue(y) => Some(y),
            _ => None,
        })
        .find(|x| x.ident == "regex")
        .and_then(|x| match x.lit {
            Lit::Str(y) => Some(y.value()),
            _ => None,
        })
}