hyperderive 0.0.3

Custom derives for the `hyperdrive` crate. You should never use this crate directly. It does not expose a stable API and may break at any time. Use `hyperdrive` directly instead.
Documentation
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{Attribute, Data, Index, Meta};
use synstructure::Structure;

pub fn derive_request_context(s: Structure<'_>) -> TokenStream {
    deny_attr("as_ref", &s.ast().attrs);
    let additional_impls = match &s.ast().data {
        Data::Struct(st) => {
            let mut impls = Vec::new();
            for (index, field) in st.fields.iter().enumerate() {
                let as_ref_count = field
                    .attrs
                    .iter()
                    .filter(|attr| match attr.parse_meta() {
                        Ok(ref meta) if meta.name() == "as_ref" => {
                            if let Meta::Word(_) = meta {
                                true
                            } else {
                                if let Some(field) = &field.ident {
                                    panic!(
                                        "invalid syntax for #[as_ref] attribute on field `{}`",
                                        field
                                    );
                                } else {
                                    panic!(
                                        "invalid syntax for #[as_ref] attribute on field of type `{}`",
                                        field.ty.clone().into_token_stream()
                                    );
                                }
                            }
                        }
                        _ => false,
                    })
                    .count();

                match as_ref_count {
                    0 => {} // no AsRef impl generated
                    1 => {
                        let ty = &field.ty;
                        let field_name = if let Some(name) = &field.ident {
                            quote!(#name)
                        } else {
                            let index = Index::from(index);
                            quote!(#index)
                        };
                        impls.push(s.gen_impl(quote! {
                            gen impl AsRef<#ty> for @Self {
                                fn as_ref(&self) -> &#ty { &self.#field_name }
                            }
                        }));
                    }
                    _ => {
                        let name = if let Some(name) = &field.ident {
                            name.into_token_stream()
                        } else {
                            field.ty.clone().into_token_stream()
                        };
                        panic!(
                            "too many #[as_ref] attributes on `{}` (only one is permitted)",
                            name
                        )
                    }
                }
            }
            impls
        }
        Data::Enum(e) => {
            for variant in &e.variants {
                deny_attr("as_ref", &variant.attrs);

                for field in &variant.fields {
                    deny_attr("as_ref", &field.attrs);
                }
            }
            Vec::new()
        }
        Data::Union(u) => {
            for field in &u.fields.named {
                deny_attr("as_ref", &field.attrs);
            }
            Vec::new()
        }
    };

    let asref_nocontext = s.gen_impl(quote!(
        extern crate hyperdrive;
        use hyperdrive::NoContext;

        gen impl AsRef<NoContext> for @Self {
            fn as_ref(&self) -> &NoContext { &NoContext }
        }
    ));
    let asref_self = s.gen_impl(quote!(
        gen impl AsRef<Self> for @Self {
            fn as_ref(&self) -> &Self { self }
        }
    ));
    let request_context = s.gen_impl(quote!(
        extern crate hyperdrive;
        use hyperdrive::RequestContext;

        gen impl RequestContext for @Self {}
    ));

    quote!(
        #asref_nocontext

        #asref_self

        #(#additional_impls)*

        #request_context
    )
}

fn deny_attr<'a, I>(name: &str, attrs: I)
where
    I: IntoIterator<Item = &'a Attribute>,
{
    for attr in attrs {
        if let Ok(meta) = attr.parse_meta() {
            if meta.name() == name {
                panic!("#[{}] attribute is only allowed on struct fields", name);
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::derive_request_context;
    use synstructure::test_derive;

    /// Expands the given item by putting a `#[derive(RequestContext)]` on it.
    macro_rules! expand {
        (
            $i:item
        ) => {
            test_derive! {
                derive_request_context {
                    $i
                }
                expands to {} no_build
            }
        };
    }

    #[test]
    #[should_panic(expected = "#[as_ref] attribute is only allowed on struct fields")]
    fn asref_on_struct() {
        expand! {
            #[as_ref]
            struct MyStruct {
                field: u8,
            }
        }
    }

    #[test]
    #[should_panic(expected = "#[as_ref] attribute is only allowed on struct fields")]
    fn asref_enum_field() {
        expand! {
            enum MyEnum {
                Variant {
                    #[as_ref]
                    field: u8,
                }
            }
        }
    }

    #[test]
    #[should_panic(expected = "#[as_ref] attribute is only allowed on struct fields")]
    fn asref_enum_variant() {
        expand! {
            enum MyEnum {
                #[as_ref]
                Variant {
                    field: u8,
                }
            }
        }
    }

    #[test]
    #[should_panic(expected = "invalid syntax for #[as_ref] attribute on field `field`")]
    fn invalid1() {
        expand! {
            struct MyStruct {
                #[as_ref = "no"]
                field: u8,
            }
        }
    }

    #[test]
    #[should_panic(expected = "invalid syntax for #[as_ref] attribute on field of type `u8`")]
    fn invalid2() {
        expand! {
            struct MyStruct(#[as_ref = "aaa"] u8);
        }
    }

    #[test]
    #[should_panic(expected = "too many #[as_ref] attributes on `field1`")]
    fn invalid3_too_many() {
        expand! {
            struct MyStruct {
                field0: u8,

                #[as_ref]
                #[as_ref]
                field1: u8,
            }
        }
    }

    #[test]
    #[should_panic(expected = "too many #[as_ref] attributes on `u8`")]
    fn invalid4_too_many() {
        expand! {
            struct MyStruct(#[as_ref] #[as_ref] u8);
        }
    }
}