use std::borrow::Cow;
use std::fmt;
use serde::de::{Deserialize, Deserializer, Error, MapAccess, Visitor};
use serde::ser::{Serialize, SerializeStruct, Serializer};
use crate::json::Value;
use crate::model::channel::PermissionOverwrite;
use crate::model::guild::automod::{Action, EventType, TriggerMetadata, TriggerType};
use crate::model::guild::{
DefaultMessageNotificationLevel,
ExplicitContentFilter,
MfaLevel,
VerificationLevel,
};
use crate::model::id::{ApplicationId, ChannelId, GenericId, GuildId, RoleId, UserId};
use crate::model::sticker::StickerFormatType;
use crate::model::{Permissions, Timestamp};
#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)]
#[non_exhaustive]
pub struct AffectedRole {
pub id: RoleId,
pub name: String,
}
#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum EntityType {
Int(u64),
Str(String),
}
#[derive(Debug, PartialEq)]
#[cfg_attr(not(feature = "simd_json"), derive(Eq))]
#[non_exhaustive]
pub enum Change {
Actions {
old: Option<Vec<Action>>,
new: Option<Vec<Action>>,
},
AfkChannelId {
old: Option<ChannelId>,
new: Option<ChannelId>,
},
AfkTimeout {
old: Option<u64>,
new: Option<u64>,
},
Allow {
old: Option<Permissions>,
new: Option<Permissions>,
},
ApplicationId {
old: Option<ApplicationId>,
new: Option<ApplicationId>,
},
Archived {
old: Option<bool>,
new: Option<bool>,
},
Asset {
old: Option<String>,
new: Option<String>,
},
AutoArchiveDuration {
old: Option<u64>,
new: Option<u64>,
},
Available {
old: Option<bool>,
new: Option<bool>,
},
AvatarHash {
old: Option<String>,
new: Option<String>,
},
BannerHash {
old: Option<String>,
new: Option<String>,
},
Bitrate {
old: Option<u64>,
new: Option<u64>,
},
ChannelId {
old: Option<ChannelId>,
new: Option<ChannelId>,
},
Code {
old: Option<String>,
new: Option<String>,
},
Colour {
old: Option<u64>,
new: Option<u64>,
},
CommunicationDisabledUntil {
old: Option<Timestamp>,
new: Option<Timestamp>,
},
Deaf {
old: Option<bool>,
new: Option<bool>,
},
DefaultAutoArchiveDuration {
old: Option<u64>,
new: Option<u64>,
},
DefaultMessageNotifications {
old: Option<DefaultMessageNotificationLevel>,
new: Option<DefaultMessageNotificationLevel>,
},
Deny {
old: Option<Permissions>,
new: Option<Permissions>,
},
Description {
old: Option<String>,
new: Option<String>,
},
DiscoverySplashHash {
old: Option<String>,
new: Option<String>,
},
Enabled {
old: Option<bool>,
new: Option<bool>,
},
EnableEmoticons {
old: Option<bool>,
new: Option<bool>,
},
EntityType {
old: Option<u64>,
new: Option<u64>,
},
EventType {
old: Option<EventType>,
new: Option<EventType>,
},
ExemptChannels {
old: Option<Vec<ChannelId>>,
new: Option<Vec<ChannelId>>,
},
ExemptRoles {
old: Option<Vec<RoleId>>,
new: Option<Vec<RoleId>>,
},
ExpireBehavior {
old: Option<u64>,
new: Option<u64>,
},
ExpireGracePeriod {
old: Option<u64>,
new: Option<u64>,
},
ExplicitContentFilter {
old: Option<ExplicitContentFilter>,
new: Option<ExplicitContentFilter>,
},
FormatType {
old: Option<StickerFormatType>,
new: Option<StickerFormatType>,
},
GuildId {
old: Option<GuildId>,
new: Option<GuildId>,
},
Hoist {
old: Option<bool>,
new: Option<bool>,
},
IconHash {
old: Option<String>,
new: Option<String>,
},
ImageHash {
old: Option<String>,
new: Option<String>,
},
Id {
old: Option<GenericId>,
new: Option<GenericId>,
},
Invitable {
old: Option<bool>,
new: Option<bool>,
},
InviterId {
old: Option<UserId>,
new: Option<UserId>,
},
Location {
old: Option<String>,
new: Option<String>,
},
Locked {
old: Option<bool>,
new: Option<bool>,
},
MaxAge {
old: Option<u64>,
new: Option<u64>,
},
MaxUses {
old: Option<u64>,
new: Option<u64>,
},
Mentionable {
old: Option<bool>,
new: Option<bool>,
},
MfaLevel {
old: Option<MfaLevel>,
new: Option<MfaLevel>,
},
Mute {
old: Option<bool>,
new: Option<bool>,
},
Name {
old: Option<String>,
new: Option<String>,
},
Nick {
old: Option<String>,
new: Option<String>,
},
Nsfw {
old: Option<bool>,
new: Option<bool>,
},
OwnerId {
old: Option<UserId>,
new: Option<UserId>,
},
PermissionOverwrites {
old: Option<Vec<PermissionOverwrite>>,
new: Option<Vec<PermissionOverwrite>>,
},
Permissions {
old: Option<Permissions>,
new: Option<Permissions>,
},
Position {
old: Option<u64>,
new: Option<u64>,
},
PreferredLocale {
old: Option<String>,
new: Option<String>,
},
PrivacyLevel {
old: Option<u64>,
new: Option<u64>,
},
PruneDeleteDays {
old: Option<u64>,
new: Option<u64>,
},
PublicUpdatesChannelId {
old: Option<ChannelId>,
new: Option<ChannelId>,
},
RateLimitPerUser {
old: Option<u64>,
new: Option<u64>,
},
Region {
old: Option<String>,
new: Option<String>,
},
RolesAdded {
old: Option<Vec<AffectedRole>>,
new: Option<Vec<AffectedRole>>,
},
RolesRemove {
old: Option<Vec<AffectedRole>>,
new: Option<Vec<AffectedRole>>,
},
RulesChannelId {
old: Option<ChannelId>,
new: Option<ChannelId>,
},
SplashHash {
old: Option<String>,
new: Option<String>,
},
Status {
old: Option<u64>,
new: Option<u64>,
},
SystemChannelId {
old: Option<ChannelId>,
new: Option<ChannelId>,
},
Tags {
old: Option<String>,
new: Option<String>,
},
Temporary {
old: Option<bool>,
new: Option<bool>,
},
Topic {
old: Option<String>,
new: Option<String>,
},
TriggerMetadata {
old: Option<TriggerMetadata>,
new: Option<TriggerMetadata>,
},
TriggerType {
old: Option<TriggerType>,
new: Option<TriggerType>,
},
Type {
old: Option<EntityType>,
new: Option<EntityType>,
},
UnicodeEmoji {
old: Option<String>,
new: Option<String>,
},
UserLimit {
old: Option<u64>,
new: Option<u64>,
},
Uses {
old: Option<u64>,
new: Option<u64>,
},
VanityUrlCode {
old: Option<String>,
new: Option<String>,
},
VerificationLevel {
old: Option<VerificationLevel>,
new: Option<VerificationLevel>,
},
WidgetChannelId {
old: Option<ChannelId>,
new: Option<ChannelId>,
},
WidgetEnabled {
old: Option<bool>,
new: Option<bool>,
},
Other {
name: String,
old: Option<Value>,
new: Option<Value>,
},
}
impl Change {
#[must_use]
pub fn key(&self) -> Cow<'_, str> {
macro_rules! variant_keys {
($($Variant:ident: $key:literal,)*) => {
match self {
$(Self::$Variant { .. } => Cow::from($key),)*
Self::Other { name, .. } => Cow::from(name),
}
}
}
variant_keys! {
Actions: "actions",
AfkChannelId: "afk_channel_id",
AfkTimeout: "afk_timeout",
Allow: "allow",
ApplicationId: "application_id",
Archived: "archived",
Asset: "asset",
AutoArchiveDuration: "auto_archive_duration",
Available: "available",
AvatarHash: "avatar_hash",
BannerHash: "banner_hash",
Bitrate: "bitrate",
ChannelId: "channel_id",
Code: "code",
Colour: "color",
CommunicationDisabledUntil: "communication_disabled_until",
Deaf: "deaf",
DefaultAutoArchiveDuration: "default_auto_archive_duration",
DefaultMessageNotifications: "default_message_notifications",
Deny: "deny",
Description: "description",
DiscoverySplashHash: "discovery_splash_hash",
Enabled: "enabled",
EnableEmoticons: "enable_emoticons",
EntityType: "entity_type",
EventType: "event_type",
ExemptChannels: "exempt_channels",
ExemptRoles: "exempt_roles",
ExpireBehavior: "expire_behavior",
ExpireGracePeriod: "expire_grace_period",
ExplicitContentFilter: "explicit_content_filter",
FormatType: "format_type",
GuildId: "guild_id",
Hoist: "hoist",
IconHash: "icon_hash",
ImageHash: "image_hash",
Id: "id",
Invitable: "invitable",
InviterId: "inviter_id",
Location: "location",
Locked: "locked",
MaxAge: "max_age",
MaxUses: "max_uses",
Mentionable: "mentionable",
MfaLevel: "mfa_level",
Mute: "mute",
Name: "name",
Nick: "nick",
Nsfw: "nsfw",
OwnerId: "owner_id",
PermissionOverwrites: "permission_overwrites",
Permissions: "permissions",
Position: "position",
PreferredLocale: "preferred_locale",
PrivacyLevel: "privacy_level",
PruneDeleteDays: "prune_delete_days",
PublicUpdatesChannelId: "public_updates_channel_id",
RateLimitPerUser: "rate_limit_per_user",
Region: "region",
RolesAdded: "$add",
RolesRemove: "$remove",
RulesChannelId: "rules_channel_id",
SplashHash: "splash_hash",
Status: "status",
SystemChannelId: "system_channel_id",
Tags: "tags",
Temporary: "temporary",
Topic: "topic",
TriggerMetadata: "trigger_metadata",
TriggerType: "trigger_type",
Type: "type",
UnicodeEmoji: "unicode_emoji",
UserLimit: "user_limit",
Uses: "uses",
VanityUrlCode: "vanity_url_code",
VerificationLevel: "verification_level",
WidgetChannelId: "widget_channel_id",
WidgetEnabled: "widget_enabled",
}
}
}
impl Serialize for Change {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
macro_rules! serialize_variants {
(__impl $key:expr, $old:expr, $new:expr) => {{
let len = 1 + usize::from($old.is_some()) + usize::from($new.is_some());
let mut s = serializer.serialize_struct("Change", len)?;
s.serialize_field("key", &$key)?;
if $old.is_some() {
s.serialize_field("old_value", $old)?;
} else {
s.skip_field("old_value")?;
}
if $new.is_some() {
s.serialize_field("new_value", $new)?;
} else {
s.skip_field("new_value")?;
}
s.end()
}};
($($Variant:ident: $key:literal,)*) => {
match self {
$(Self::$Variant { old, new } => {
serialize_variants!(__impl $key, old, new)
},)*
Self::Other { name, old, new } => {
serialize_variants!(__impl name, old, new)
},
}
};
}
serialize_variants! {
Actions: "actions",
AfkChannelId: "afk_channel_id",
AfkTimeout: "afk_timeout",
Allow: "allow",
ApplicationId: "application_id",
Archived: "archived",
Asset: "asset",
AutoArchiveDuration: "auto_archive_duration",
Available: "available",
AvatarHash: "avatar_hash",
BannerHash: "banner_hash",
Bitrate: "bitrate",
ChannelId: "channel_id",
Code: "code",
Colour: "color",
CommunicationDisabledUntil: "communication_disabled_until",
Deaf: "deaf",
DefaultAutoArchiveDuration: "default_auto_archive_duration",
DefaultMessageNotifications: "default_message_notifications",
Deny: "deny",
Description: "description",
DiscoverySplashHash: "discovery_splash_hash",
Enabled: "enabled",
EnableEmoticons: "enable_emoticons",
EntityType: "entity_type",
EventType: "event_type",
ExemptChannels: "exempt_channels",
ExemptRoles: "exempt_roles",
ExpireBehavior: "expire_behavior",
ExpireGracePeriod: "expire_grace_period",
ExplicitContentFilter: "explicit_content_filter",
FormatType: "format_type",
GuildId: "guild_id",
Hoist: "hoist",
IconHash: "icon_hash",
ImageHash: "image_hash",
Id: "id",
Invitable: "invitable",
InviterId: "inviter_id",
Location: "location",
Locked: "locked",
MaxAge: "max_age",
MaxUses: "max_uses",
Mentionable: "mentionable",
MfaLevel: "mfa_level",
Mute: "mute",
Name: "name",
Nick: "nick",
Nsfw: "nsfw",
OwnerId: "owner_id",
PermissionOverwrites: "permission_overwrites",
Permissions: "permissions",
Position: "position",
PreferredLocale: "preferred_locale",
PrivacyLevel: "privacy_level",
PruneDeleteDays: "prune_delete_days",
PublicUpdatesChannelId: "public_updates_channel_id",
RateLimitPerUser: "rate_limit_per_user",
Region: "region",
RolesAdded: "$add",
RolesRemove: "$remove",
RulesChannelId: "rules_channel_id",
SplashHash: "splash_hash",
Status: "status",
SystemChannelId: "system_channel_id",
Tags: "tags",
Temporary: "temporary",
Topic: "topic",
TriggerMetadata: "trigger_metadata",
TriggerType: "trigger_type",
Type: "type",
UnicodeEmoji: "unicode_emoji",
UserLimit: "user_limit",
Uses: "uses",
VanityUrlCode: "vanity_url_code",
VerificationLevel: "verification_level",
WidgetChannelId: "widget_channel_id",
WidgetEnabled: "widget_enabled",
}
}
}
impl<'de> Deserialize<'de> for Change {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_any(ChangeVisitor)
}
}
struct ChangeVisitor;
impl<'de> Visitor<'de> for ChangeVisitor {
type Value = Change;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("Change enum")
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Change, A::Error> {
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "snake_case")]
enum Field {
Key,
OldValue,
NewValue,
}
let mut key: Option<MaybeUnknown<Key>> = None;
let mut old: Option<Option<serde_value::Value>> = None;
let mut new: Option<Option<serde_value::Value>> = None;
while let Some(k) = map.next_key()? {
match k {
Field::Key => {
if key.is_some() {
return Err(Error::duplicate_field("key"));
}
key = Some(map.next_value()?);
},
Field::OldValue => {
if old.is_some() {
return Err(Error::duplicate_field("old_value"));
}
old = Some(map.next_value()?);
},
Field::NewValue => {
if new.is_some() {
return Err(Error::duplicate_field("new_value"));
}
new = Some(map.next_value()?);
},
}
}
let key = key.ok_or_else(|| Error::missing_field("key"))?;
let old = old.unwrap_or_default();
let new = new.unwrap_or_default();
macro_rules! deserialize_variants {
($($Variant:ident: $Type:ty,)*) => {
match key {
$(MaybeUnknown::Known(Key::$Variant) => Change::$Variant {
old: old.map(<$Type>::deserialize).transpose().map_err(Error::custom)?,
new: new.map(<$Type>::deserialize).transpose().map_err(Error::custom)?,
},)*
MaybeUnknown::Unknown(name) => Change::Other {
name,
old: old.map(Value::deserialize).transpose().map_err(Error::custom)?,
new: new.map(Value::deserialize).transpose().map_err(Error::custom)?,
},
}
};
}
let change = deserialize_variants! {
Actions: Vec<Action>,
AfkChannelId: ChannelId,
AfkTimeout: u64,
Allow: Permissions,
ApplicationId: ApplicationId,
Archived: bool,
Asset: String,
AutoArchiveDuration: u64,
Available: bool,
AvatarHash: String,
BannerHash: String,
Bitrate: u64,
ChannelId: ChannelId,
Code: String,
Colour: u64,
CommunicationDisabledUntil: Timestamp,
Deaf: bool,
DefaultAutoArchiveDuration: u64,
DefaultMessageNotifications: DefaultMessageNotificationLevel,
Deny: Permissions,
Description: String,
DiscoverySplashHash: String,
Enabled: bool,
EnableEmoticons: bool,
EntityType: u64,
EventType: EventType,
ExemptChannels: Vec<ChannelId>,
ExemptRoles: Vec<RoleId>,
ExpireBehavior: u64,
ExpireGracePeriod: u64,
ExplicitContentFilter: ExplicitContentFilter,
FormatType: StickerFormatType,
GuildId: GuildId,
Hoist: bool,
IconHash: String,
ImageHash: String,
Id: GenericId,
Invitable: bool,
InviterId: UserId,
Location: String,
Locked: bool,
MaxAge: u64,
MaxUses: u64,
Mentionable: bool,
MfaLevel: MfaLevel,
Mute: bool,
Name: String,
Nick: String,
Nsfw: bool,
OwnerId: UserId,
PermissionOverwrites: Vec<PermissionOverwrite>,
Permissions: Permissions,
Position: u64,
PreferredLocale: String,
PrivacyLevel: u64,
PruneDeleteDays: u64,
PublicUpdatesChannelId: ChannelId,
RateLimitPerUser: u64,
Region: String,
RolesAdded: Vec<AffectedRole>,
RolesRemove: Vec<AffectedRole>,
RulesChannelId: ChannelId,
SplashHash: String,
Status: u64,
SystemChannelId: ChannelId,
Tags: String,
Temporary: bool,
Topic: String,
TriggerMetadata: TriggerMetadata,
TriggerType: TriggerType,
Type: EntityType,
UnicodeEmoji: String,
UserLimit: u64,
Uses: u64,
VanityUrlCode: String,
VerificationLevel: VerificationLevel,
WidgetChannelId: ChannelId,
WidgetEnabled: bool,
};
Ok(change)
}
}
#[derive(Deserialize)]
#[serde(untagged)]
enum MaybeUnknown<T> {
Known(T),
Unknown(String),
}
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "snake_case")]
enum Key {
Actions,
AfkChannelId,
AfkTimeout,
Allow,
ApplicationId,
Archived,
Asset,
AutoArchiveDuration,
Available,
AvatarHash,
BannerHash,
Bitrate,
ChannelId,
Code,
#[serde(rename = "color")]
Colour,
CommunicationDisabledUntil,
Deaf,
DefaultAutoArchiveDuration,
DefaultMessageNotifications,
Deny,
Description,
DiscoverySplashHash,
Enabled,
EnableEmoticons,
EntityType,
EventType,
ExemptChannels,
ExemptRoles,
ExpireBehavior,
ExpireGracePeriod,
ExplicitContentFilter,
FormatType,
GuildId,
Hoist,
IconHash,
ImageHash,
Id,
Invitable,
InviterId,
Location,
Locked,
MaxAge,
MaxUses,
Mentionable,
MfaLevel,
Mute,
Name,
Nick,
Nsfw,
OwnerId,
PermissionOverwrites,
Permissions,
Position,
PreferredLocale,
PrivacyLevel,
PruneDeleteDays,
PublicUpdatesChannelId,
RateLimitPerUser,
Region,
RulesChannelId,
SplashHash,
Status,
SystemChannelId,
Tags,
Temporary,
Topic,
TriggerMetadata,
TriggerType,
Type,
UnicodeEmoji,
UserLimit,
Uses,
VanityUrlCode,
VerificationLevel,
WidgetChannelId,
WidgetEnabled,
#[serde(rename = "$add")]
RolesAdded,
#[serde(rename = "$remove")]
RolesRemove,
}
#[cfg(test)]
mod tests {
use serde_test::{assert_tokens, Token};
use super::*;
#[test]
fn afk_channel_id_variant() {
let value = Change::AfkChannelId {
old: Some(ChannelId(1)),
new: Some(ChannelId(2)),
};
assert_tokens(&value, &[
Token::Struct {
name: "Change",
len: 3,
},
Token::Str("key"),
Token::Str("afk_channel_id"),
Token::Str("old_value"),
Token::Some,
Token::NewtypeStruct {
name: "ChannelId",
},
Token::Str("1"),
Token::Str("new_value"),
Token::Some,
Token::NewtypeStruct {
name: "ChannelId",
},
Token::Str("2"),
Token::StructEnd,
]);
}
#[test]
fn skip_serializing_if_none() {
let value = Change::AfkChannelId {
old: None,
new: Some(ChannelId(2)),
};
assert_tokens(&value, &[
Token::Struct {
name: "Change",
len: 2,
},
Token::Str("key"),
Token::Str("afk_channel_id"),
Token::Str("new_value"),
Token::Some,
Token::NewtypeStruct {
name: "ChannelId",
},
Token::Str("2"),
Token::StructEnd,
]);
let value = Change::AfkChannelId {
old: Some(ChannelId(1)),
new: None,
};
assert_tokens(&value, &[
Token::Struct {
name: "Change",
len: 2,
},
Token::Str("key"),
Token::Str("afk_channel_id"),
Token::Str("old_value"),
Token::Some,
Token::NewtypeStruct {
name: "ChannelId",
},
Token::Str("1"),
Token::StructEnd,
]);
}
#[test]
fn entity_type_variant() {
let value = Change::Type {
old: Some(EntityType::Int(123)),
new: Some(EntityType::Str("discord".into())),
};
assert_tokens(&value, &[
Token::Struct {
name: "Change",
len: 3,
},
Token::Str("key"),
Token::Str("type"),
Token::Str("old_value"),
Token::Some,
Token::U64(123),
Token::Str("new_value"),
Token::Some,
Token::Str("discord"),
Token::StructEnd,
]);
}
#[test]
fn permissions_variant() {
let value = Change::Permissions {
old: Some(Permissions::default()),
new: Some(Permissions::MANAGE_GUILD),
};
assert_tokens(&value, &[
Token::Struct {
name: "Change",
len: 3,
},
Token::Str("key"),
Token::Str("permissions"),
Token::Str("old_value"),
Token::Some,
Token::Str("0"),
Token::Str("new_value"),
Token::Some,
Token::Str("32"),
Token::StructEnd,
]);
}
}