yade 0.1.2

Yet Another Derive Error
Documentation
extern crate proc_macro;
extern crate syn;

#[macro_use] extern crate synstructure;
#[macro_use] extern crate quote;

const DISPLAY_ATTR: &'static str = "display";
const DISPLAY_MSG: &'static str = "msg";
const CAUSE_ATTR: &'static str = "cause";

decl_derive!([YadeError, attributes(display, cause)] => error_derive);
fn error_derive(s: synstructure::Structure) -> quote::Tokens {
    let display_impl = generate_display_impl(&s);
    let error_impl = generate_error_impl(&s);

    quote! {
        #display_impl
        #error_impl
    }
}

decl_derive!([YadeKind, attributes(display)] => error_kind_derive);
fn error_kind_derive(s: synstructure::Structure) -> quote::Tokens {
    generate_display_impl(&s)
}

const INVALID_CAUSE: &'static str = "Cause must be a type implementing Error, a Box<Error>, or an Option<Box<Error>>";

fn generate_error_impl(s: &synstructure::Structure) -> quote::Tokens {
    let cause_matches = s.each_variant(|v| {
        if let Some(cause) = v.bindings().iter().find(is_cause) {
            let path = match cause.ast().ty {
                syn::Ty::Path(_, ref path) => path,
                _ => panic!(INVALID_CAUSE),
            };
            let parent_type = path.segments.last().unwrap();

            match parent_type.ident.as_ref() {
                "Option" => {
                    let child_path = match parent_type.parameters {
                        syn::PathParameters::AngleBracketed(ref data) => {
                            match data.types[0] {
                                syn::Ty::Path(_, ref path) => path,
                                _ => panic!(INVALID_CAUSE),
                            }
                        },
                        _ => panic!(INVALID_CAUSE),
                    };
                    let child_type = child_path.segments.last().unwrap();
                    match child_type.ident.as_ref() {
                        "Box" => {
                            quote!(return #cause.as_ref().map(|c|c.as_ref()))
                        },
                        _ => panic!(INVALID_CAUSE),
                    }

                },
                "Box" => quote!(return Some(#cause.as_ref())),
                _ => quote!(return Some(#cause)),
            }
        } else {
            quote!(return None)
        }
    });

    s.bound_impl("::std::error::Error", quote! {
        #[allow(unreachable_code)]
        fn cause(&self) -> Option<&::std::error::Error> {
            match *self { #cause_matches }
            None
        }

        #[allow(unreachable_code)]
        fn description(&self) -> &str {
            "For a description please use the Display trait implementation of this Error"
        }
    })
}

fn generate_display_impl(s: &synstructure::Structure) -> quote::Tokens {
    let display_matches = s.each_variant(|v| {
        let ast = &v.ast();
        let name = ast.ident.to_string();

        let msg = match find_error_msg(&ast.attrs) {
            Some(msg) => msg,
            None => {
                return quote! {
                    return write!(f, "{}", #name);
                }
            }
        };

        if msg.is_empty() {
            panic!("Attribute '{}' must contain `{} = \"\"`", DISPLAY_ATTR, DISPLAY_MSG);
        }

        let s = match msg[0] {
            syn::NestedMetaItem::MetaItem(syn::MetaItem::NameValue(ref i, ref lit)) if i == DISPLAY_MSG => {
                lit.clone()
            }
            _ => panic!("Attribute '{}' must contain `{} = \"\"`", DISPLAY_ATTR, DISPLAY_MSG),
        };
        use quote::ToTokens;
        let args = msg[1..].iter().map(|arg| match *arg {
            syn::NestedMetaItem::Literal(syn::Lit::Int(i, _)) => {
                let bi = &v.bindings()[i as usize];
                quote!(#bi)
            }
            syn::NestedMetaItem::MetaItem(syn::MetaItem::Word(ref id)) => {
                if id.as_ref().starts_with("_") {
                    if let Ok(idx) = id.as_ref()[1..].parse::<usize>() {
                        let bi = &v.bindings()[idx];
                        return quote!(#bi)
                    }
                }
                for bi in v.bindings() {
                    if bi.ast().ident.as_ref() == Some(id) {
                        return quote!(#bi);
                    }
                }
                panic!("Couldn't find field '{}' of '{}'", id, name);
            }
            _ => {
                let mut tokens = quote::Tokens::new();
                arg.to_tokens(&mut tokens);
                panic!("Invalid '{}' attribute argument `{}` for '{}'", DISPLAY_ATTR, tokens, name);
            },
        });

        quote! {
            return write!(f, #s #(, #args)*)
        }
    });

    s.bound_impl("::std::fmt::Display", quote! {
        #[allow(unreachable_code)]
        fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
            match *self { #display_matches }
            write!(f, "There was an unknown error.")
        }
    })
}

fn find_error_msg(attrs: &[syn::Attribute]) -> Option<&[syn::NestedMetaItem]> {
    let mut error_msg = None;

    for attr in attrs {
        if attr.name() == DISPLAY_ATTR {
            if error_msg.is_some() {
                panic!("Attribute '{}' specified multiple times.", DISPLAY_ATTR)
            } else {
                if let syn::MetaItem::List(_, ref list)  = attr.value {
                    error_msg = Some(&list[..]);
                } else {
                    panic!("Attribute '{}' requires parens", DISPLAY_ATTR)
                }
            }
        }
    }

    error_msg
}

fn is_cause(bi: &&synstructure::BindingInfo) -> bool {
    bi.ast().attrs.iter().any(|attr| attr.name() == CAUSE_ATTR)
}