Skip to main content

vs_protocol/
codes.rs

1//! Short codes used on the wire.
2//!
3//! Tables match `docs/codes.md` at the workspace root. Every code
4//! enum implements [`Display`](std::fmt::Display) (the canonical wire
5//! form) and [`FromStr`] (returning [`UnknownCode`] on a stranger).
6
7use std::fmt;
8use std::str::FromStr;
9
10/// A code string that did not match any known variant of the requested
11/// kind. Returned by [`FromStr`] implementations on the code enums.
12#[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            /// The canonical wire-format string for this variant.
40            #[must_use]
41            pub const fn as_str(self) -> &'static str {
42                match self {
43                    $( Self::$variant => $wire, )+
44                }
45            }
46
47            /// Every variant of this enum, in source order.
48            #[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    /// A11y role code as it appears on the wire.
77    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    /// Operation an agent can perform on a tree node.
108    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    /// A protocol-level error code (the `! CODE` form on the wire).
121    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    /// A protocol-level warning code (the `? code` form on the wire).
139    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}