diff --git a/Cargo.toml b/Cargo.toml index 8f2a399..6138138 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,21 +1,15 @@ [package] name = "webhook" version = "2.1.2" -edition = "2018" +authors = ["Thomas"] +edition = "2024" description = "Discord Webhook API Wrapper" readme = "README.md" repository = "https://github.com/thoo0224/webhook-rs" license = "MIT" keywords = ["discord", "discord-api", "webhook", "discord-webhook"] -authors = ["Thomas"] -publish = true - -exclude = [ - "examples/*", - ".env" -] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +categories = ["api-bindings"] +exclude = ["examples/*"] [features] default = ["client"] @@ -24,7 +18,12 @@ full = ["client", "models"] models = [] [dependencies] -hyper = { version = "0.14.16", features = ["client", "http1", "http2", "tcp"], optional = true } +hyper = { version = "0.14.16", features = [ + "client", + "http1", + "http2", + "tcp", +], optional = true } hyper-tls = { version = "0.5.0", features = ["vendored"], optional = true } serde = { version = "1.0.131", features = ["derive"] } @@ -32,4 +31,4 @@ serde_json = "1.0.72" [dev-dependencies] tokio = { version = "1.14.0", features = ["full"] } -dotenv = "0.15.0" \ No newline at end of file +dotenv = "0.15.0" diff --git a/examples/example.rs b/examples/example.rs index 0562388..306e996 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -1,7 +1,8 @@ use webhook::client::{WebhookClient, WebhookResult}; use webhook::models::NonLinkButtonStyle; -const IMAGE_URL: &'static str = "https://cdn.discordapp.com/avatars/312157715449249795/a_b8b3b0c35f3dee2b6586a0dd58697e29.png"; +const IMAGE_URL: &str = + "https://cdn.discordapp.com/avatars/312157715449249795/a_b8b3b0c35f3dee2b6586a0dd58697e29.png"; #[tokio::main] async fn main() -> WebhookResult<()> { @@ -12,26 +13,36 @@ async fn main() -> WebhookResult<()> { let webhook_info = client.get_information().await?; println!("webhook: {:?}", webhook_info); - client.send(|message| message - .content("@everyone") - .username("Thoo") - .avatar_url(IMAGE_URL) - .embed(|embed| embed - .title("Webhook") - .description("Hello, World!") - .footer("Footer", Some(String::from(IMAGE_URL))) - .image(IMAGE_URL) - .thumbnail(IMAGE_URL) - .author("Lmao#0001", Some(String::from(IMAGE_URL)), Some(String::from(IMAGE_URL))) - .field("name", "value", false))).await?; + client + .send(|message| { + message + .content("@everyone") + .username("Thoo") + .avatar_url(IMAGE_URL) + .embed(|embed| { + embed + .title("Webhook") + .description("Hello, World!") + .footer("Footer", Some(String::from(IMAGE_URL))) + .image(IMAGE_URL) + .thumbnail(IMAGE_URL) + .author( + "Lmao#0001", + Some(String::from(IMAGE_URL)), + Some(String::from(IMAGE_URL)), + ) + .field("name", "value", false) + }) + }) + .await?; + + application_webhook_example(&url).await?; Ok(()) } -// to try out using application webhook run: -// `application_webhook_example(&url).await?;` async fn application_webhook_example(url: &str) -> WebhookResult<()> { - let client = WebhookClient::new(&url); + let client = WebhookClient::new(url); let webhook_info = client.get_information().await?; println!("webhook: {:?}", webhook_info); @@ -48,17 +59,17 @@ async fn application_webhook_example(url: &str) -> WebhookResult<()> { .emoji("625891304081063986", "mage", false) .custom_id("id_0") }) - .regular_button(|button| { - button - .style(NonLinkButtonStyle::Secondary) - .label("Secondary!") - .emoji("625891304081063986", "mage", false) - .custom_id("id_1") - }) - .link_button(|button| button.label("Click Me!").url("https://discord.com")) + .regular_button(|button| { + button + .style(NonLinkButtonStyle::Secondary) + .label("Secondary!") + .emoji("625891304081063986", "mage", false) + .custom_id("id_1") + }) + .link_button(|button| button.label("Click Me!").url("https://discord.com")) }) }) .await?; Ok(()) -} \ No newline at end of file +} diff --git a/src/client.rs b/src/client.rs index f3113c4..dd00517 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,7 +7,7 @@ use std::str::FromStr; use crate::models::{DiscordApiCompatible, Message, MessageContext, Webhook}; -pub type WebhookResult = std::result::Result>; +pub type WebhookResult = std::result::Result>; /// A Client that sends webhooks for discord. pub struct WebhookClient { @@ -16,6 +16,7 @@ pub struct WebhookClient { } impl WebhookClient { + #[must_use] pub fn new(url: &str) -> Self { let https_connector = HttpsConnector::new(); let client = Client::builder().build::<_, hyper::Body>(https_connector); @@ -32,22 +33,22 @@ impl WebhookClient { /// .content("content") /// .username("username")).await?; /// ``` - pub async fn send(&self, function: Func) -> WebhookResult + pub async fn send(&self, function: F) -> WebhookResult where - Func: Fn(&mut Message) -> &mut Message, + F: Fn(&mut Message) -> &mut Message, { let mut message = Message::new(); function(&mut message); let mut message_context = MessageContext::new(); match message.check_compatibility(&mut message_context) { - Ok(_) => (), + Ok(()) => (), Err(error_message) => { return Err(Box::new(std::io::Error::new( std::io::ErrorKind::InvalidInput, error_message, ))); } - }; + } let result = self.send_message(&message).await?; Ok(result) @@ -93,31 +94,27 @@ impl WebhookClient { #[cfg(test)] mod tests { - use crate::models::{ActionRow, DiscordApiCompatible, Embed, EmbedAuthor, EmbedField, EmbedFooter, Message, MessageContext, NonLinkButtonStyle}; + use crate::models::{ + ActionRow, DiscordApiCompatible, Embed, EmbedAuthor, EmbedField, EmbedFooter, Message, + MessageContext, NonLinkButtonStyle, + }; - fn assert_message_error( - message_build: BuildFunc, - msg_pred: MessagePred, - ) + fn assert_message_error(message_build: B, message_pred: P) where - BuildFunc: Fn(&mut Message) -> &mut Message, - MessagePred: Fn(&str) -> bool, + B: Fn(&mut Message) -> &mut Message, + P: Fn(&str) -> bool, { let mut message = Message::new(); message_build(&mut message); match message.check_compatibility(&mut MessageContext::new()) { Err(err) => { - assert!( - msg_pred(&err.to_string()), - "Unexpected error message {}", - err.to_string() - ) + assert!(message_pred(&err), "Unexpected error message {err}"); } - Ok(_) => assert!(false, "Error is expected"), - }; + Ok(()) => panic!("Expected error"), + } } - fn contains_all_predicate(needles: Vec<&str>) -> Box bool> { + fn contains_all_predicate(needles: &[&str]) -> Box bool> { let owned_needles: Vec = needles.iter().map(|n| n.to_string()).collect(); Box::new(move |haystack| { let lower_haystack = haystack.to_lowercase(); @@ -127,14 +124,11 @@ mod tests { }) } - fn assert_valid_message(func: BuildFunc) - where - BuildFunc: Fn(&mut Message) -> &mut Message, - { + fn assert_valid_message(func: impl Fn(&mut Message) -> &mut Message) { let mut message = Message::new(); func(&mut message); if let Err(unexpected) = message.check_compatibility(&mut MessageContext::new()) { - assert!(false, "Unexpected validation error {}", unexpected); + panic!("Unexpected validation error {}", unexpected); } } @@ -142,7 +136,7 @@ mod tests { fn empty_action_row_prohibited() { assert_message_error( |message| message.action_row(|row| row), - contains_all_predicate(vec!["action row", "empty"]), + contains_all_predicate(&["action row", "empty"]), ); } @@ -159,7 +153,7 @@ mod tests { }) }) }, - contains_all_predicate(vec!["twice"]), + contains_all_predicate(&["twice"]), ); } @@ -179,14 +173,15 @@ mod tests { }) }) }, - contains_all_predicate(vec!["twice"]), + contains_all_predicate(&["twice"]), ); } - #[test] fn send_message_button_style_required() { + #[test] + fn send_message_button_style_required() { assert_message_error( |message| message.action_row(|row| row.regular_button(|button| button.custom_id("0"))), - contains_all_predicate(vec!["style"]), + contains_all_predicate(&["style"]), ); } @@ -194,7 +189,7 @@ mod tests { fn send_message_url_required() { assert_message_error( |message| message.action_row(|row| row.link_button(|button| button.label("test"))), - contains_all_predicate(vec!["url"]), + contains_all_predicate(&["url"]), ); } @@ -207,7 +202,7 @@ mod tests { } message }, - contains_all_predicate(vec!["interval", "row"]), + contains_all_predicate(&["interval", "row"]), ); } @@ -223,7 +218,7 @@ mod tests { }) }) }, - contains_all_predicate(vec!["interval", "label"]), + contains_all_predicate(&["interval", "label"]), ); } @@ -235,7 +230,7 @@ mod tests { row.regular_button(|btn| btn.style(NonLinkButtonStyle::Primary)) }) }, - contains_all_predicate(vec!["custom id"]), + contains_all_predicate(&["custom id"]), ); } @@ -245,13 +240,12 @@ mod tests { |message| { message.action_row(|row| { row.regular_button(|btn| { - btn.style(NonLinkButtonStyle::Primary).custom_id( - &"a".repeat(Message::CUSTOM_ID_LEN_INTERVAL.max_allowed + 1), - ) + btn.style(NonLinkButtonStyle::Primary) + .custom_id(&"a".repeat(Message::CUSTOM_ID_LEN_INTERVAL.max_allowed + 1)) }) }) }, - contains_all_predicate(vec!["interval", "custom id"]), + contains_all_predicate(&["interval", "custom id"]), ); } @@ -269,7 +263,7 @@ mod tests { row }) }, - contains_all_predicate(vec!["interval", "button"]), + contains_all_predicate(&["interval", "button"]), ); } @@ -317,79 +311,88 @@ mod tests { #[test] fn embed_title_len_enforced() { - assert_message_error(|message| { - message - .embed(|embed| { - embed - .title(&"a".repeat(Embed::TITLE_LEN_INTERVAL.max_allowed + 1)) + assert_message_error( + |message| { + message.embed(|embed| { + embed.title(&"a".repeat(Embed::TITLE_LEN_INTERVAL.max_allowed + 1)) }) - }, - contains_all_predicate(vec!["interval", "embed", "title", "length"]), + }, + contains_all_predicate(&["interval", "embed", "title", "length"]), ) } #[test] fn embed_description_len_enforced() { - assert_message_error(|message| { - message - .embed(|embed| { - embed - .description(&"a".repeat(Embed::DESCRIPTION_LEN_INTERVAL.max_allowed + 1)) + assert_message_error( + |message| { + message.embed(|embed| { + embed.description(&"a".repeat(Embed::DESCRIPTION_LEN_INTERVAL.max_allowed + 1)) }) - }, - contains_all_predicate(vec!["interval", "embed", "description", "length"]), + }, + contains_all_predicate(&["interval", "embed", "description", "length"]), ) } #[test] fn embed_author_name_len_enforced() { - assert_message_error(|message| { - message - .embed(|embed| { - embed - .author(&"a".repeat(EmbedAuthor::NAME_LEN_INTERVAL.max_allowed + 1), None, None) + assert_message_error( + |message| { + message.embed(|embed| { + embed.author( + &"a".repeat(EmbedAuthor::NAME_LEN_INTERVAL.max_allowed + 1), + None, + None, + ) }) - }, - contains_all_predicate(vec!["interval", "embed", "author", "name", "length"]), + }, + contains_all_predicate(&["interval", "embed", "author", "name", "length"]), ) } #[test] fn embed_footer_text_len_enforced() { - assert_message_error(|message| { - message - .embed(|embed| { - embed - .footer(&"a".repeat(EmbedFooter::TEXT_LEN_INTERVAL.max_allowed + 1), None) + assert_message_error( + |message| { + message.embed(|embed| { + embed.footer( + &"a".repeat(EmbedFooter::TEXT_LEN_INTERVAL.max_allowed + 1), + None, + ) }) - }, - contains_all_predicate(vec!["interval", "embed", "footer", "text", "length"]), + }, + contains_all_predicate(&["interval", "embed", "footer", "text", "length"]), ) } #[test] fn embed_field_name_len_enforced() { - assert_message_error(|message| { - message - .embed(|embed| { - embed - .field(&"a".repeat(EmbedField::NAME_LEN_INTERVAL.max_allowed + 1), "None", false) + assert_message_error( + |message| { + message.embed(|embed| { + embed.field( + &"a".repeat(EmbedField::NAME_LEN_INTERVAL.max_allowed + 1), + "None", + false, + ) }) - }, - contains_all_predicate(vec!["interval", "embed", "field", "name", "length"]), + }, + contains_all_predicate(&["interval", "embed", "field", "name", "length"]), ) } #[test] fn embed_field_value_len_enforced() { - assert_message_error(|message| { - message - .embed(|embed| { - embed - .field("None", &"a".repeat(EmbedField::VALUE_LEN_INTERVAL.max_allowed + 1), false) + assert_message_error( + |message| { + message.embed(|embed| { + embed.field( + "None", + &"a".repeat(EmbedField::VALUE_LEN_INTERVAL.max_allowed + 1), + false, + ) }) - }, - contains_all_predicate(vec!["interval", "embed", "field", "value", "length"]), + }, + contains_all_predicate(&["interval", "embed", "field", "value", "length"]), ) } @@ -397,41 +400,42 @@ mod tests { fn embed_total_char_length_enforced() { // adds 2 embeds with maximum length descriptions // which should overflow the maximum allowed characters for embeds in total - assert!(Embed::DESCRIPTION_LEN_INTERVAL.max_allowed * 2 > Message::EMBED_TOTAL_TEXT_LEN_INTERVAL.max_allowed, "Key test values modified, fix this test!"); + const { + assert!( + Embed::DESCRIPTION_LEN_INTERVAL.max_allowed * 2 + > Message::EMBED_TOTAL_TEXT_LEN_INTERVAL.max_allowed, + "Key test values modified, fix this test!" + ); + } - assert_message_error(|message| { - message - .embed(|embed| { - embed - .description(&"a".repeat(Embed::DESCRIPTION_LEN_INTERVAL.max_allowed)) - }) - .embed(|embed| { - embed - .description(&"a".repeat(Embed::DESCRIPTION_LEN_INTERVAL.max_allowed)) - }) - }, - contains_all_predicate(vec!["interval", "character", "count", "embed"]), + assert_message_error( + |message| { + message + .embed(|embed| { + embed.description(&"a".repeat(Embed::DESCRIPTION_LEN_INTERVAL.max_allowed)) + }) + .embed(|embed| { + embed.description(&"a".repeat(Embed::DESCRIPTION_LEN_INTERVAL.max_allowed)) + }) + }, + contains_all_predicate(&["interval", "character", "count", "embed"]), ) } #[test] - #[should_panic] + #[should_panic = "You can't have more than 25 fields in an embed!"] fn field_count_enforced() { assert_valid_message(|message| { - message - .embed(|embed| { - for _ in 0..Embed::FIELDS_LEN_INTERVAL.max_allowed + 1 { - embed.field("None", "a", false); - } - embed - }) + message.embed(|embed| { + for _ in 0..Embed::FIELDS_LEN_INTERVAL.max_allowed + 1 { + embed.field("None", "a", false); + } + embed + }) }) } - fn test_is_send(t: T) - where - T: Send, - { + fn test_is_send(t: T) { drop(t); } diff --git a/src/lib.rs b/src/lib.rs index 6688c65..dc8da03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,14 @@ pub mod models; #[cfg(feature = "client")] -pub mod client; \ No newline at end of file +pub mod client; + +// # TODOs +// +// ## Webhook related +// * Attachments +// * Components +// +// ## Organization related +// * Update `hyper` crate +// * diff --git a/src/models.rs b/src/models.rs index 1b07fce..1f32510 100644 --- a/src/models.rs +++ b/src/models.rs @@ -3,14 +3,14 @@ use std::collections::HashSet; use std::fmt::Display; type Snowflake = String; -pub struct Interval { - pub max_allowed: T, +pub struct Interval { pub min_allowed: T, + pub max_allowed: T, } impl Interval { pub const fn from_min_max(min_allowed: T, max_allowed: T) -> Self { - Interval { + Self { min_allowed, max_allowed, } @@ -22,8 +22,9 @@ impl Interval { } macro_rules! interval_member { -($name:ident, $option_inner_t:ty, $lower_bound:expr, $upper_bound:expr) => { - pub(crate) const $name : Interval<$option_inner_t> = Interval::from_min_max($lower_bound, $upper_bound); + ($name:ident, $option_inner_t:ty, $lower_bound:expr, $upper_bound:expr) => { + pub(crate) const $name: Interval<$option_inner_t> = + Interval::from_min_max($lower_bound, $upper_bound); }; } @@ -62,6 +63,14 @@ fn interval_check( } impl MessageContext { + pub(crate) fn new() -> Self { + Self { + custom_ids: HashSet::new(), + button_count_in_action_row: 0, + embeds_character_counter: 0, + } + } + /// Tries to register a custom id. /// /// # Watch out! @@ -79,10 +88,11 @@ impl MessageContext { interval_check( &Message::CUSTOM_ID_LEN_INTERVAL, &id.len(), - "Custom ID length")?; + "Custom ID length", + )?; if !self.custom_ids.insert(id.to_string()) { - return Err(format!("Attempt to use the same custom ID ({}) twice!", id)); + return Err(format!("Attempt to use the same custom ID ({id}) twice!")); } Ok(()) } @@ -93,9 +103,8 @@ impl MessageContext { /// /// None on no error. Some(String) containing the reason for failure. pub fn register_embed(&mut self, embed: &Embed) -> Result<(), String> { - - self.embeds_character_counter += embed.title.as_ref().map_or(0, |s| s.len()); - self.embeds_character_counter += embed.description.as_ref().map_or(0, |s| s.len()); + self.embeds_character_counter += embed.title.as_ref().map_or(0, String::len); + self.embeds_character_counter += embed.description.as_ref().map_or(0, String::len); self.embeds_character_counter += embed.footer.as_ref().map_or(0, |f| f.text.len()); self.embeds_character_counter += embed.author.as_ref().map_or(0, |a| a.name.len()); @@ -106,18 +115,11 @@ impl MessageContext { interval_check( &Message::EMBED_TOTAL_TEXT_LEN_INTERVAL, &self.embeds_character_counter, - "Character count across all embeds")?; + "Character count across all embeds", + )?; Ok(()) } - pub(crate) fn new() -> MessageContext { - MessageContext { - custom_ids: HashSet::new(), - button_count_in_action_row: 0, - embeds_character_counter: 0 - } - } - /// Tries to register a button using the button's custom id. /// /// # Return value @@ -134,7 +136,8 @@ impl MessageContext { interval_check( &ActionRow::BUTTON_COUNT_INTERVAL, &self.button_count_in_action_row, - "Button count")?; + "Button count", + )?; Ok(()) } /// Switches the context to register components logically in a "new" action row. @@ -142,7 +145,7 @@ impl MessageContext { /// # Watch out! /// This function shall be called only once per one action row. (due to the lack of action row /// identification) - fn register_action_row(&mut self) { + const fn register_action_row(&mut self) { self.button_count_in_action_row = 0; self.button_count_in_action_row = 0; } @@ -160,8 +163,15 @@ pub struct Message { pub action_rows: Vec, } +impl Default for Message { + fn default() -> Self { + Self::new() + } +} + impl Message { - pub fn new() -> Self { + #[must_use] + pub const fn new() -> Self { Self { content: None, username: None, @@ -235,7 +245,7 @@ impl Message { } } -#[derive(Serialize, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct Embed { pub title: Option, #[serde(rename = "type")] @@ -254,7 +264,14 @@ pub struct Embed { pub fields: Vec, } +impl Default for Embed { + fn default() -> Self { + Self::new() + } +} + impl Embed { + #[must_use] pub fn new() -> Self { Self { title: None, @@ -334,9 +351,11 @@ impl Embed { } pub fn field(&mut self, name: &str, value: &str, inline: bool) -> &mut Self { - if self.fields.len() == Embed::FIELDS_LEN_INTERVAL.max_allowed { - panic!("You can't have more than {} fields in an embed!", Embed::FIELDS_LEN_INTERVAL.max_allowed) - } + assert!( + self.fields.len() < Self::FIELDS_LEN_INTERVAL.max_allowed, + "You can't have more than {} fields in an embed!", + Self::FIELDS_LEN_INTERVAL.max_allowed + ); self.fields.push(EmbedField::new(name, value, inline)); self @@ -348,7 +367,7 @@ impl Embed { interval_member!(FIELDS_LEN_INTERVAL, usize, 0, 25); } -#[derive(Serialize, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct EmbedField { pub name: String, pub value: String, @@ -356,6 +375,7 @@ pub struct EmbedField { } impl EmbedField { + #[must_use] pub fn new(name: &str, value: &str, inline: bool) -> Self { Self { name: name.to_owned(), @@ -367,13 +387,14 @@ impl EmbedField { interval_member!(VALUE_LEN_INTERVAL, usize, 0, 1024); } -#[derive(Serialize, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct EmbedFooter { pub text: String, pub icon_url: Option, } impl EmbedFooter { + #[must_use] pub fn new(text: &str, icon_url: Option) -> Self { Self { text: text.to_owned(), @@ -387,12 +408,13 @@ pub type EmbedImage = EmbedUrlSource; pub type EmbedThumbnail = EmbedUrlSource; pub type EmbedVideo = EmbedUrlSource; -#[derive(Serialize, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct EmbedUrlSource { pub url: String, } impl EmbedUrlSource { + #[must_use] pub fn new(url: &str) -> Self { Self { url: url.to_owned(), @@ -400,13 +422,14 @@ impl EmbedUrlSource { } } -#[derive(Serialize, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct EmbedProvider { pub name: String, pub url: String, } impl EmbedProvider { + #[must_use] pub fn new(name: &str, url: &str) -> Self { Self { name: name.to_owned(), @@ -415,7 +438,7 @@ impl EmbedProvider { } } -#[derive(Serialize, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct EmbedAuthor { pub name: String, pub url: Option, @@ -423,6 +446,7 @@ pub struct EmbedAuthor { } impl EmbedAuthor { + #[must_use] pub fn new(name: &str, url: Option, icon_url: Option) -> Self { Self { name: name.to_owned(), @@ -433,41 +457,44 @@ impl EmbedAuthor { interval_member!(NAME_LEN_INTERVAL, usize, 0, 256); } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum AllowedMention { RoleMention, UserMention, EveryoneMention, } -fn resolve_allowed_mention_name(allowed_mention: AllowedMention) -> String { - match allowed_mention { - AllowedMention::RoleMention => "roles".to_string(), - AllowedMention::UserMention => "users".to_string(), - AllowedMention::EveryoneMention => "everyone".to_string(), +impl AllowedMention { + fn resolve(self) -> &'static str { + match self { + AllowedMention::RoleMention => "roles", + AllowedMention::UserMention => "users", + AllowedMention::EveryoneMention => "everyone", + } } } -#[derive(Serialize, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct AllowedMentions { - pub parse: Option>, + pub parse: Option>, pub roles: Option>, pub users: Option>, pub replied_user: bool, } impl AllowedMentions { + #[must_use] pub fn new( parse: Option>, roles: Option>, users: Option>, replied_user: bool, ) -> Self { - let mut parse_strings: Vec = vec![]; - if parse.is_some() { - parse - .unwrap() - .into_iter() - .for_each(|x| parse_strings.push(resolve_allowed_mention_name(x))) + let mut parse_strings: Vec<&str> = vec![]; + if let Some(parse) = parse { + for x in parse { + parse_strings.push(x.resolve()); + } } Self { @@ -481,23 +508,20 @@ impl AllowedMentions { // ready to be extended with other components // non-composite here specifically means *not an action row* -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] enum NonCompositeComponent { Button(Button), } impl Serialize for NonCompositeComponent { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { + fn serialize(&self, serializer: S) -> Result { match self { - NonCompositeComponent::Button(button) => button.serialize(serializer), + Self::Button(button) => button.serialize(serializer), } } } -#[derive(Serialize, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct ActionRow { #[serde(rename = "type")] pub component_type: u8, @@ -505,8 +529,9 @@ pub struct ActionRow { } impl ActionRow { - fn new() -> ActionRow { - ActionRow { + #[must_use] + const fn new() -> Self { + Self { component_type: 1, components: vec![], } @@ -538,7 +563,7 @@ impl ActionRow { interval_member!(BUTTON_COUNT_INTERVAL, usize, 0, 5); } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] pub enum NonLinkButtonStyle { Primary, Secondary, @@ -547,20 +572,20 @@ pub enum NonLinkButtonStyle { } impl NonLinkButtonStyle { - fn get_button_style(&self) -> ButtonStyles { - match *self { - NonLinkButtonStyle::Primary => ButtonStyles::Primary, - NonLinkButtonStyle::Secondary => ButtonStyles::Secondary, - NonLinkButtonStyle::Success => ButtonStyles::Success, - NonLinkButtonStyle::Danger => ButtonStyles::Danger, + const fn get_button_style(self) -> ButtonStyles { + match self { + Self::Primary => ButtonStyles::Primary, + Self::Secondary => ButtonStyles::Secondary, + Self::Success => ButtonStyles::Success, + Self::Danger => ButtonStyles::Danger, } } } -// since link button has an explicit way of creation via the action row -// this enum is kept hidden from the user ans the NonLinkButtonStyle is created to avoid -// user confusion -#[derive(Debug)] +// Since link button has an explicit way of creation via the action row, +// this enum is kept hidden from the user and the NonLinkButtonStyle is +// created to avoid user confusion. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum ButtonStyles { Primary, Secondary, @@ -575,17 +600,17 @@ impl Serialize for ButtonStyles { S: Serializer, { let to_serialize = match *self { - ButtonStyles::Primary => 1, - ButtonStyles::Secondary => 2, - ButtonStyles::Success => 3, - ButtonStyles::Danger => 4, - ButtonStyles::Link => 5, + Self::Primary => 1, + Self::Secondary => 2, + Self::Success => 3, + Self::Danger => 4, + Self::Link => 5, }; serializer.serialize_i32(to_serialize) } } -#[derive(Serialize, Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct PartialEmoji { pub id: Snowflake, pub name: String, @@ -593,7 +618,7 @@ pub struct PartialEmoji { } /// the button struct intended for serialized -#[derive(Serialize, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] struct Button { #[serde(rename = "type")] pub component_type: i8, @@ -606,7 +631,8 @@ struct Button { } impl Button { - fn new( + #[must_use] + const fn new( style: Option, label: Option, emoji: Option, @@ -627,7 +653,7 @@ impl Button { } /// Data holder for shared fields of link and regular buttons -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] struct ButtonCommonBase { pub label: Option, pub emoji: Option, @@ -635,8 +661,13 @@ struct ButtonCommonBase { } impl ButtonCommonBase { - fn new(label: Option, emoji: Option, disabled: Option) -> Self { - ButtonCommonBase { + #[must_use] + const fn new( + label: Option, + emoji: Option, + disabled: Option, + ) -> Self { + Self { label, emoji, disabled, @@ -656,13 +687,13 @@ impl ButtonCommonBase { self } - fn disabled(&mut self, disabled: bool) -> &mut Self { + const fn disabled(&mut self, disabled: bool) -> &mut Self { self.disabled = Some(disabled); self } } -/// a macro which takes an identifier (`base`) of the ButtonCommonBase (relative to `self`) +/// a macro which takes an identifier (`base`) of the [`ButtonCommonBase`] (relative to `self`) /// and generates setter functions that delegate their inputs to the `self.base` macro_rules! button_base_delegation { ($base:ident) => { @@ -683,15 +714,16 @@ macro_rules! button_base_delegation { }; } -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct LinkButton { button_base: ButtonCommonBase, url: Option, } impl LinkButton { - fn new() -> Self { - LinkButton { + #[must_use] + const fn new() -> Self { + Self { button_base: ButtonCommonBase::new(None, None, None), url: None, } @@ -705,6 +737,7 @@ impl LinkButton { button_base_delegation!(button_base); } +#[derive(Debug, Clone, PartialEq, Eq)] pub struct RegularButton { button_base: ButtonCommonBase, custom_id: Option, @@ -712,8 +745,9 @@ pub struct RegularButton { } impl RegularButton { - fn new() -> Self { - RegularButton { + #[must_use] + const fn new() -> Self { + Self { button_base: ButtonCommonBase::new(None, None, None), custom_id: None, style: None, @@ -753,7 +787,7 @@ impl ToSerializableButton for LinkButton { impl ToSerializableButton for RegularButton { fn to_serializable_button(&self) -> Button { Button::new( - self.style.clone().map(|s| s.get_button_style()), + self.style.map(|s| s.get_button_style()), self.button_base.label.clone(), self.button_base.emoji.clone(), None, @@ -775,7 +809,7 @@ pub(crate) trait DiscordApiCompatible { impl DiscordApiCompatible for NonCompositeComponent { fn check_compatibility(&self, context: &mut MessageContext) -> Result<(), String> { match self { - NonCompositeComponent::Button(b) => b.check_compatibility(context), + Self::Button(b) => b.check_compatibility(context), } } } @@ -786,7 +820,7 @@ impl DiscordApiCompatible for Button { interval_check(&Message::LABEL_LEN_INTERVAL, &label.len(), "Label length")?; } - return match self.style { + match self.style { None => Err("Button style must be set!".to_string()), Some(ButtonStyles::Link) => { if self.url.is_none() { @@ -796,17 +830,19 @@ impl DiscordApiCompatible for Button { } } // list all remaining in case a style with different requirements is added - Some(ButtonStyles::Danger) - | Some(ButtonStyles::Primary) - | Some(ButtonStyles::Success) - | Some(ButtonStyles::Secondary) => { - return if let Some(id) = self.custom_id.as_ref() { + Some( + ButtonStyles::Danger + | ButtonStyles::Primary + | ButtonStyles::Success + | ButtonStyles::Secondary, + ) => { + if let Some(id) = self.custom_id.as_ref() { context.register_button(id) } else { Err("Custom ID of a NonLink button must be set!".to_string()) - }; + } } - }; + } } } @@ -817,46 +853,66 @@ impl DiscordApiCompatible for ActionRow { return Err("Empty action row detected!".to_string()); } - self.components.iter().fold(Ok(()), |acc, component| { - acc.and(component.check_compatibility(context)) - }) + for component in &self.components { + component.check_compatibility(context)?; + } + Ok(()) } } impl DiscordApiCompatible for Message { fn check_compatibility(&self, context: &mut MessageContext) -> Result<(), String> { interval_check( - &Message::ACTION_ROW_COUNT_INTERVAL, + &Self::ACTION_ROW_COUNT_INTERVAL, &self.action_rows.len(), - "Action row count")?; + "Action row count", + )?; + + for embed in &self.embeds { + embed.check_compatibility(context)?; + } - self.embeds - .iter() - .fold(Ok(()), |acc, emb| acc.and(emb.check_compatibility(context)))?; + for action_row in &self.action_rows { + action_row.check_compatibility(context)?; + } - self.action_rows - .iter() - .fold(Ok(()), |acc, row| acc.and(row.check_compatibility(context))) + Ok(()) } } impl DiscordApiCompatible for Embed { fn check_compatibility(&self, context: &mut MessageContext) -> Result<(), String> { context.register_embed(self)?; - interval_check(&Self::FIELDS_LEN_INTERVAL, &self.fields.len(), "Embed field count")?; + interval_check( + &Self::FIELDS_LEN_INTERVAL, + &self.fields.len(), + "Embed field count", + )?; if let Some(title) = self.title.as_ref() { - interval_check(&Self::TITLE_LEN_INTERVAL, &title.len(), "Embed title length")?; + interval_check( + &Self::TITLE_LEN_INTERVAL, + &title.len(), + "Embed title length", + )?; } if let Some(description) = self.description.as_ref() { - interval_check(&Self::DESCRIPTION_LEN_INTERVAL, &description.len(), "Embed description length")?; + interval_check( + &Self::DESCRIPTION_LEN_INTERVAL, + &description.len(), + "Embed description length", + )?; } - self.author.as_ref().map_or_else(|| Ok(()), |a| a.check_compatibility(context))?; - self.footer.as_ref().map_or_else(|| Ok(()), |f| f.check_compatibility(context))?; + self.author + .as_ref() + .map_or_else(|| Ok(()), |a| a.check_compatibility(context))?; + self.footer + .as_ref() + .map_or_else(|| Ok(()), |f| f.check_compatibility(context))?; - for field in self.fields.iter() { + for field in &self.fields { field.check_compatibility(context)?; } Ok(()) @@ -865,22 +921,38 @@ impl DiscordApiCompatible for Embed { impl DiscordApiCompatible for EmbedAuthor { fn check_compatibility(&self, _context: &mut MessageContext) -> Result<(), String> { - interval_check(&Self::NAME_LEN_INTERVAL, &self.name.len(), "Embed author name length")?; + interval_check( + &Self::NAME_LEN_INTERVAL, + &self.name.len(), + "Embed author name length", + )?; Ok(()) } } impl DiscordApiCompatible for EmbedFooter { fn check_compatibility(&self, _context: &mut MessageContext) -> Result<(), String> { - interval_check(&Self::TEXT_LEN_INTERVAL, &self.text.len(), "Embed footer text length")?; + interval_check( + &Self::TEXT_LEN_INTERVAL, + &self.text.len(), + "Embed footer text length", + )?; Ok(()) } } impl DiscordApiCompatible for EmbedField { fn check_compatibility(&self, _context: &mut MessageContext) -> Result<(), String> { - interval_check(&Self::VALUE_LEN_INTERVAL, &self.value.len(), "Embed field value length")?; - interval_check(&Self::NAME_LEN_INTERVAL, &self.name.len(), "Embed field name length")?; + interval_check( + &Self::VALUE_LEN_INTERVAL, + &self.value.len(), + "Embed field value length", + )?; + interval_check( + &Self::NAME_LEN_INTERVAL, + &self.name.len(), + "Embed field name length", + )?; Ok(()) } -} \ No newline at end of file +}