Skip to main content

polyoxide_venue/
wire.rs

1//! Wire enums: one spelling per variant, written once.
2
3use std::fmt;
4
5use crate::class::{Class, Classify};
6
7/// A string that is not one of a closed set's wire spellings.
8///
9/// A [`wire_enum!`](crate::wire_enum) type's `FromStr` returns it.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct UnknownVariant {
12    /// The Rust type being parsed.
13    pub type_name: &'static str,
14    /// The offending input.
15    pub value: String,
16}
17
18impl fmt::Display for UnknownVariant {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        write!(f, "{:?} is not a valid {}", self.value, self.type_name)
21    }
22}
23
24impl std::error::Error for UnknownVariant {}
25
26/// An `InvalidRequest`: the caller parsed a spelling no variant has. Only
27/// `FromStr` builds one, on a value the caller supplies; a server frame whose
28/// spelling does not parse is the venue's decode error instead.
29impl Classify for UnknownVariant {
30    fn class(&self) -> Class {
31        Class::InvalidRequest
32    }
33}
34
35/// Declares a closed set the client sends: one wire spelling per variant.
36///
37/// The enum derives `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `Hash` and
38/// serde's `Serialize` and `Deserialize`, renamed to the wire spellings, and
39/// gets `ALL`, `as_str`, `Display` and a `FromStr` whose error is
40/// [`UnknownVariant`]. Attributes written above the enum, such as
41/// `#[non_exhaustive]`, are kept, and so are those above each variant.
42///
43/// The expansion derives `::serde::Serialize` and `::serde::Deserialize`,
44/// so the calling crate depends on serde with its `derive` feature.
45///
46/// ```
47/// polyoxide_venue::wire_enum! {
48///     /// Side of a trade or position.
49///     pub enum Side { Long => "long", Short => "short" }
50/// }
51///
52/// assert_eq!(Side::Long.as_str(), "long");
53/// assert_eq!("short".parse::<Side>(), Ok(Side::Short));
54/// assert_eq!(Side::ALL, &[Side::Long, Side::Short]);
55/// let err = "flat".parse::<Side>().unwrap_err();
56/// assert_eq!(err.to_string(), r#""flat" is not a valid Side"#);
57/// ```
58#[macro_export]
59macro_rules! wire_enum {
60    (
61        $(#[$meta:meta])*
62        $vis:vis enum $name:ident {
63            $( $(#[$vmeta:meta])* $variant:ident => $wire:literal ),+ $(,)?
64        }
65    ) => {
66        $(#[$meta])*
67        #[derive(
68            Debug, Clone, Copy, PartialEq, Eq, Hash, ::serde::Serialize, ::serde::Deserialize,
69        )]
70        $vis enum $name {
71            $( $(#[$vmeta])* #[serde(rename = $wire)] $variant, )+
72        }
73
74        impl $name {
75            /// Every variant, in declaration order.
76            pub const ALL: &'static [Self] = &[$( Self::$variant ),+];
77
78            /// The wire spelling.
79            pub fn as_str(self) -> &'static str {
80                match self {
81                    $( Self::$variant => $wire, )+
82                }
83            }
84        }
85
86        impl ::core::fmt::Display for $name {
87            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
88                f.write_str(self.as_str())
89            }
90        }
91
92        impl ::core::str::FromStr for $name {
93            type Err = $crate::UnknownVariant;
94
95            fn from_str(s: &str) -> ::core::result::Result<Self, Self::Err> {
96                match s {
97                    $( $wire => ::core::result::Result::Ok(Self::$variant), )+
98                    _ => ::core::result::Result::Err($crate::UnknownVariant {
99                        type_name: ::core::stringify!($name),
100                        value: ::std::borrow::ToOwned::to_owned(s),
101                    }),
102                }
103            }
104        }
105    };
106}
107
108/// Declares a set the venue reports and extends over time: a spelling this
109/// version does not know is kept verbatim in `Other` instead of failing the
110/// response.
111///
112/// The enum is `#[non_exhaustive]` and derives `Debug`, `Clone`, `PartialEq`,
113/// `Eq` and `Hash`. It gets `ALL` (the known variants), `as_str`,
114/// `from_wire`, `Display`, a `FromStr` that never fails, and serde's
115/// `Serialize` and `Deserialize` as a string. Attributes written above the
116/// enum and each variant are kept.
117///
118/// The expansion implements serde's `Serialize` and `Deserialize` by hand,
119/// naming `::serde`, so the calling crate depends on serde, without needing
120/// its `derive` feature.
121///
122/// ```
123/// polyoxide_venue::open_enum! {
124///     /// Lifecycle state of a transaction.
125///     pub enum State {
126///         /// Accepted.
127///         New => "STATE_NEW",
128///         /// Confirmed on chain.
129///         Confirmed => "STATE_CONFIRMED",
130///     }
131/// }
132///
133/// assert_eq!(State::from_wire("STATE_NEW"), State::New);
134/// let queued = State::from_wire("STATE_QUEUED");
135/// assert_eq!(queued, State::Other("STATE_QUEUED".into()));
136/// assert_eq!(queued.as_str(), "STATE_QUEUED");
137/// assert_eq!(State::ALL, &[State::New, State::Confirmed]);
138/// ```
139#[macro_export]
140macro_rules! open_enum {
141    (
142        $(#[$meta:meta])*
143        $vis:vis enum $name:ident {
144            $( $(#[$vmeta:meta])* $variant:ident => $wire:literal ),+ $(,)?
145        }
146    ) => {
147        $(#[$meta])*
148        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
149        #[non_exhaustive]
150        $vis enum $name {
151            $( $(#[$vmeta])* $variant, )+
152            /// A value this version of the SDK does not recognise, kept verbatim.
153            /// Sending it in a request passes the string through unchanged.
154            ///
155            /// Build it with `from_wire` or `FromStr`, never directly:
156            /// `Other` holding a known spelling serialises as the named variant
157            /// does, but does not compare equal to it.
158            Other(::std::string::String),
159        }
160
161        impl $name {
162            /// Every variant this SDK knows, in declaration order. `Other` is
163            /// not among them: it holds whatever else the server sends.
164            pub const ALL: &'static [Self] = &[$( Self::$variant ),+];
165
166            /// The wire spelling.
167            pub fn as_str(&self) -> &str {
168                match self {
169                    $( Self::$variant => $wire, )+
170                    Self::Other(raw) => raw,
171                }
172            }
173
174            /// Parse a wire spelling; anything unrecognised becomes `Other`.
175            pub fn from_wire(s: &str) -> Self {
176                match s {
177                    $( $wire => Self::$variant, )+
178                    other => Self::Other(::std::borrow::ToOwned::to_owned(other)),
179                }
180            }
181        }
182
183        impl ::core::fmt::Display for $name {
184            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
185                f.write_str(self.as_str())
186            }
187        }
188
189        impl ::core::str::FromStr for $name {
190            type Err = ::core::convert::Infallible;
191
192            fn from_str(s: &str) -> ::core::result::Result<Self, Self::Err> {
193                ::core::result::Result::Ok(Self::from_wire(s))
194            }
195        }
196
197        impl ::serde::Serialize for $name {
198            fn serialize<S: ::serde::Serializer>(
199                &self,
200                serializer: S,
201            ) -> ::core::result::Result<S::Ok, S::Error> {
202                serializer.serialize_str(self.as_str())
203            }
204        }
205
206        impl<'de> ::serde::Deserialize<'de> for $name {
207            fn deserialize<D: ::serde::Deserializer<'de>>(
208                deserializer: D,
209            ) -> ::core::result::Result<Self, D::Error> {
210                let raw = <::std::string::String as ::serde::Deserialize>::deserialize(deserializer)?;
211                ::core::result::Result::Ok(match raw.as_str() {
212                    $( $wire => Self::$variant, )+
213                    _ => Self::Other(raw),
214                })
215            }
216        }
217    };
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    crate::wire_enum! {
225        /// A closed test set, with a non-identifier spelling.
226        #[non_exhaustive]
227        pub enum Interval {
228            /// One minute.
229            M1 => "1m",
230            /// One month, which differs from `1m` only in case.
231            Mo1 => "1M",
232        }
233    }
234
235    crate::open_enum! {
236        /// An open test set.
237        pub enum State {
238            /// Accepted.
239            New => "STATE_NEW",
240            /// Confirmed on chain.
241            Confirmed => "STATE_CONFIRMED",
242        }
243    }
244
245    #[test]
246    fn an_unknown_variant_names_the_type_and_the_value() {
247        let err = UnknownVariant {
248            type_name: "Interval",
249            value: "2m".into(),
250        };
251        assert_eq!(err.to_string(), r#""2m" is not a valid Interval"#);
252        let boxed: Box<dyn std::error::Error> = Box::new(err);
253        assert!(boxed.source().is_none());
254    }
255
256    #[test]
257    fn an_unknown_variant_is_an_invalid_request() {
258        let err = "2m".parse::<Interval>().unwrap_err();
259        assert_eq!(err.class(), Class::InvalidRequest);
260        assert!(err.is_fault());
261        assert!(!err.is_retriable());
262        assert_eq!(Classify::retry_after(&err), None);
263    }
264
265    #[test]
266    fn a_wire_enum_round_trips_and_refuses_an_unknown_spelling() {
267        assert_eq!(Interval::ALL, &[Interval::M1, Interval::Mo1]);
268        for &variant in Interval::ALL {
269            let wire = variant.as_str();
270            assert_eq!(variant.to_string(), wire);
271            assert_eq!(wire.parse::<Interval>(), Ok(variant));
272            let json = serde_json::to_string(&variant).unwrap();
273            assert_eq!(json, format!("{wire:?}"));
274            assert_eq!(serde_json::from_str::<Interval>(&json).unwrap(), variant);
275        }
276        assert_eq!(
277            "2m".parse::<Interval>(),
278            Err(UnknownVariant {
279                type_name: "Interval",
280                value: "2m".into()
281            })
282        );
283        assert!(serde_json::from_str::<Interval>(r#""2m""#).is_err());
284    }
285
286    #[test]
287    fn an_open_enum_keeps_an_unknown_value_verbatim() {
288        let queued: State = serde_json::from_str(r#""STATE_QUEUED""#).unwrap();
289        assert_eq!(queued, State::Other("STATE_QUEUED".into()));
290        assert_eq!(queued.as_str(), "STATE_QUEUED");
291        assert_eq!(queued.to_string(), "STATE_QUEUED");
292        assert_eq!(serde_json::to_string(&queued).unwrap(), r#""STATE_QUEUED""#);
293
294        let known: State = serde_json::from_str(r#""STATE_NEW""#).unwrap();
295        assert_eq!(known, State::New);
296        assert_eq!(State::ALL, &[State::New, State::Confirmed]);
297    }
298
299    #[test]
300    fn from_wire_and_from_str_agree() {
301        for wire in ["STATE_NEW", "STATE_CONFIRMED", "STATE_QUEUED", ""] {
302            let Ok(parsed) = wire.parse::<State>();
303            assert_eq!(State::from_wire(wire), parsed, "{wire:?}");
304            assert_eq!(parsed.as_str(), wire);
305        }
306    }
307}