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::{VariantOrder, assert_order};
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 expect_nonpanic() {
99        #[derive(VariantOrder)]
100        #[expect(unused)]
101        enum TestEnum {
102            A,
103            B(),
104            C {},
105        }
106
107        assert_order::<TestEnum, _, _>(["A", "B", "C"]);
108    }
109
110    #[test]
111    fn expect_order_panic() {
112        let thread = spawn(|| {
113            #[derive(VariantOrder)]
114            #[expect(unused)]
115            enum TestEnum {
116                A,
117                B(),
118                C {},
119            }
120
121            assert_order::<TestEnum, _, _>(["A", "C", "B"]);
122        });
123
124        let panic = thread.join().expect_err("expected panic");
125        let panic = panic
126            .downcast_ref::<String>()
127            .expect("panic as string")
128            .as_str();
129
130        assert_eq!(panic, "Variant name mismatch: Expected \"C\", got \"B\"");
131    }
132
133    #[test]
134    fn expect_too_long_panic() {
135        let thread = spawn(|| {
136            #[derive(VariantOrder)]
137            #[expect(unused)]
138            enum TestEnum {
139                A,
140                B(),
141                C {},
142            }
143
144            assert_order::<TestEnum, _, _>(["A", "B"]);
145        });
146
147        let panic = thread.join().expect_err("expected panic");
148        let panic = panic
149            .downcast_ref::<String>()
150            .expect("panic as string")
151            .as_str();
152
153        assert_eq!(panic, "unexpected length: expected 2, got 3");
154    }
155
156    #[test]
157    fn expect_too_short_panic() {
158        let thread = spawn(|| {
159            #[derive(VariantOrder)]
160            #[expect(unused)]
161            enum TestEnum {
162                A,
163                B(),
164                C {},
165            }
166
167            assert_order::<TestEnum, _, _>(["A", "B", "C", "D"]);
168        });
169
170        let panic = thread.join().expect_err("expected panic");
171        let panic = *(panic
172            .downcast_ref::<&'static str>()
173            .expect("panic as string"));
174
175        assert_eq!(panic, "Expected more canonical variants.");
176    }
177}