Skip to main content

assertr_derive/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(clippy::unwrap_used)]
3#![allow(clippy::needless_continue)]
4
5mod fluent_aliases;
6
7use proc_macro::TokenStream;
8
9use darling::{Error, FromDeriveInput, FromField, ast};
10use proc_macro2::Span;
11use quote::quote;
12use syn::{DeriveInput, Ident, ItemTrait, Path, Type, Visibility, parse_macro_input};
13
14#[derive(Debug, FromField)]
15#[darling(attributes(assertr_eq))]
16struct MyFieldReceiver {
17    ident: Option<Ident>,
18
19    ty: Type,
20
21    vis: Visibility,
22
23    #[darling(default)]
24    map_type: Option<Type>,
25
26    #[darling(default)]
27    compare_with: Option<Path>,
28}
29
30#[derive(Debug, FromDeriveInput)]
31#[darling(attributes(assertr_eq), supports(struct_any))]
32struct MyInputReceiver {
33    ident: Ident,
34
35    data: ast::Data<(), MyFieldReceiver>,
36}
37
38impl MyInputReceiver {
39    pub fn fields(&self) -> &ast::Fields<MyFieldReceiver> {
40        match &self.data {
41            ast::Data::Enum(_) => panic!("Only structs are supported"),
42            ast::Data::Struct(fields) => fields,
43        }
44    }
45}
46
47/// Derive macro for `AssertrEq`.
48///
49/// # Panics
50///
51/// This proc macro will panic if applied to an enum, as only structs are supported.
52#[proc_macro_derive(AssertrEq, attributes(assertr_eq))]
53pub fn store(input: TokenStream) -> TokenStream {
54    let ast = parse_macro_input!(input as DeriveInput);
55
56    let input: MyInputReceiver = match FromDeriveInput::from_derive_input(&ast) {
57        Ok(args) => args,
58        Err(err) => return Error::write_errors(err).into(),
59    };
60
61    let original_struct_ident = input.ident.clone();
62
63    let filtered_fields = input.fields().iter().filter(|field| match field.vis {
64        Visibility::Public(_) => true,
65        Visibility::Restricted(_) | Visibility::Inherited => false,
66    });
67
68    let eq_struct_ident = Ident::new(
69        format!("{}AssertrEq", input.ident).as_str(),
70        Span::call_site(),
71    );
72
73    let eq_struct_fields = filtered_fields.clone().map(|field| {
74        let vis = &field.vis;
75        let ident = &field.ident;
76        let ty = match &field.map_type {
77            None => &field.ty,
78            Some(ty) => ty,
79        };
80        quote! { #vis #ident: ::assertr::Eq<#ty> }
81    });
82
83    let eq_impls = filtered_fields.map(|field| {
84        let ident = field
85            .ident
86            .as_ref()
87            .expect("only named fields are supported!");
88        let ident_string = ident.to_string();
89        let ty = match &field.map_type {
90            None => &field.ty,
91            Some(ty) => ty,
92        };
93        let eq_args = quote! { &self.#ident, v, ctx.as_deref_mut() };
94        let eq_check = match &field.compare_with {
95            None => quote! { ::assertr::AssertrPartialEq::<#ty>::eq(#eq_args) },
96            Some(eq_check) => {
97                quote! { #eq_check(#eq_args) }
98            }
99        };
100        quote! {
101            && match &other.#ident {
102                ::assertr::Eq::Any => true,
103                ::assertr::Eq::Eq(v) => {
104                    let eq = #eq_check;
105                    if !eq {
106                        if let Some(ctx) = ctx.as_mut() {
107                            ctx.add_field_difference(#ident_string, v, &self.#ident);
108                        }
109                    }
110                    eq
111                },
112            }
113        }
114    });
115
116    Into::into(quote! {
117        #[derive(::core::fmt::Debug, ::core::default::Default)]
118        pub struct #eq_struct_ident {
119            #(#eq_struct_fields),*
120        }
121
122        impl ::assertr::AssertrPartialEq<#eq_struct_ident> for &#original_struct_ident {
123            fn eq(&self, other: &#eq_struct_ident, mut ctx: Option<&mut ::assertr::EqContext>) -> bool {
124                true #(#eq_impls)*
125            }
126        }
127
128        impl ::assertr::AssertrPartialEq<#eq_struct_ident> for #original_struct_ident {
129            fn eq(&self, other: &#eq_struct_ident, ctx: Option<&mut ::assertr::EqContext>) -> bool {
130                ::assertr::AssertrPartialEq::eq(&self, other, ctx)
131            }
132        }
133    })
134}
135
136/// Attribute macro that generates fluent aliases for assertion trait methods.
137///
138/// Place on a trait definition to auto-generate `be_*` aliases for `is_*` methods
139/// and `have_*` aliases for `has_*` methods. Generated aliases are gated behind
140/// `#[cfg(feature = "fluent")]`.
141///
142/// Use `#[fluent_alias("custom_name")]` on a method for a custom alias name.
143/// Use `#[no_fluent_alias]` on a method to skip alias generation.
144#[proc_macro_attribute]
145pub fn fluent_aliases(_attr: TokenStream, item: TokenStream) -> TokenStream {
146    let trait_def = parse_macro_input!(item as ItemTrait);
147    fluent_aliases::fluent_aliases_impl(trait_def).into()
148}