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