diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 21bd07d475f..2b2bfd7ac22 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -7,7 +7,7 @@ version = "0.0.0" [dev-dependencies] anyhow = { default-features = false, features = ["std"], version = "1" } -ed25519-dalek = "1" +ed25519-dalek = "2" futures-util = { default-features = false, version = "0.3" } hex = "0.4" hyper = { features = ["client", "server", "http2", "runtime"], version = "0.14" } diff --git a/examples/gateway-reshard.rs b/examples/gateway-reshard.rs index 4e0364b3591..df5ec0ce962 100644 --- a/examples/gateway-reshard.rs +++ b/examples/gateway-reshard.rs @@ -28,7 +28,7 @@ async fn main() -> anyhow::Result<()> { // Run `gateway_runner` and `reshard` concurrently until the first one // finishes. tokio::select! { - // Gateway_runner only finises on errors, so break the loop and exit + // Gateway_runner only finishes on errors, so break the loop and exit // the program. _ = gateway_runner(Arc::clone(&client), shards) => break, // Resharding complete! Time to run `gateway_runner` with the new diff --git a/examples/model-webhook-slash.rs b/examples/model-webhook-slash.rs index 08f0d1fbf89..a4783d0e2ed 100644 --- a/examples/model-webhook-slash.rs +++ b/examples/model-webhook-slash.rs @@ -1,4 +1,4 @@ -use ed25519_dalek::{PublicKey, Verifier, PUBLIC_KEY_LENGTH}; +use ed25519_dalek::{Verifier, VerifyingKey, PUBLIC_KEY_LENGTH}; use hex::FromHex; use hyper::{ header::CONTENT_TYPE, @@ -16,8 +16,8 @@ use twilight_model::{ }; /// Public key given from Discord. -static PUB_KEY: Lazy = Lazy::new(|| { - PublicKey::from_bytes(&<[u8; PUBLIC_KEY_LENGTH] as FromHex>::from_hex("PUBLIC_KEY").unwrap()) +static PUB_KEY: Lazy = Lazy::new(|| { + VerifyingKey::from_bytes(&<[u8; PUBLIC_KEY_LENGTH] as FromHex>::from_hex("PUBLIC_KEY").unwrap()) .unwrap() }); diff --git a/twilight-cache-inmemory/src/event/reaction.rs b/twilight-cache-inmemory/src/event/reaction.rs index 7ef5894257b..d18983e9bb6 100644 --- a/twilight-cache-inmemory/src/event/reaction.rs +++ b/twilight-cache-inmemory/src/event/reaction.rs @@ -1,6 +1,6 @@ use crate::{config::ResourceType, InMemoryCache, UpdateCache}; use twilight_model::{ - channel::message::{Reaction, ReactionType}, + channel::message::{Reaction, ReactionCountDetails, ReactionType}, gateway::payload::incoming::{ ReactionAdd, ReactionRemove, ReactionRemoveAll, ReactionRemoveEmoji, }, @@ -39,9 +39,15 @@ impl UpdateCache for ReactionAdd { .unwrap_or_default(); message.reactions.push(Reaction { + burst_colors: Vec::new(), count: 1, + count_details: ReactionCountDetails { + burst: 0, + normal: 1, + }, emoji: self.0.emoji.clone(), me, + me_burst: false, }); } } diff --git a/twilight-cache-inmemory/src/lib.rs b/twilight-cache-inmemory/src/lib.rs index d2693462ae2..1c657fbfa2c 100644 --- a/twilight-cache-inmemory/src/lib.rs +++ b/twilight-cache-inmemory/src/lib.rs @@ -861,7 +861,7 @@ impl UpdateCache for Event { Event::GuildStickersUpdate(v) => c.update(v), Event::GuildUpdate(v) => c.update(v.deref()), Event::IntegrationCreate(v) => c.update(v.deref()), - Event::IntegrationDelete(v) => c.update(v.deref()), + Event::IntegrationDelete(v) => c.update(v), Event::IntegrationUpdate(v) => c.update(v.deref()), Event::InteractionCreate(v) => c.update(v.deref()), Event::MemberAdd(v) => c.update(v.deref()), diff --git a/twilight-gateway-queue/src/in_memory.rs b/twilight-gateway-queue/src/in_memory.rs index 4f80d771849..cd9f8c2ba1a 100644 --- a/twilight-gateway-queue/src/in_memory.rs +++ b/twilight-gateway-queue/src/in_memory.rs @@ -58,6 +58,7 @@ async fn runner( .take(max_concurrency.into()) .collect::>(); + #[allow(clippy::ignored_unit_patterns)] loop { tokio::select! { biased; diff --git a/twilight-gateway-queue/src/lib.rs b/twilight-gateway-queue/src/lib.rs index d7b43d8375f..426b90d21bf 100644 --- a/twilight-gateway-queue/src/lib.rs +++ b/twilight-gateway-queue/src/lib.rs @@ -24,7 +24,7 @@ pub const LIMIT_PERIOD: Duration = Duration::from_secs(60 * 60 * 24); /// Abstraction for types processing gateway identify requests. /// -/// For convenience in twilight-gateway, implementors must also implement +/// For convenience in twilight-gateway, implementers must also implement /// [`Debug`]. pub trait Queue: Debug { /// Enqueue a shard with this ID. diff --git a/twilight-gateway/src/config.rs b/twilight-gateway/src/config.rs index 746cea144f7..0aa5974cef8 100644 --- a/twilight-gateway/src/config.rs +++ b/twilight-gateway/src/config.rs @@ -67,7 +67,7 @@ pub struct Config { /// Session information to resume a shard on initialization. session: Option, /// TLS connector for Websocket connections. - // We need this to be public so [`stream`] can re-use TLS on multiple shards + // We need this to be public so [`stream`] can reuse TLS on multiple shards // if unconfigured. tls: Arc, /// Token used to authenticate when identifying with the gateway. diff --git a/twilight-http/CHANGELOG.md b/twilight-http/CHANGELOG.md index 52790cfc697..9879da5426d 100644 --- a/twilight-http/CHANGELOG.md +++ b/twilight-http/CHANGELOG.md @@ -1581,7 +1581,7 @@ Replace references to `Path::WebhooksIdTokenMessageId` with `CreateInvite::{max_age, max_uses}` now return validation errors, so the results returned from them need to be handled. -Don't re-use `hyper` clients via the builder. If you need to configure the +Don't reuse `hyper` clients via the builder. If you need to configure the underlying `hyper` client please create an issue with the reason why. Errors are no longer enums and don't expose their concrete underlying error @@ -1632,7 +1632,7 @@ Return validation errors for `CreateInvite::max_age` and Remove ability to get current user's DM channels ([#782] - [@vivian]). Remove `ClientBuilder::hyper_client` and `From for Client` which -were available to re-use `hyper` clients ([#768] - [@vivian]). +were available to reuse `hyper` clients ([#768] - [@vivian]). Return updated copy of member when updating a member ([#758] - [@vivian]). diff --git a/twilight-http/src/request/guild/ban/create_ban.rs b/twilight-http/src/request/guild/ban/create_ban.rs index 0809fa15571..6d32418e685 100644 --- a/twilight-http/src/request/guild/ban/create_ban.rs +++ b/twilight-http/src/request/guild/ban/create_ban.rs @@ -5,6 +5,7 @@ use crate::{ response::{marker::EmptyBody, Response, ResponseFuture}, routing::Route, }; +use serde::Serialize; use std::future::IntoFuture; use twilight_model::id::{ marker::{GuildMarker, UserMarker}, @@ -16,7 +17,9 @@ use twilight_validate::request::{ ValidationError, }; +#[derive(Serialize)] struct CreateBanFields { + /// Number of seconds to delete messages for, between `0` and `604800`. delete_message_seconds: Option, } @@ -120,10 +123,10 @@ impl TryIntoRequest for CreateBan<'_> { fn try_into_request(self) -> Result { let fields = self.fields.map_err(Error::validation)?; let mut request = Request::builder(&Route::CreateBan { - delete_message_seconds: fields.delete_message_seconds, guild_id: self.guild_id.get(), user_id: self.user_id.get(), - }); + }) + .json(&fields); if let Some(reason) = self.reason.map_err(Error::validation)? { request = request.headers(request::audit_header(reason)?); @@ -156,10 +159,11 @@ mod tests { let client = Client::new(String::new()); let request = client .create_ban(GUILD_ID, USER_ID) + .delete_message_seconds(100) .reason(REASON) .try_into_request()?; - assert!(request.body().is_none()); + assert!(request.body().is_some()); assert!(request.form().is_none()); assert_eq!(Method::Put, request.method()); diff --git a/twilight-http/src/request/guild/create_guild_channel.rs b/twilight-http/src/request/guild/create_guild_channel.rs index 45c063db316..f9473743477 100644 --- a/twilight-http/src/request/guild/create_guild_channel.rs +++ b/twilight-http/src/request/guild/create_guild_channel.rs @@ -42,6 +42,12 @@ struct CreateGuildChannelFields<'a> { default_reaction_emoji: Option<&'a DefaultReaction>, #[serde(skip_serializing_if = "Option::is_none")] default_sort_order: Option, + /// Initial `rate_limit_per_user` to set on newly created threads in a channel. + /// This field is copied to the thread at creation time and does not live update. + /// + /// This field is only applicable for text, announcement, media, and forum channels. + #[serde(skip_serializing_if = "Option::is_none")] + default_thread_rate_limit_per_user: Option, #[serde(rename = "type", skip_serializing_if = "Option::is_none")] kind: Option, name: &'a str, @@ -86,6 +92,7 @@ impl<'a> CreateGuildChannel<'a> { default_forum_layout: None, default_reaction_emoji: None, default_sort_order: None, + default_thread_rate_limit_per_user: None, kind: None, name, nsfw: None, @@ -185,6 +192,29 @@ impl<'a> CreateGuildChannel<'a> { self } + /// Set the default number of seconds that a user must wait before before they are + /// able to send another message in any newly-created thread in the channel. + /// + /// This field is only applicable for text, announcement, media, and forum channels. + /// The minimum is 0 and the maximum is 21600. This is also known as "Slow Mode". See + /// [Discord Docs/Channel Object]. + /// + /// [Discord Docs/Channel Object]: https://discordapp.com/developers/docs/resources/channel#channel-object-channel-structure + pub fn default_thread_rate_limit_per_user( + mut self, + default_thread_rate_limit_per_user: u16, + ) -> Self { + self.fields = self.fields.and_then(|mut fields| { + validate_rate_limit_per_user(default_thread_rate_limit_per_user)?; + + fields.default_thread_rate_limit_per_user = Some(default_thread_rate_limit_per_user); + + Ok(fields) + }); + + self + } + /// Set the kind of channel. pub fn kind(mut self, kind: ChannelType) -> Self { if let Ok(fields) = self.fields.as_mut() { diff --git a/twilight-http/src/routing.rs b/twilight-http/src/routing.rs index ce7fa8df8af..82466e57b45 100644 --- a/twilight-http/src/routing.rs +++ b/twilight-http/src/routing.rs @@ -33,9 +33,6 @@ pub enum Route<'a> { }, /// Route information to create a ban on a user in a guild. CreateBan { - /// The number of seconds' worth of the user's messages to delete in the - /// guild's channels. - delete_message_seconds: Option, /// The ID of the guild. guild_id: u64, /// The ID of the user. @@ -1695,24 +1692,6 @@ impl Display for Route<'_> { f.write_str("/auto-moderation/rules") } - Route::CreateBan { - guild_id, - delete_message_seconds, - user_id, - } => { - f.write_str("guilds/")?; - Display::fmt(guild_id, f)?; - f.write_str("/bans/")?; - Display::fmt(user_id, f)?; - f.write_str("?")?; - - if let Some(delete_message_seconds) = delete_message_seconds { - f.write_str("delete_message_seconds=")?; - Display::fmt(delete_message_seconds, f)?; - } - - Ok(()) - } Route::CreateChannel { guild_id } | Route::GetChannels { guild_id } | Route::UpdateGuildChannels { guild_id } => { @@ -1955,7 +1934,9 @@ impl Display for Route<'_> { f.write_str("/crosspost") } - Route::DeleteBan { guild_id, user_id } | Route::GetBan { guild_id, user_id } => { + Route::DeleteBan { guild_id, user_id } + | Route::GetBan { guild_id, user_id } + | Route::CreateBan { guild_id, user_id } => { f.write_str("guilds/")?; Display::fmt(guild_id, f)?; f.write_str("/bans/")?; @@ -4346,22 +4327,20 @@ mod tests { fn create_ban() { let mut route = Route::CreateBan { guild_id: GUILD_ID, - delete_message_seconds: None, user_id: USER_ID, }; assert_eq!( route.to_string(), - format!("guilds/{GUILD_ID}/bans/{USER_ID}?") + format!("guilds/{GUILD_ID}/bans/{USER_ID}") ); route = Route::CreateBan { guild_id: GUILD_ID, - delete_message_seconds: Some(259_200), user_id: USER_ID, }; assert_eq!( route.to_string(), - format!("guilds/{GUILD_ID}/bans/{USER_ID}?delete_message_seconds=259200") + format!("guilds/{GUILD_ID}/bans/{USER_ID}") ); } diff --git a/twilight-mention/src/timestamp.rs b/twilight-mention/src/timestamp.rs index 33ff54396fe..be93cc5b648 100644 --- a/twilight-mention/src/timestamp.rs +++ b/twilight-mention/src/timestamp.rs @@ -163,7 +163,7 @@ impl Ord for Timestamp { impl PartialOrd for Timestamp { fn partial_cmp(&self, other: &Timestamp) -> Option { - self.unix.partial_cmp(&other.unix) + Some(self.cmp(other)) } } diff --git a/twilight-model/src/channel/channel_type.rs b/twilight-model/src/channel/channel_type.rs index a77aab2810c..ec57aa1f55b 100644 --- a/twilight-model/src/channel/channel_type.rs +++ b/twilight-model/src/channel/channel_type.rs @@ -20,6 +20,12 @@ pub enum ChannelType { GuildDirectory, /// Channel that can only contain threads. GuildForum, + /// Channel the can only contain threads with media content. + /// + /// See the [help center article] for more information. + /// + /// [help center article]: https://creator-support.discord.com/hc/en-us/articles/14346342766743 + GuildMedia, Unknown(u8), } @@ -38,6 +44,7 @@ impl From for ChannelType { 13 => ChannelType::GuildStageVoice, 14 => ChannelType::GuildDirectory, 15 => ChannelType::GuildForum, + 16 => ChannelType::GuildMedia, unknown => ChannelType::Unknown(unknown), } } @@ -58,6 +65,7 @@ impl From for u8 { ChannelType::GuildStageVoice => 13, ChannelType::GuildDirectory => 14, ChannelType::GuildForum => 15, + ChannelType::GuildMedia => 16, ChannelType::Unknown(unknown) => unknown, } } @@ -77,6 +85,7 @@ impl ChannelType { /// - [`GuildVoice`][`Self::GuildVoice`] /// - [`PublicThread`][`Self::PublicThread`] /// - [`PrivateThread`][`Self::PrivateThread`] + /// - [`GuildMedia`][`Self::GuildMedia`] pub const fn is_guild(self) -> bool { matches!( self, @@ -89,6 +98,7 @@ impl ChannelType { | Self::GuildStageVoice | Self::GuildText | Self::GuildVoice + | Self::GuildMedia ) } @@ -121,6 +131,7 @@ impl ChannelType { Self::Private => "Private", Self::PrivateThread => "PrivateThread", Self::PublicThread => "PublicThread", + Self::GuildMedia => "GuildMedia", Self::Unknown(_) => "Unknown", } } @@ -141,6 +152,7 @@ mod tests { const_assert!(ChannelType::GuildStageVoice.is_guild()); const_assert!(ChannelType::GuildText.is_guild()); const_assert!(ChannelType::GuildVoice.is_guild()); + const_assert!(ChannelType::GuildMedia.is_guild()); const_assert!(ChannelType::AnnouncementThread.is_thread()); const_assert!(ChannelType::PublicThread.is_thread()); @@ -159,6 +171,8 @@ mod tests { serde_test::assert_tokens(&ChannelType::PrivateThread, &[Token::U8(12)]); serde_test::assert_tokens(&ChannelType::GuildStageVoice, &[Token::U8(13)]); serde_test::assert_tokens(&ChannelType::GuildDirectory, &[Token::U8(14)]); + serde_test::assert_tokens(&ChannelType::GuildForum, &[Token::U8(15)]); + serde_test::assert_tokens(&ChannelType::GuildMedia, &[Token::U8(16)]); serde_test::assert_tokens(&ChannelType::Unknown(99), &[Token::U8(99)]); } @@ -175,6 +189,7 @@ mod tests { assert_eq!("Private", ChannelType::Private.name()); assert_eq!("PrivateThread", ChannelType::PrivateThread.name()); assert_eq!("PublicThread", ChannelType::PublicThread.name()); + assert_eq!("GuildMedia", ChannelType::GuildMedia.name()); assert_eq!("Unknown", ChannelType::Unknown(99).name()); } } diff --git a/twilight-model/src/channel/message/mod.rs b/twilight-model/src/channel/message/mod.rs index 6dba6c160d8..cf262e16be6 100644 --- a/twilight-model/src/channel/message/mod.rs +++ b/twilight-model/src/channel/message/mod.rs @@ -26,7 +26,7 @@ pub use self::{ interaction::MessageInteraction, kind::MessageType, mention::Mention, - reaction::{Reaction, ReactionType}, + reaction::{Reaction, ReactionCountDetails, ReactionType}, reference::MessageReference, role_subscription_data::RoleSubscriptionData, sticker::Sticker, @@ -196,6 +196,7 @@ pub struct Message { #[cfg(test)] mod tests { use super::{ + reaction::ReactionCountDetails, sticker::{MessageSticker, StickerFormatType}, Message, MessageActivity, MessageActivityType, MessageApplication, MessageFlags, MessageReference, MessageType, Reaction, ReactionType, @@ -478,11 +479,17 @@ mod tests { mentions: Vec::new(), pinned: false, reactions: vec![Reaction { + burst_colors: Vec::new(), count: 7, + count_details: ReactionCountDetails { + burst: 0, + normal: 7, + }, emoji: ReactionType::Unicode { name: "a".to_owned(), }, me: true, + me_burst: false, }], reference: Some(MessageReference { channel_id: Some(Id::new(1)), @@ -653,10 +660,23 @@ mod tests { Token::Seq { len: Some(1) }, Token::Struct { name: "Reaction", - len: 3, + len: 6, }, + Token::Str("burst_colors"), + Token::Seq { len: Some(0) }, + Token::SeqEnd, Token::Str("count"), Token::U64(7), + Token::Str("count_details"), + Token::Struct { + name: "ReactionCountDetails", + len: 2, + }, + Token::Str("burst"), + Token::U64(0), + Token::Str("normal"), + Token::U64(7), + Token::StructEnd, Token::Str("emoji"), Token::Struct { name: "ReactionType", @@ -667,6 +687,8 @@ mod tests { Token::StructEnd, Token::Str("me"), Token::Bool(true), + Token::Str("me_burst"), + Token::Bool(false), Token::StructEnd, Token::SeqEnd, Token::Str("message_reference"), diff --git a/twilight-model/src/channel/message/reaction.rs b/twilight-model/src/channel/message/reaction.rs index e1ddee51af8..6538529554a 100644 --- a/twilight-model/src/channel/message/reaction.rs +++ b/twilight-model/src/channel/message/reaction.rs @@ -1,15 +1,24 @@ -use crate::id::{marker::EmojiMarker, Id}; +use crate::{ + id::{marker::EmojiMarker, Id}, + util::HexColor, +}; use serde::{Deserialize, Serialize}; /// Reaction below a message. #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct Reaction { + /// HEX colors used for super reaction. + pub burst_colors: Vec, /// Amount of reactions this emoji has. pub count: u64, + /// Reaction count details for each type of reaction. + pub count_details: ReactionCountDetails, /// Emoji of this reaction. pub emoji: ReactionType, /// Whether the current user has reacted with this emoji. pub me: bool, + /// Whether the current user super-reacted using this emoji + pub me_burst: bool, } /// Type of [`Reaction`]. @@ -46,20 +55,35 @@ pub enum ReactionType { }, } +/// Breakdown of normal and super reaction counts for the associated emoji. +#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)] +pub struct ReactionCountDetails { + /// Count of super reactions. + pub burst: u64, + /// Count of normal reactions. + pub normal: u64, +} + #[cfg(test)] mod tests { - use super::{Reaction, ReactionType}; - use crate::id::Id; + use super::{Reaction, ReactionCountDetails, ReactionType}; + use crate::{id::Id, util::HexColor}; use serde_test::Token; #[test] fn message_reaction_unicode() { let value = Reaction { + burst_colors: Vec::from([HexColor(255, 255, 255)]), count: 7, + count_details: ReactionCountDetails { + burst: 0, + normal: 7, + }, emoji: ReactionType::Unicode { name: "a".to_owned(), }, me: true, + me_burst: false, }; serde_test::assert_tokens( @@ -67,10 +91,24 @@ mod tests { &[ Token::Struct { name: "Reaction", - len: 3, + len: 6, }, + Token::Str("burst_colors"), + Token::Seq { len: Some(1) }, + Token::Str("#FFFFFF"), + Token::SeqEnd, Token::Str("count"), Token::U64(7), + Token::Str("count_details"), + Token::Struct { + name: "ReactionCountDetails", + len: 2, + }, + Token::Str("burst"), + Token::U64(0), + Token::Str("normal"), + Token::U64(7), + Token::StructEnd, Token::Str("emoji"), Token::Struct { name: "ReactionType", @@ -81,6 +119,8 @@ mod tests { Token::StructEnd, Token::Str("me"), Token::Bool(true), + Token::Str("me_burst"), + Token::Bool(false), Token::StructEnd, ], ); diff --git a/twilight-model/src/id/mod.rs b/twilight-model/src/id/mod.rs index cc6e078dc50..afab72b5a42 100644 --- a/twilight-model/src/id/mod.rs +++ b/twilight-model/src/id/mod.rs @@ -382,7 +382,7 @@ impl PartialEq> for u64 { impl PartialOrd for Id { fn partial_cmp(&self, other: &Self) -> Option { - self.value.partial_cmp(&other.value) + Some(self.cmp(other)) } } diff --git a/twilight-model/src/util/hex_color.rs b/twilight-model/src/util/hex_color.rs new file mode 100644 index 00000000000..fb66b0ed6b9 --- /dev/null +++ b/twilight-model/src/util/hex_color.rs @@ -0,0 +1,130 @@ +use std::fmt::Formatter; +use std::fmt::{Display, Result as FmtResult}; +use std::num::ParseIntError; +use std::str::FromStr; + +use serde::de::Visitor; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +/// Represents a color in the RGB format using hexadecimal notation. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct HexColor( + /// Red component of the color. + pub u8, + /// Green component of the color. + pub u8, + /// Blue component of the color. + pub u8, +); + +impl Display for HexColor { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + f.write_fmt(format_args!("#{:02X}{:02X}{:02X}", self.0, self.1, self.2)) + } +} + +impl Serialize for HexColor { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&self.to_string()) + } +} + +pub enum HexColorParseError { + InvalidLength, + InvalidFormat, + InvalidCharacter(ParseIntError), +} + +impl From for HexColorParseError { + fn from(err: ParseIntError) -> Self { + Self::InvalidCharacter(err) + } +} + +impl FromStr for HexColor { + type Err = HexColorParseError; + + fn from_str(s: &str) -> Result { + if !s.starts_with('#') { + return Err(HexColorParseError::InvalidFormat); + } + + let s = s.trim_start_matches('#'); + + let (r, g, b) = match s.len() { + 3 => ( + u8::from_str_radix(&s[0..1], 16)?, + u8::from_str_radix(&s[1..2], 16)?, + u8::from_str_radix(&s[2..3], 16)?, + ), + 6 => ( + u8::from_str_radix(&s[0..2], 16)?, + u8::from_str_radix(&s[2..4], 16)?, + u8::from_str_radix(&s[4..6], 16)?, + ), + _ => return Err(HexColorParseError::InvalidLength), + }; + + Ok(Self(r, g, b)) + } +} + +struct HexColorVisitor; + +impl<'de> Visitor<'de> for HexColorVisitor { + type Value = HexColor; + + fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + formatter.write_str("a hex color string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + HexColor::from_str(v).map_err(|_| E::custom("invalid hex color")) + } +} + +impl<'de> Deserialize<'de> for HexColor { + fn deserialize>(deserializer: D) -> Result { + deserializer.deserialize_str(HexColorVisitor) + } +} + +#[cfg(test)] +mod tests { + use super::HexColor; + + #[test] + fn hex_color_display() { + let hex_color = HexColor(255, 255, 255); + assert_eq!(hex_color.to_string(), "#FFFFFF"); + } + + #[test] + fn serialize() { + let hex_color = HexColor(252, 177, 3); + let serialized = serde_json::to_string(&hex_color).unwrap(); + assert_eq!(serialized, "\"#FCB103\""); + } + + #[test] + fn serialize_2() { + let hex_color = HexColor(255, 255, 255); + let serialized = serde_json::to_string(&hex_color).unwrap(); + assert_eq!(serialized, "\"#FFFFFF\""); + } + + #[test] + fn deserialize() { + let deserialized: HexColor = serde_json::from_str("\"#FFFFFF\"").unwrap(); + assert_eq!(deserialized, HexColor(255, 255, 255)); + } + + #[test] + fn deserialize_invalid() { + let deserialized: Result = serde_json::from_str("\"#GGGGGG\""); + assert!(deserialized.is_err()); + } +} diff --git a/twilight-model/src/util/mod.rs b/twilight-model/src/util/mod.rs index b1d0a6e9f5f..c30df90e120 100644 --- a/twilight-model/src/util/mod.rs +++ b/twilight-model/src/util/mod.rs @@ -1,9 +1,10 @@ //! Utilities for efficiently parsing and representing data from Discord's API. pub mod datetime; +pub mod hex_color; pub mod image_hash; -pub use self::{datetime::Timestamp, image_hash::ImageHash}; +pub use self::{datetime::Timestamp, hex_color::HexColor, image_hash::ImageHash}; #[allow(clippy::trivially_copy_pass_by_ref)] pub(crate) fn is_false(value: &bool) -> bool { diff --git a/twilight-standby/src/lib.rs b/twilight-standby/src/lib.rs index 25b75cc889c..4d60c1f23d5 100644 --- a/twilight-standby/src/lib.rs +++ b/twilight-standby/src/lib.rs @@ -849,7 +849,7 @@ impl Standby { // // A form of enumeration can't be used because sometimes the index // doesn't advance; iterators would continue to provide incrementing - // enumeration indexes while we sometimes want to re-use an index. + // enumeration indexes while we sometimes want to reuse an index. let mut index = 0; let mut results = ProcessResults::new();