use std::fmt;
use crate::class::{Class, Classify};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownVariant {
pub type_name: &'static str,
pub value: String,
}
impl fmt::Display for UnknownVariant {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?} is not a valid {}", self.value, self.type_name)
}
}
impl std::error::Error for UnknownVariant {}
impl Classify for UnknownVariant {
fn class(&self) -> Class {
Class::InvalidRequest
}
}
#[macro_export]
macro_rules! wire_enum {
(
$(#[$meta:meta])*
$vis:vis enum $name:ident {
$( $(#[$vmeta:meta])* $variant:ident => $wire:literal ),+ $(,)?
}
) => {
$(#[$meta])*
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, ::serde::Serialize, ::serde::Deserialize,
)]
$vis enum $name {
$( $(#[$vmeta])* #[serde(rename = $wire)] $variant, )+
}
impl $name {
pub const ALL: &'static [Self] = &[$( Self::$variant ),+];
pub fn as_str(self) -> &'static str {
match self {
$( Self::$variant => $wire, )+
}
}
}
impl ::core::fmt::Display for $name {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
f.write_str(self.as_str())
}
}
impl ::core::str::FromStr for $name {
type Err = $crate::UnknownVariant;
fn from_str(s: &str) -> ::core::result::Result<Self, Self::Err> {
match s {
$( $wire => ::core::result::Result::Ok(Self::$variant), )+
_ => ::core::result::Result::Err($crate::UnknownVariant {
type_name: ::core::stringify!($name),
value: ::std::borrow::ToOwned::to_owned(s),
}),
}
}
}
};
}
#[macro_export]
macro_rules! open_enum {
(
$(#[$meta:meta])*
$vis:vis enum $name:ident {
$( $(#[$vmeta:meta])* $variant:ident => $wire:literal ),+ $(,)?
}
) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
$vis enum $name {
$( $(#[$vmeta])* $variant, )+
Other(::std::string::String),
}
impl $name {
pub const ALL: &'static [Self] = &[$( Self::$variant ),+];
pub fn as_str(&self) -> &str {
match self {
$( Self::$variant => $wire, )+
Self::Other(raw) => raw,
}
}
pub fn from_wire(s: &str) -> Self {
match s {
$( $wire => Self::$variant, )+
other => Self::Other(::std::borrow::ToOwned::to_owned(other)),
}
}
}
impl ::core::fmt::Display for $name {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
f.write_str(self.as_str())
}
}
impl ::core::str::FromStr for $name {
type Err = ::core::convert::Infallible;
fn from_str(s: &str) -> ::core::result::Result<Self, Self::Err> {
::core::result::Result::Ok(Self::from_wire(s))
}
}
impl ::serde::Serialize for $name {
fn serialize<S: ::serde::Serializer>(
&self,
serializer: S,
) -> ::core::result::Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> ::serde::Deserialize<'de> for $name {
fn deserialize<D: ::serde::Deserializer<'de>>(
deserializer: D,
) -> ::core::result::Result<Self, D::Error> {
let raw = <::std::string::String as ::serde::Deserialize>::deserialize(deserializer)?;
::core::result::Result::Ok(match raw.as_str() {
$( $wire => Self::$variant, )+
_ => Self::Other(raw),
})
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
crate::wire_enum! {
#[non_exhaustive]
pub enum Interval {
M1 => "1m",
Mo1 => "1M",
}
}
crate::open_enum! {
pub enum State {
New => "STATE_NEW",
Confirmed => "STATE_CONFIRMED",
}
}
#[test]
fn an_unknown_variant_names_the_type_and_the_value() {
let err = UnknownVariant {
type_name: "Interval",
value: "2m".into(),
};
assert_eq!(err.to_string(), r#""2m" is not a valid Interval"#);
let boxed: Box<dyn std::error::Error> = Box::new(err);
assert!(boxed.source().is_none());
}
#[test]
fn an_unknown_variant_is_an_invalid_request() {
let err = "2m".parse::<Interval>().unwrap_err();
assert_eq!(err.class(), Class::InvalidRequest);
assert!(err.is_fault());
assert!(!err.is_retriable());
assert_eq!(Classify::retry_after(&err), None);
}
#[test]
fn a_wire_enum_round_trips_and_refuses_an_unknown_spelling() {
assert_eq!(Interval::ALL, &[Interval::M1, Interval::Mo1]);
for &variant in Interval::ALL {
let wire = variant.as_str();
assert_eq!(variant.to_string(), wire);
assert_eq!(wire.parse::<Interval>(), Ok(variant));
let json = serde_json::to_string(&variant).unwrap();
assert_eq!(json, format!("{wire:?}"));
assert_eq!(serde_json::from_str::<Interval>(&json).unwrap(), variant);
}
assert_eq!(
"2m".parse::<Interval>(),
Err(UnknownVariant {
type_name: "Interval",
value: "2m".into()
})
);
assert!(serde_json::from_str::<Interval>(r#""2m""#).is_err());
}
#[test]
fn an_open_enum_keeps_an_unknown_value_verbatim() {
let queued: State = serde_json::from_str(r#""STATE_QUEUED""#).unwrap();
assert_eq!(queued, State::Other("STATE_QUEUED".into()));
assert_eq!(queued.as_str(), "STATE_QUEUED");
assert_eq!(queued.to_string(), "STATE_QUEUED");
assert_eq!(serde_json::to_string(&queued).unwrap(), r#""STATE_QUEUED""#);
let known: State = serde_json::from_str(r#""STATE_NEW""#).unwrap();
assert_eq!(known, State::New);
assert_eq!(State::ALL, &[State::New, State::Confirmed]);
}
#[test]
fn from_wire_and_from_str_agree() {
for wire in ["STATE_NEW", "STATE_CONFIRMED", "STATE_QUEUED", ""] {
let Ok(parsed) = wire.parse::<State>();
assert_eq!(State::from_wire(wire), parsed, "{wire:?}");
assert_eq!(parsed.as_str(), wire);
}
}
}