Skip to main content

sip_header/
macros.rs

1/// Generates a non-exhaustive enum mapping Rust variants to canonical protocol strings.
2///
3/// Produces: enum definition, `ALL` const, `as_str()`, `Display`, `AsRef<str>`,
4/// and `FromStr`. `FromStr` uses `eq_ignore_ascii_case` — appropriate for
5/// user-facing catalog types (header names, variable names) where input may
6/// come from config files. Wire protocol state types use hand-written strict
7/// `FromStr` instead.
8///
9/// Two error forms:
10///
11/// - `error_type: ParseMyEnumError,` — the error newtype is defined separately
12///   by the caller as `struct ParseMyEnumError(pub String)`; its `Display` is
13///   the caller's to keep free of the input.
14/// - `error_type: ParseMyEnumError => "unknown my value",` — the newtype, its
15///   `Display` (`"unknown my value (<n> bytes)"`, the rejected input stays on
16///   the field), and `std::error::Error` are generated.
17///
18/// An optional leading `tests_mod: my_enum_tests,` generates a `#[cfg(test)]`
19/// module with round-trip, case-insensitivity, `Display`, and unknown-input
20/// tests over `ALL` (requires `PartialEq` on the error type).
21///
22/// # Example
23///
24/// ```ignore
25/// define_header_enum! {
26///     tests_mod: my_enum_tests,
27///     error_type: ParseMyEnumError => "unknown my value",
28///     /// Doc comment for the enum.
29///     pub enum MyEnum {
30///         Foo => "foo-wire",
31///         Bar => "bar-wire",
32///     }
33/// }
34/// ```
35#[macro_export]
36macro_rules! define_header_enum {
37    (
38        $(tests_mod: $tests_mod:ident,)?
39        error_type: $Err:ident $(=> $err_msg:literal)?,
40        $(#[$enum_meta:meta])*
41        $vis:vis enum $Name:ident {
42            $(
43                $(#[$var_meta:meta])*
44                $variant:ident => $wire:literal
45            ),+ $(,)?
46        }
47    ) => {
48        $(
49            #[doc = concat!("Error for an unrecognized value; displays as `", $err_msg, " (<n> bytes)`. The rejected input is the field.")]
50            #[derive(Debug, Clone, PartialEq, Eq)]
51            $vis struct $Err(pub String);
52
53            impl std::fmt::Display for $Err {
54                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55                    write!(f, concat!($err_msg, " ({} bytes)"), self.0.len())
56                }
57            }
58
59            impl std::error::Error for $Err {}
60        )?
61
62        $(#[$enum_meta])*
63        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65        #[non_exhaustive]
66        #[allow(missing_docs)]
67        $vis enum $Name {
68            $(
69                $(#[$var_meta])*
70                $variant,
71            )+
72        }
73
74        impl $Name {
75            /// All variants, in declaration order (respecting any `#[cfg]` attributes).
76            // allow(unused_doc_comments): variant doc attrs are propagated onto
77            // array elements so that #[cfg] attrs also propagate; the doc attrs
78            // are harmless noise here. Same pattern in as_str/from_str below.
79            #[allow(unused_doc_comments)]
80            pub const ALL: &'static [Self] = &[
81                $(
82                    $(#[$var_meta])*
83                    $Name::$variant,
84                )+
85            ];
86
87            /// Canonical protocol string.
88            #[allow(unused_doc_comments)]
89            pub fn as_str(&self) -> &'static str {
90                match self {
91                    $(
92                        $(#[$var_meta])*
93                        $Name::$variant => $wire,
94                    )+
95                }
96            }
97        }
98
99        impl std::fmt::Display for $Name {
100            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101                f.write_str(self.as_str())
102            }
103        }
104
105        impl AsRef<str> for $Name {
106            fn as_ref(&self) -> &str {
107                self.as_str()
108            }
109        }
110
111        impl std::str::FromStr for $Name {
112            type Err = $Err;
113
114            #[allow(unused_doc_comments)]
115            fn from_str(s: &str) -> Result<Self, Self::Err> {
116                $(
117                    $(#[$var_meta])*
118                    if s.eq_ignore_ascii_case($wire) {
119                        return Ok($Name::$variant);
120                    }
121                )+
122                Err($Err(s.to_string()))
123            }
124        }
125
126        $(
127            #[cfg(test)]
128            mod $tests_mod {
129                use super::{$Err, $Name};
130
131                #[test]
132                fn round_trip() {
133                    for v in $Name::ALL {
134                        assert_eq!(v.as_str().parse::<$Name>(), Ok(*v));
135                    }
136                }
137
138                #[test]
139                fn case_insensitive() {
140                    for v in $Name::ALL {
141                        assert_eq!(v.as_str().to_lowercase().parse::<$Name>(), Ok(*v));
142                        assert_eq!(v.as_str().to_uppercase().parse::<$Name>(), Ok(*v));
143                    }
144                }
145
146                #[test]
147                fn display_matches_as_str() {
148                    for v in $Name::ALL {
149                        assert_eq!(v.to_string(), v.as_str());
150                    }
151                }
152
153                #[test]
154                fn unknown_input_err() {
155                    let input = "\u{0}no-such-value\u{0}";
156                    assert_eq!(input.parse::<$Name>(), Err($Err(input.to_string())));
157                }
158            }
159        )?
160    };
161}
162
163/// Implements `FromStr` by delegating to an inherent `parse(&str)` method.
164macro_rules! impl_from_str_via_parse {
165    ($Type:ty, $Err:ty) => {
166        impl std::str::FromStr for $Type {
167            type Err = $Err;
168
169            fn from_str(s: &str) -> Result<Self, Self::Err> {
170                Self::parse(s)
171            }
172        }
173    };
174}
175
176#[cfg(test)]
177mod tests {
178    define_header_enum! {
179        tests_mod: test_enum_generated,
180        error_type: ParseTestEnumError => "unknown test value",
181        /// Exercises generated error newtype, `ALL`, and test module.
182        pub(crate) enum TestEnum {
183            /// `Foo-Wire`.
184            Foo => "Foo-Wire",
185            /// `Bar-Wire`.
186            Bar => "Bar-Wire",
187            /// `Draft-Wire`.
188            #[cfg(feature = "draft")]
189            Draft => "Draft-Wire",
190        }
191    }
192
193    /// Hand-written error for the old-form invocation.
194    #[derive(Debug, Clone, PartialEq, Eq)]
195    pub(crate) struct ParseOldEnumError(pub String);
196
197    impl std::fmt::Display for ParseOldEnumError {
198        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199            write!(f, "unknown old value: {}", self.0)
200        }
201    }
202
203    impl std::error::Error for ParseOldEnumError {}
204
205    define_header_enum! {
206        error_type: ParseOldEnumError,
207        /// Old-form invocation stays source-compatible.
208        pub(crate) enum OldEnum {
209            /// `Old-Wire`.
210            One => "Old-Wire",
211        }
212    }
213
214    #[test]
215    fn generated_error_display() {
216        let e = ParseTestEnumError("nope".to_string());
217        assert_eq!(e.to_string(), "unknown test value (4 bytes)");
218    }
219
220    #[test]
221    fn all_const_respects_cfg() {
222        #[cfg(not(feature = "draft"))]
223        assert_eq!(TestEnum::ALL, &[TestEnum::Foo, TestEnum::Bar]);
224        #[cfg(feature = "draft")]
225        assert_eq!(
226            TestEnum::ALL,
227            &[TestEnum::Foo, TestEnum::Bar, TestEnum::Draft]
228        );
229    }
230
231    #[test]
232    fn old_form_generates_all() {
233        assert_eq!(OldEnum::ALL, &[OldEnum::One]);
234        assert_eq!("old-wire".parse::<OldEnum>(), Ok(OldEnum::One));
235    }
236}