1use std::fmt;
8use std::str::FromStr;
9
10#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
13#[error("unknown {kind} code: {value}")]
14pub struct UnknownCode {
15 pub kind: &'static str,
16 pub value: String,
17}
18
19macro_rules! code_enum {
20 (
21 $(#[$attr:meta])*
22 $vis:vis enum $name:ident as $kind:literal {
23 $(
24 $(#[$variant_attr:meta])*
25 $variant:ident => $wire:literal
26 ),+ $(,)?
27 }
28 ) => {
29 $(#[$attr])*
30 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
31 $vis enum $name {
32 $(
33 $(#[$variant_attr])*
34 $variant,
35 )+
36 }
37
38 impl $name {
39 #[must_use]
41 pub const fn as_str(self) -> &'static str {
42 match self {
43 $( Self::$variant => $wire, )+
44 }
45 }
46
47 #[must_use]
49 pub const fn all() -> &'static [Self] {
50 &[ $( Self::$variant, )+ ]
51 }
52 }
53
54 impl fmt::Display for $name {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 f.write_str(self.as_str())
57 }
58 }
59
60 impl FromStr for $name {
61 type Err = UnknownCode;
62 fn from_str(s: &str) -> Result<Self, Self::Err> {
63 Ok(match s {
64 $( $wire => Self::$variant, )+
65 other => return Err(UnknownCode {
66 kind: $kind,
67 value: other.to_string(),
68 }),
69 })
70 }
71 }
72 };
73}
74
75code_enum! {
76 pub enum Role as "role" {
78 Doc => "doc",
79 Btn => "btn",
80 Lnk => "lnk",
81 Tf => "tf",
82 Ta => "ta",
83 Sel => "sel",
84 Chk => "chk",
85 Rad => "rad",
86 Img => "img",
87 Hd => "hd",
88 P => "p",
89 Li => "li",
90 Lst => "lst",
91 Tbl => "tbl",
92 Row => "row",
93 Cell => "cell",
94 Hdr => "hdr",
95 Nav => "nav",
96 Frm => "frm",
97 Dlg => "dlg",
98 Itm => "itm",
99 Sec => "sec",
100 Art => "art",
101 Mn => "mn",
102 El => "el",
103 }
104}
105
106code_enum! {
107 pub enum Op as "op" {
109 Click => "click",
110 Fill => "fill",
111 Scroll => "scroll",
112 Key => "key",
113 Submit => "submit",
114 Hover => "hover",
115 Focus => "focus",
116 }
117}
118
119code_enum! {
120 pub enum ErrorCode as "error" {
122 StaleToken => "STALE_TOKEN",
123 EngineUnsupported => "ENGINE_UNSUPPORTED",
124 PolicyDeny => "POLICY_DENY",
125 Timeout => "TIMEOUT",
126 NotFound => "NOT_FOUND",
127 ConfirmRequired => "CONFIRM_REQUIRED",
128 DaemonStartFailed => "DAEMON_START_FAILED",
129 EngineCrash => "ENGINE_CRASH",
130 BadRequest => "BAD_REQUEST",
131 UnknownKind => "UNKNOWN_KIND",
132 EvalError => "EVAL_ERROR",
133 EvalSyntax => "EVAL_SYNTAX",
134 }
135}
136
137code_enum! {
138 pub enum WarningCode as "warning" {
140 Nav => "nav",
141 CaptchaVisible => "captcha_visible",
142 AuthLoaded => "auth_loaded",
143 ViewportChanged => "viewport_changed",
144 IdempotentHit => "idempotent_hit",
145 ConsoleError => "console_error",
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 fn round_trip<C>(samples: &[C])
154 where
155 C: Copy + PartialEq + fmt::Debug + fmt::Display + FromStr<Err = UnknownCode>,
156 {
157 for c in samples {
158 let s = c.to_string();
159 let parsed: C = s.parse().expect("known code");
160 assert_eq!(parsed, *c);
161 }
162 }
163
164 #[test]
165 fn roles_round_trip() {
166 round_trip(Role::all());
167 }
168
169 #[test]
170 fn ops_round_trip() {
171 round_trip(Op::all());
172 }
173
174 #[test]
175 fn error_codes_round_trip() {
176 round_trip(ErrorCode::all());
177 }
178
179 #[test]
180 fn warning_codes_round_trip() {
181 round_trip(WarningCode::all());
182 }
183
184 #[test]
185 fn unknown_role_rejected() {
186 let err = "xx".parse::<Role>().unwrap_err();
187 assert_eq!(err.kind, "role");
188 assert_eq!(err.value, "xx");
189 }
190
191 #[test]
192 fn unknown_error_rejected() {
193 let err = "WHATEVER".parse::<ErrorCode>().unwrap_err();
194 assert_eq!(err.kind, "error");
195 }
196
197 #[test]
198 fn role_display_matches_wire() {
199 assert_eq!(Role::Btn.to_string(), "btn");
200 assert_eq!(Role::Lnk.as_str(), "lnk");
201 }
202
203 #[test]
204 fn role_count() {
205 assert_eq!(Role::all().len(), 25);
206 assert_eq!(Op::all().len(), 7);
207 }
208}