assertr-derive 0.2.5

Derive AssertrEq implementations.
Documentation
#![forbid(unsafe_code)]
#![deny(clippy::unwrap_used)]
#![allow(clippy::needless_continue)]

mod fluent_aliases;

use proc_macro::TokenStream;

use darling::{Error, FromDeriveInput, FromField, ast};
use proc_macro2::Span;
use quote::quote;
use syn::{DeriveInput, Ident, ItemTrait, Path, Type, Visibility, parse_macro_input};

#[derive(Debug, FromField)]
#[darling(attributes(assertr_eq))]
struct MyFieldReceiver {
    ident: Option<Ident>,

    ty: Type,

    vis: Visibility,

    #[darling(default)]
    map_type: Option<Type>,

    #[darling(default)]
    compare_with: Option<Path>,
}

#[derive(Debug, FromDeriveInput)]
#[darling(attributes(assertr_eq), supports(struct_any))]
struct MyInputReceiver {
    ident: Ident,

    data: ast::Data<(), MyFieldReceiver>,
}

impl MyInputReceiver {
    pub fn fields(&self) -> &ast::Fields<MyFieldReceiver> {
        match &self.data {
            ast::Data::Enum(_) => panic!("Only structs are supported"),
            ast::Data::Struct(fields) => fields,
        }
    }
}

/// Derive macro for `AssertrEq`.
///
/// # Panics
///
/// This proc macro will panic if applied to an enum, as only structs are supported.
#[proc_macro_derive(AssertrEq, attributes(assertr_eq))]
pub fn store(input: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);

    let input: MyInputReceiver = match FromDeriveInput::from_derive_input(&ast) {
        Ok(args) => args,
        Err(err) => return Error::write_errors(err).into(),
    };

    let original_struct_ident = input.ident.clone();

    let filtered_fields = input.fields().iter().filter(|field| match field.vis {
        Visibility::Public(_) => true,
        Visibility::Restricted(_) | Visibility::Inherited => false,
    });

    let eq_struct_ident = Ident::new(
        format!("{}AssertrEq", input.ident).as_str(),
        Span::call_site(),
    );

    let eq_struct_fields = filtered_fields.clone().map(|field| {
        let vis = &field.vis;
        let ident = &field.ident;
        let ty = match &field.map_type {
            None => &field.ty,
            Some(ty) => ty,
        };
        quote! { #vis #ident: ::assertr::Eq<#ty> }
    });

    let eq_impls = filtered_fields.map(|field| {
        let ident = field
            .ident
            .as_ref()
            .expect("only named fields are supported!");
        let ident_string = ident.to_string();
        let ty = match &field.map_type {
            None => &field.ty,
            Some(ty) => ty,
        };
        let eq_args = quote! { &self.#ident, v, ctx.as_deref_mut() };
        let eq_check = match &field.compare_with {
            None => quote! { ::assertr::AssertrPartialEq::<#ty>::eq(#eq_args) },
            Some(eq_check) => {
                quote! { #eq_check(#eq_args) }
            }
        };
        quote! {
            && match &other.#ident {
                ::assertr::Eq::Any => true,
                ::assertr::Eq::Eq(v) => {
                    let eq = #eq_check;
                    if !eq {
                        if let Some(ctx) = ctx.as_mut() {
                            ctx.add_field_difference(#ident_string, v, &self.#ident);
                        }
                    }
                    eq
                },
            }
        }
    });

    Into::into(quote! {
        #[derive(::core::fmt::Debug, ::core::default::Default)]
        pub struct #eq_struct_ident {
            #(#eq_struct_fields),*
        }

        impl ::assertr::AssertrPartialEq<#eq_struct_ident> for &#original_struct_ident {
            fn eq(&self, other: &#eq_struct_ident, mut ctx: Option<&mut ::assertr::EqContext>) -> bool {
                true #(#eq_impls)*
            }
        }

        impl ::assertr::AssertrPartialEq<#eq_struct_ident> for #original_struct_ident {
            fn eq(&self, other: &#eq_struct_ident, ctx: Option<&mut ::assertr::EqContext>) -> bool {
                ::assertr::AssertrPartialEq::eq(&self, other, ctx)
            }
        }
    })
}

/// Attribute macro that generates fluent aliases for assertion trait methods.
///
/// Place on a trait definition to auto-generate `be_*` aliases for `is_*` methods
/// and `have_*` aliases for `has_*` methods. Generated aliases are gated behind
/// `#[cfg(feature = "fluent")]`.
///
/// Use `#[fluent_alias("custom_name")]` on a method for a custom alias name.
/// Use `#[no_fluent_alias]` on a method to skip alias generation.
#[proc_macro_attribute]
pub fn fluent_aliases(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let trait_def = parse_macro_input!(item as ItemTrait);
    fluent_aliases::fluent_aliases_impl(trait_def).into()
}