partial_cmp_derive/
lib.rs

1//! # partial-cmp-derive
2//!
3//! A procedural macro crate for deriving `PartialEq`, `Eq`, `PartialOrd`, `Ord`, and `Hash`
4//! with fine-grained control over field comparison and hashing behavior.
5//!
6//! ## Features
7//!
8//! - **Skip fields**: Use `#[ord(skip)]` to exclude fields from all comparisons and hashing.
9//!   Skipped fields are ignored in equality, ordering, and hash computations.
10//! - **Sort order**: Use `#[ord(order = "asc")]` or `#[ord(order = "desc")]` per field
11//! - **Explicit ordering**: Use `#[ord(by = [field1(desc), field2(asc)])]` at struct level
12//! - **Field priority**: Use `#[ord(priority = N)]` for implicit ordering (lower = first)
13//! - **Key extraction**: Use `#[ord(key = "path::to::fn")]` to extract a comparable key
14//! - **Reverse all**: Use `#[ord(reverse)]` at struct level to reverse entire comparison
15//! - **Enum ranking**: Use `#[ord(rank = N)]` to control variant ordering
16//! - **Option handling**: Use `#[ord(none_order = "first")]` or `"last"`
17//! - **Trait selection**: Control which traits are generated with skip flags
18//!
19//! ## Trait Generation
20//!
21//! By default, all five traits are generated: `PartialEq`, `Eq`, `PartialOrd`, `Ord`, and `Hash`.
22//! The `Hash` implementation is consistent with `Eq`, ensuring the invariant
23//! `a == b -> hash(a) == hash(b)` holds. You can opt out of specific traits:
24//!
25//! - `#[ord(skip_partial_eq)]` — Don't generate `PartialEq` (implies no other traits)
26//! - `#[ord(skip_eq)]` — Don't generate `Eq` (also disables `Ord`)
27//! - `#[ord(skip_partial_ord)]` — Don't generate `PartialOrd` (also disables `Ord`)
28//! - `#[ord(skip_ord)]` — Don't generate `Ord`
29//! - `#[ord(skip_hash)]` — Don't generate `Hash`
30//!
31//! ## Example
32//!
33//! ```rust
34//! use partial_cmp_derive::PartialCmp;
35//!
36//! // Generates PartialEq, Eq, PartialOrd, Ord, and Hash
37//! #[derive(PartialCmp)]
38//! struct Point {
39//!     x: i32,
40//!     y: i32,
41//! }
42//!
43//! // Skipped fields are excluded from all comparisons and hashing
44//! // Use skip_eq, skip_ord, and skip_hash when non-skipped fields don't implement those traits
45//! #[derive(Debug, PartialCmp)]
46//! #[ord(skip_eq, skip_ord, skip_hash)]
47//! struct Measurement {
48//!     #[ord(skip)]
49//!     raw_value: f32,  // Ignored in eq, cmp, and hash
50//!     timestamp: u64,
51//! }
52//!
53//! // Compare by absolute value using a key function
54//! #[derive(Debug, PartialCmp)]
55//! struct AbsValue {
56//!     #[ord(key = "abs_key")]
57//!     value: i32,
58//! }
59//!
60//! fn abs_key(v: &i32) -> i32 {
61//!     v.abs()
62//! }
63//! ```
64//!
65//! This generates consistent implementations where skipped fields are ignored
66//! in equality, ordering, and hash computations.
67//!
68//! ## Key Extraction
69//!
70//! The `key` attribute allows you to specify a function that extracts a comparable
71//! value from a field. This single function is used for `Eq`, `Ord`, and `Hash`,
72//! ensuring consistency across all three traits automatically:
73//!
74//! ```rust
75//! use partial_cmp_derive::PartialCmp;
76//!
77//! fn abs_key(v: &i32) -> i32 {
78//!     v.abs()
79//! }
80//!
81//! #[derive(Debug, PartialCmp)]
82//! struct AbsValue {
83//!     #[ord(key = "abs_key")]
84//!     value: i32,
85//! }
86//!
87//! let a = AbsValue { value: -5 };
88//! let b = AbsValue { value: 5 };
89//!
90//! // Both are equal because abs(-5) == abs(5)
91//! assert_eq!(a, b);
92//! // And their hashes are equal too (Hash/Eq invariant maintained)
93//! ```
94//!
95//! The key function signature should be `fn(&T) -> U` where `U: Ord + Hash`.
96
97mod expand;
98mod types;
99
100use darling::FromDeriveInput;
101use proc_macro::TokenStream;
102
103use syn::{DeriveInput, parse_macro_input};
104
105use expand::cmp::expand_derive;
106use types::OrdDerive;
107
108/// Derives comparison and hash traits with customizable field behavior.
109///
110/// By default, this macro generates implementations for `PartialEq`, `Eq`,
111/// `PartialOrd`, `Ord`, and `Hash`. All traits respect the same field configuration,
112/// ensuring consistent behavior.
113///
114/// # Struct-Level Attributes
115///
116/// - `#[ord(reverse)]` — Reverses the final comparison result
117/// - `#[ord(by = [field1(asc), field2(desc)])]` — Explicit field comparison order
118/// - `#[ord(skip_partial_eq)]` — Don't generate `PartialEq` (disables all other traits too)
119/// - `#[ord(skip_eq)]` — Don't generate `Eq` (also disables `Ord`)
120/// - `#[ord(skip_partial_ord)]` — Don't generate `PartialOrd` (also disables `Ord`)
121/// - `#[ord(skip_ord)]` — Don't generate `Ord`
122/// - `#[ord(skip_hash)]` — Don't generate `Hash`
123///
124/// # Field-Level Attributes
125///
126/// - `#[ord(skip)]` — Exclude this field from all comparisons and hashing
127/// - `#[ord(order = "asc")]` or `#[ord(order = "desc")]` — Sort direction
128/// - `#[ord(priority = N)]` — Comparison priority (lower = compared first)
129/// - `#[ord(key = "path::to::fn")]` — Key extraction function (signature: `fn(&T) -> U`)
130/// - `#[ord(none_order = "first")]` or `#[ord(none_order = "last")]` — Option handling
131///
132/// # Variant-Level Attributes (Enums)
133///
134/// - `#[ord(rank = N)]` — Explicit variant ranking (lower = less than)
135///
136/// # Key Extraction
137///
138/// The `key` attribute specifies a function that extracts a comparable/hashable value
139/// from a field. The extracted key is used consistently for `Eq`, `Ord`, and `Hash`,
140/// ensuring the invariant `a == b -> hash(a) == hash(b)` is maintained.
141///
142/// ```rust
143/// use partial_cmp_derive::PartialCmp;
144///
145/// fn abs_key(v: &i32) -> i32 {
146///     v.abs()
147/// }
148///
149/// #[derive(Debug, PartialCmp)]
150/// struct AbsValue {
151///     #[ord(key = "abs_key")]
152///     value: i32,
153/// }
154///
155/// let a = AbsValue { value: -5 };
156/// let b = AbsValue { value: 5 };
157///
158/// assert_eq!(a, b);  // Equal because abs(-5) == abs(5)
159/// ```
160///
161/// # Example
162///
163/// ```rust
164/// use partial_cmp_derive::PartialCmp;
165///
166/// #[derive(Debug, PartialCmp)]
167/// #[ord(by = [score(desc), name(asc)])]
168/// struct Player {
169///     id: u64,      // Not compared (not in `by` list)
170///     name: String,
171///     score: u32,
172/// }
173/// ```
174#[proc_macro_derive(PartialCmp, attributes(ord))]
175pub fn partial_cmp_derive(input: TokenStream) -> TokenStream {
176    let input = parse_macro_input!(input as DeriveInput);
177
178    let ord_derive = match OrdDerive::from_derive_input(&input) {
179        Ok(v) => v,
180        Err(e) => return e.write_errors().into(),
181    };
182
183    expand_derive(&ord_derive).into()
184}