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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
//! Provides a derive macro that implements `Envconfig` trait.
//! For complete documentation please see [envconfig](https://docs.rs/envconfig).

extern crate proc_macro;
extern crate syn;
#[macro_use]
extern crate quote;

use proc_macro::TokenStream;

use syn::{Ident, Lit, MetaItem, NestedMetaItem};

#[derive(Debug)]
struct Field {
    name: Ident,
    var_name: String,
}

#[proc_macro_derive(Envconfig, attributes(envconfig))]
pub fn derive(input: TokenStream) -> TokenStream {
    let s = input.to_string();
    let ast = syn::parse_derive_input(&s).unwrap();

    let struct_name = &ast.ident;

    let field_nodes = fetch_fields_from_ast_body(ast.body, &struct_name.to_string());

    let fields: Vec<Field> = field_nodes.iter().map(|f| parse_field(f.clone())).collect();

    let gen_fields = fields.iter().map(|f| {
        let ident = &f.name;
        let var_name = &f.var_name;
        quote! {
            #ident: ::envconfig::load_var(#var_name)?
        }
    });

    let gen = quote! {
        impl Envconfig for #struct_name {
            fn init() -> Result<Self, ::envconfig::Error> {
                let config = Self {
                    #(#gen_fields,)*
                };
                Ok(config)
            }
        }
    };

    gen.parse().unwrap()
}

fn fetch_fields_from_ast_body(body: syn::Body, name: &str) -> Vec<syn::Field> {
    match body {
        ::syn::Body::Struct(variant_data) => match variant_data {
            ::syn::VariantData::Struct(fields) => fields,
            _ => panic!(
                "Envconfig trait can not be derived from `{}` because it is not a struct.",
                name
            ),
        },
        _ => panic!(
            "Envconfig trait can not be derived from `{}` because it is not a struct.",
            name
        ),
    }
}

fn parse_field(field_node: syn::Field) -> Field {
    let mut from: Option<String> = None;
    // let mut default: Option<::syn::Lit> = None;

    // Get name of the field
    let name = field_node.ident.unwrap();

    // Find `envconfig` attribute on the given field
    let attr = field_node
        .attrs
        .iter()
        .find(|a| a.name() == "envconfig")
        .unwrap_or_else(|| panic!("Field `{}` must have `envconfig` attribute.", name));

    // Unwrap list from `envconfig` attribute.
    let list = match attr.value {
        MetaItem::List(ref _ident, ref list) => list,
        _ => panic!("Envconfig: attribute `envconfig` must be a list"),
    };

    // Iterate over items of `envconfig` attribute.
    // Each item is supposed to have name and value.
    for item in list.iter() {
        let mt = match item {
            NestedMetaItem::MetaItem(mt) => mt,
            _ => panic!(
                "Envconfig: failed to parse `envconfig` attribute for field `{}`",
                name
            ),
        };
        let (ident, value) = match mt {
            MetaItem::NameValue(ident, lit) => (ident, lit),
            _ => panic!(
                "Envconfig: failed to parse `envconfig` attribute for field `{}`",
                name
            ),
        };

        let item_name = format!("{}", ident);

        match item_name.as_str() {
            "from" => match value {
                Lit::Str(s, _style) => {
                    from = Some(s.to_string());
                }
                _ => panic!("Envconfig: value of `from` must be a string"),
            },
            // TODO: handle "default" here as well
            _ => panic!("Envconfig: unknown item on `{}`", item_name),
        }
    }

    let from_value =
        from.unwrap_or_else(|| panic!("attribute `envconfig` must contain `from` item"));

    Field {
        name,
        var_name: from_value,
    }
}