skippable_partialeq/
lib.rs

1//! Ignore fields in PartialEq custom implementations.
2//!
3//! # Examples
4//! ```
5//! use skippable_partialeq::SkippablePartialEq;
6//! use chrono::{DateTime, TimeZone, Utc};
7//! 
8
9//! #[derive(Debug, SkippablePartialEq)]
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//! You can also skip specific fields that do not follow a pattern using the `#[skip]` attribute above the fields you want to ignore:
39//! 
40//! ```
41//! use skippable_partialeq::SkippablePartialEq;
42//! use chrono::{DateTime, TimeZone, Utc};
43//! 
44//! #[derive(Debug, SkippablePartialEq)]
45//! pub struct Post {
46//!     pub id: i64,
47//!     pub content: String,
48//!     pub author: i32,
49//!     #[skip]
50//!     pub creation_date: DateTime<Utc>,
51//! }
52
53//! assert_eq!(
54//!     Post {
55//!         id: 1,
56//!         content: "test".to_string(),
57//!         author: 1,
58//!         creation_date: Utc.timestamp_millis_opt(1715017040672).unwrap(),
59//!     },
60//!     Post {
61//!         id: 1,
62//!         content: "test".to_string(),
63//!         author: 1,
64//!         creation_date: Utc::now(),
65//!     }
66//! )
67//! ```
68
69extern crate proc_macro;
70use proc_macro::TokenStream;
71use quote::{quote, ToTokens};
72use syn::{parse_macro_input, DeriveInput};
73
74
75#[proc_macro_derive(SkippablePartialEq, attributes(exclude_suffix, skip))]
76pub fn partial_eq_except_timestamps(input: TokenStream) -> TokenStream {
77    let ast = parse_macro_input!(input as DeriveInput);
78
79    let mut args: Vec<String> = Vec::new();
80    for attr in ast.attrs.iter() {
81        if attr.path().is_ident("exclude_suffix") {
82            let meta_list = attr.meta.require_list();
83            if let Ok(meta_list) = meta_list {
84                for arg in meta_list.tokens.to_token_stream() {
85                    let arg = arg.to_string();
86                    if arg != "," {
87                        args.push(arg);
88                    }
89                } 
90            }
91        }
92    }
93
94    let name = &ast.ident;
95    let fields = if let syn::Data::Struct(syn::DataStruct {
96        fields: syn::Fields::Named(syn::FieldsNamed { named, .. }),
97        ..
98    }) = ast.data
99    {
100        named
101    } else {
102        panic!("SkippablePartialEq can only be derived for structs with named fields");
103    };
104
105
106    if !fields.iter().any(|field| field.attrs.iter().any(|attr| attr.path().is_ident("skip"))) && args.is_empty() {
107        panic!("SkippablePartialEq needs arguments to know what fields to skip");
108    }
109        
110
111    let field_comparisons = fields.iter().filter_map(|field| {
112        let has_specific_skip = field.attrs.iter().any(|attr| attr.path().is_ident("skip"));
113
114        if has_specific_skip {
115            return None
116        } else {
117            let ident = &field.ident;
118            let field_type = &field.ty;
119            let field_name = ident.as_ref().unwrap().to_string();
120    
121            if args.iter().any(|arg| field_name.ends_with(&format!("_{}", arg))) {
122                if field_type
123                .to_token_stream()
124                .to_string()
125                .starts_with("Option")
126                {
127                    return Some(quote! { self.#ident.is_none() && other.#ident.is_none() || self.#ident.is_some() && other.#ident.is_some()});
128                }   
129                    None
130            } else {
131                Some(quote! { &self.#ident == &other.#ident })
132            }
133        }
134    });
135
136    let expanded = quote! {
137        impl PartialEq for #name {
138            fn eq(&self, other: &Self) -> bool {
139                #(#field_comparisons)&&*
140            }
141        }
142    };
143
144    TokenStream::from(expanded)
145}