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}