timeless_partialeq/
lib.rs

1//! Ignore fields ending with a specific suffix in PartialEq custom implementations.
2//!
3//! # Examples
4//! ```
5//! use timeless_partialeq::TimelessPartialEq;
6//! use chrono::{DateTime, TimeZone, Utc};
7//! 
8
9//! #[derive(Debug, TimelessPartialEq)]
10//! #[exclude_suffix(at, date)]
11//! pub struct Post {
12//!     pub id: i64,
13//!     pub content: String,
14//!     pub author: i32,
15//!     pub creation_date: DateTime<Utc>,
16//!     pub updated_at: Option<DateTime<Utc>>,
17//! }
18//! 
19
20//! assert_eq!(
21//!     Post {
22//!         id: 1,
23//!         content: "test".to_string(),
24//!         author: 1,
25//!         creation_date: Utc.timestamp_millis_opt(1715017040672).unwrap(),
26//!         updated_at: Some(Utc.timestamp_millis_opt(1715017020672).unwrap()),
27//!     },
28//!     Post {
29//!         id: 1,
30//!         content: "test".to_string(),
31//!         author: 1,
32//!         creation_date: Utc::now(),
33//!         updated_at: Some(Utc::now()),
34//!     }
35//! ) // true
36//! ```
37//! 
38//! # About the crate
39//! This crate was made to solve a very specific problem: assert the equality of two objects despite the timestamp differences. It was also made so that I could study proc macros.
40//! However, just after a day after publishing it, I realized that it can be broader than just timestamps.
41//! 
42//! I will not make a commitment into iterating this quickly, but it is in my plans to expand the scope of the crate.
43
44extern crate proc_macro;
45use proc_macro::TokenStream;
46use quote::{quote, ToTokens};
47use syn::{parse_macro_input, DeriveInput};
48
49
50#[proc_macro_derive(TimelessPartialEq, attributes(exclude_suffix))]
51pub fn partial_eq_except_timestamps(input: TokenStream) -> TokenStream {
52    let ast = parse_macro_input!(input as DeriveInput);
53
54    let mut args: Vec<String> = Vec::new();
55    
56    for attr in ast.attrs.iter() {
57        if attr.path().is_ident("exclude_suffix") {
58            let meta_list = attr.meta.require_list();
59            if let Ok(meta_list) = meta_list {
60                for arg in meta_list.tokens.to_token_stream() {
61                    let arg = arg.to_string();
62                    if arg != "," {
63                        args.push(arg);
64                    }
65                } 
66            }
67        }
68    }
69
70    let name = &ast.ident;
71    let fields = if let syn::Data::Struct(syn::DataStruct {
72        fields: syn::Fields::Named(syn::FieldsNamed { named, .. }),
73        ..
74    }) = ast.data
75    {
76        named
77    } else {
78        panic!("TimelessPartialEq can only be derived for structs with named fields");
79    };
80
81    let field_comparisons = fields.iter().filter_map(|field| {
82        let ident = &field.ident;
83        let field_type = &field.ty;
84        let field = ident.as_ref().unwrap().to_string();
85        if args.is_empty() {
86            args.push("at".to_string());
87        }
88        if args.iter().any(|arg| field.ends_with(&format!("_{}", arg))) {
89            if field_type
90            .to_token_stream()
91            .to_string()
92            .starts_with("Option")
93            {
94                return Some(quote! { self.#ident.is_none() && other.#ident.is_none() || self.#ident.is_some() && other.#ident.is_some()});
95            }   
96                None
97        } else {
98            Some(quote! { &self.#ident == &other.#ident })
99        }
100    });
101
102    let expanded = quote! {
103        impl PartialEq for #name {
104            fn eq(&self, other: &Self) -> bool {
105                #(#field_comparisons)&&*
106            }
107        }
108    };
109
110    TokenStream::from(expanded)
111}