Skip to content

Commit

Permalink
Preserve values for unknown enum variants (#2008)
Browse files Browse the repository at this point in the history
The `enum_number!` macro now takes the whole enum definition and generates
`From` trait implementations to convert a value to the enum and back.

The implementations can then be picked up by `serde` with
`#[serde(from = "u8", into = "u8")]` to (de)serialize the types.

```
enum_number! {
    #[derive(Clone, Copy, Debug, Deserialize, Serialize)]
    #[serde(from = "u8", into = "u8")]
    pub enum Foo {
        A = 1,
        B = 2,
        _ => Unknown(u8),
    }
}
```

BREAKING CHANGE: The `Unknown` variant now takes the unknown value
and the removed `fn num() -> u64` method can be replaced
with `let v = u8::from(kind)`.
  • Loading branch information
nickelc authored Jun 24, 2022
1 parent 3c103b8 commit 4cc0151
Show file tree
Hide file tree
Showing 18 changed files with 575 additions and 665 deletions.
81 changes: 34 additions & 47 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,55 +31,42 @@ pub const USER_AGENT: &str = concat!(
")"
);

/// An enum representing the [gateway opcodes].
///
/// [gateway opcodes]: https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[non_exhaustive]
pub enum Opcode {
/// Dispatches an event.
Dispatch = 0,
/// Used for ping checking.
Heartbeat = 1,
/// Used for client handshake.
Identify = 2,
/// Used to update the client status.
PresenceUpdate = 3,
/// Used to join/move/leave voice channels.
VoiceStateUpdate = 4,
/// Used for voice ping checking.
VoiceServerPing = 5,
/// Used to resume a closed connection.
Resume = 6,
/// Used to tell clients to reconnect to the gateway.
Reconnect = 7,
/// Used to request guild members.
RequestGuildMembers = 8,
/// Used to notify clients that they have an invalid session Id.
InvalidSession = 9,
/// Sent immediately after connection, contains heartbeat + server info.
Hello = 10,
/// Sent immediately following a client heartbeat that was received.
HeartbeatAck = 11,
/// Unknown opcode.
Unknown = !0,
enum_number! {
/// An enum representing the [gateway opcodes].
///
/// [gateway opcodes]: https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(from = "u8", into = "u8")]
#[non_exhaustive]
pub enum Opcode {
/// Dispatches an event.
Dispatch = 0,
/// Used for ping checking.
Heartbeat = 1,
/// Used for client handshake.
Identify = 2,
/// Used to update the client status.
PresenceUpdate = 3,
/// Used to join/move/leave voice channels.
VoiceStateUpdate = 4,
/// Used for voice ping checking.
VoiceServerPing = 5,
/// Used to resume a closed connection.
Resume = 6,
/// Used to tell clients to reconnect to the gateway.
Reconnect = 7,
/// Used to request guild members.
RequestGuildMembers = 8,
/// Used to notify clients that they have an invalid session Id.
InvalidSession = 9,
/// Sent immediately after connection, contains heartbeat + server info.
Hello = 10,
/// Sent immediately following a client heartbeat that was received.
HeartbeatAck = 11,
_ => Unknown(u8),
}
}

enum_number!(Opcode {
Dispatch,
Heartbeat,
Identify,
PresenceUpdate,
VoiceStateUpdate,
VoiceServerPing,
Resume,
Reconnect,
RequestGuildMembers,
InvalidSession,
Hello,
HeartbeatAck,
});

pub mod close_codes {
/// Unknown error; try reconnecting?
///
Expand Down
160 changes: 115 additions & 45 deletions src/internal/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,60 +42,103 @@ macro_rules! feature_cache {
}};
}

/// The `enum_number!` macro generates `From` implementations to convert between values and the
/// enum which can then be utilized by `serde` with `#[serde(from = "u8", into = "u8")]`.
///
/// When defining the enum like this:
/// ```ignore
/// enum_number! {
/// /// The `Foo` enum
/// #[derive(Clone, Copy, Deserialize, Serialize)]
/// #[serde(from = "u8", into = "u8")]
/// pub enum Foo {
/// /// First
/// Aah = 1,
/// /// Second
/// Bar = 2,
/// _ => Unknown(u8),
/// }
/// }
/// ```
///
/// Code like this will be generated:
///
/// ```
/// # use serde::{Deserialize, Serialize};
/// #
/// /// The `Foo` enum
/// #[derive(Clone, Copy, Deserialize, Serialize)]
/// #[serde(from = "u8", into = "u8")]
/// pub enum Foo {
/// /// First
/// Aah,
/// /// Second,
/// Bar,
/// /// Variant value is unknown.
/// Unknown(u8),
/// }
///
/// impl From<u8> for Foo {
/// fn from(value: u8) -> Self {
/// match value {
/// 1 => Self::Aah,
/// 2 => Self::Bar,
/// unknown => Self::Unknown(unknown),
/// }
/// }
/// }
///
/// impl From<Foo> for u8 {
/// fn from(value: Foo) -> Self {
/// match value {
/// Foo::Aah => 1,
/// Foo::Bar => 2,
/// Foo::Unknown(unknown) => unknown,
/// }
/// }
/// }
/// ```
macro_rules! enum_number {
($name:ident { $($(#[$attr:meta])? $variant:ident $(,)? )* }) => {
impl $name {
#[inline]
#[must_use]
pub fn num(&self) -> u64 {
*self as u64
}
(
$(#[$outer:meta])*
$vis:vis enum $Enum:ident {
$(
$(#[$inner:ident $($args:tt)*])*
$Variant:ident = $value:literal,
)*
_ => Unknown($T:ty),
}
) => {
$(#[$outer])*
$vis enum $Enum {
$(
$(#[$inner $($args)*])*
$Variant,
)*
/// Variant value is unknown.
Unknown($T),
}

impl serde::Serialize for $name {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where S: serde::Serializer
{
// Serialize the enum as a u64.
serializer.serialize_u64(*self as u64)
impl From<$T> for $Enum {
fn from(value: $T) -> Self {
#[allow(unused_doc_comments)]
match value {
$($(#[$inner $($args)*])* $value => Self::$Variant,)*
unknown => Self::Unknown(unknown),
}
}
}

impl<'de> serde::Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where D: serde::Deserializer<'de>
{
struct Visitor;

impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = $name;

fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>)
-> std::fmt::Result {
formatter.write_str("positive integer")
}

fn visit_u64<E>(self, value: u64) -> std::result::Result<$name, E>
where E: serde::de::Error
{
// Rust does not come with a simple way of converting a
// number to an enum, so use a big `match`.
match value {
$( $(#[$attr])? v if v == $name::$variant as u64 => Ok($name::$variant), )*
_ => {
tracing::warn!("Unknown {} value: {}", stringify!($name), value);

Ok($name::Unknown)
}
}
}
impl From<$Enum> for $T {
fn from(value: $Enum) -> Self {
#[allow(unused_doc_comments)]
match value {
$($(#[$inner $($args)*])* $Enum::$Variant => $value,)*
$Enum::Unknown(unknown) => unknown,
}

// Deserialize the enum from a u64.
deserializer.deserialize_u64(Visitor)
}
}
}
};
}

/// The macro forwards the generation to the `bitflags::bitflags!` macro and implements
Expand Down Expand Up @@ -147,3 +190,30 @@ macro_rules! bitflags {
};
() => {};
}

#[cfg(test)]
mod tests {
use serde_test::Token;

#[test]
fn enum_number() {
enum_number! {
#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(from = "u8", into = "u8")]
pub enum T {
/// AAA
A = 1,
/// BBB
B = 2,
/// CCC
C = 3,
_ => Unknown(u8),
}
}

serde_test::assert_tokens(&T::A, &[Token::U8(1)]);
serde_test::assert_tokens(&T::B, &[Token::U8(2)]);
serde_test::assert_tokens(&T::C, &[Token::U8(3)]);
serde_test::assert_tokens(&T::Unknown(123), &[Token::U8(123)]);
}
}
20 changes: 9 additions & 11 deletions src/model/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,17 @@ pub struct TeamMember {
pub user: User,
}

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[non_exhaustive]
pub enum MembershipState {
Invited = 1,
Accepted = 2,
Unknown = !0,
enum_number! {
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
#[serde(from = "u8", into = "u8")]
#[non_exhaustive]
pub enum MembershipState {
Invited = 1,
Accepted = 2,
_ => Unknown(u8),
}
}

enum_number!(MembershipState {
Invited,
Accepted
});

bitflags! {
/// The flags of the application.
#[derive(Default)]
Expand Down
Loading

0 comments on commit 4cc0151

Please sign in to comment.