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}