Skip to main content

assert_order/
lib.rs

1//! # `assert-order`
2//! Assert the definition order of enum variants.
3//!
4//! ## Why?
5//! The main motivation is that plenty of serialization formats
6//! are not self-describing, which means that for enums, in particular,
7//! reordering enum variants can result in de/serialization errors.
8//!
9//! This crate allows you to assert the order of enum variants, which
10//! can help prevent accidental reordering.
11//!
12//! ## Example
13//! ```
14//! use assert_order::{VariantOrder, assert_order};
15//!
16//! #[derive(VariantOrder)]
17//! enum TestEnum {
18//!     A,
19//!     B(),
20//!     C {},
21//! }
22//!
23//! assert_order::<TestEnum, _, _>(["A", "B", "C"]);
24//! ```
25
26pub use assert_order_derive::VariantOrder;
27
28/// Trait for getting the canonical ordering of
29/// an enum's variants.
30pub trait VariantOrder {
31    /// Returns the variant names of the enum
32    /// in order.
33    fn order() -> &'static [&'static str];
34}
35
36/// Assert the ordering of some enum implementing [VariantOrder].
37///
38///  Asserts that:
39/// * The variants for `E` occur in the order specified by
40///   `order`.
41/// * That all variants are asserted.
42pub fn assert_order<E, O, V>(order: O)
43where
44    E: VariantOrder,
45    O: IntoIterator<Item = V>,
46    V: AsRef<str>,
47{
48    let variants = E::order();
49    let iter = order.into_iter();
50
51    let mut n: usize = 0;
52    for variant in iter {
53        let variant = variant.as_ref();
54        let canonical = variants.get(n).map(|canonical| *canonical);
55
56        let Some(canonical) = canonical else {
57            panic!("Expected more canonical variants.");
58        };
59
60        assert!(
61            variant == canonical,
62            "Variant name mismatch: Expected \"{}\", got \"{}\"",
63            variant,
64            canonical,
65        );
66
67        n += 1;
68    }
69
70    assert!(
71        variants.len() == n,
72        "unexpected length: expected {}, got {}",
73        n,
74        variants.len()
75    );
76}
77
78#[cfg(test)]
79mod tests {
80    use std::thread::spawn;
81
82    use crate::{assert_order, VariantOrder};
83
84    #[test]
85    fn assert_proper_ordering() {
86        #[derive(VariantOrder)]
87        #[expect(unused)]
88        enum TestEnum {
89            A,
90            B(),
91            C {},
92        }
93
94        assert_eq!(["A", "B", "C"], TestEnum::order());
95    }
96
97    #[test]
98    fn assert_proper_ordering_lifetime() {
99        #[derive(VariantOrder)]
100        #[expect(unused)]
101        enum TestEnum<'a> {
102            A,
103            B(&'a str),
104            C {},
105        }
106
107        assert_eq!(["A", "B", "C"], TestEnum::order());
108    }
109
110    #[test]
111    fn expect_nonpanic() {
112        #[derive(VariantOrder)]
113        #[expect(unused)]
114        enum TestEnum {
115            A,
116            B(),
117            C {},
118        }
119
120        assert_order::<TestEnum, _, _>(["A", "B", "C"]);
121    }
122
123    #[test]
124    fn expect_nonpanic_lifetime() {
125        #[derive(VariantOrder)]
126        #[expect(unused)]
127        enum TestEnum<'a, T> {
128            A,
129            B(&'a str),
130            C { inner: T },
131        }
132
133        assert_order::<TestEnum::<'_, ()>, _, _>(["A", "B", "C"]);
134    }
135
136    #[test]
137    fn expect_nonpanic_generic() {
138        #[derive(VariantOrder)]
139        #[expect(unused)]
140        enum TestEnum<T> {
141            A,
142            B(T),
143            C {},
144        }
145
146        assert_order::<TestEnum::<()>, _, _>(["A", "B", "C"]);
147    }
148
149    #[test]
150    fn expect_order_panic() {
151        let thread = spawn(|| {
152            #[derive(VariantOrder)]
153            #[expect(unused)]
154            enum TestEnum {
155                A,
156                B(),
157                C {},
158            }
159
160            assert_order::<TestEnum, _, _>(["A", "C", "B"]);
161        });
162
163        let panic = thread.join().expect_err("expected panic");
164        let panic = panic
165            .downcast_ref::<String>()
166            .expect("panic as string")
167            .as_str();
168
169        assert_eq!(panic, "Variant name mismatch: Expected \"C\", got \"B\"");
170    }
171
172    #[test]
173    fn expect_too_long_panic() {
174        let thread = spawn(|| {
175            #[derive(VariantOrder)]
176            #[expect(unused)]
177            enum TestEnum {
178                A,
179                B(),
180                C {},
181            }
182
183            assert_order::<TestEnum, _, _>(["A", "B"]);
184        });
185
186        let panic = thread.join().expect_err("expected panic");
187        let panic = panic
188            .downcast_ref::<String>()
189            .expect("panic as string")
190            .as_str();
191
192        assert_eq!(panic, "unexpected length: expected 2, got 3");
193    }
194
195    #[test]
196    fn expect_too_short_panic() {
197        let thread = spawn(|| {
198            #[derive(VariantOrder)]
199            #[expect(unused)]
200            enum TestEnum {
201                A,
202                B(),
203                C {},
204            }
205
206            assert_order::<TestEnum, _, _>(["A", "B", "C", "D"]);
207        });
208
209        let panic = thread.join().expect_err("expected panic");
210        let panic = *(panic
211            .downcast_ref::<&'static str>()
212            .expect("panic as string"));
213
214        assert_eq!(panic, "Expected more canonical variants.");
215    }
216}