From 497fea21279c1f4a1db2bb93748a6cab95f32ad4 Mon Sep 17 00:00:00 2001 From: BioTomateDE Date: Tue, 27 Jan 2026 22:09:25 +0000 Subject: [PATCH 1/7] rustfmt --- examples/example.rs | 55 +++++++++------- src/client.rs | 153 ++++++++++++++++++++++++-------------------- src/lib.rs | 2 +- src/models.rs | 77 ++++++++++++++++------ 4 files changed, 174 insertions(+), 113 deletions(-) diff --git a/examples/example.rs b/examples/example.rs index 0562388..4c715f4 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: &'static str = + "https://cdn.discordapp.com/avatars/312157715449249795/a_b8b3b0c35f3dee2b6586a0dd58697e29.png"; #[tokio::main] async fn main() -> WebhookResult<()> { @@ -12,18 +13,28 @@ 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?; Ok(()) } @@ -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..b17d747 100644 --- a/src/client.rs +++ b/src/client.rs @@ -93,12 +93,12 @@ 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: BuildFunc, msg_pred: MessagePred) where BuildFunc: Fn(&mut Message) -> &mut Message, MessagePred: Fn(&str) -> bool, @@ -183,7 +183,8 @@ mod tests { ); } - #[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"]), @@ -245,9 +246,8 @@ 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)) }) }) }, @@ -317,79 +317,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(vec!["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(vec!["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(vec!["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(vec!["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(vec!["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(vec!["interval", "embed", "field", "value", "length"]), ) } @@ -397,20 +406,23 @@ 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!"); + 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(vec!["interval", "character", "count", "embed"]), ) } @@ -418,13 +430,12 @@ mod tests { #[should_panic] 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 + }) }) } diff --git a/src/lib.rs b/src/lib.rs index 6688c65..8838136 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ pub mod models; #[cfg(feature = "client")] -pub mod client; \ No newline at end of file +pub mod client; diff --git a/src/models.rs b/src/models.rs index 1b07fce..77eb951 100644 --- a/src/models.rs +++ b/src/models.rs @@ -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); }; } @@ -79,7 +80,8 @@ 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)); @@ -93,7 +95,6 @@ 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.footer.as_ref().map_or(0, |f| f.text.len()); @@ -106,7 +107,8 @@ 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(()) } @@ -114,7 +116,7 @@ impl MessageContext { MessageContext { custom_ids: HashSet::new(), button_count_in_action_row: 0, - embeds_character_counter: 0 + embeds_character_counter: 0, } } @@ -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. @@ -335,7 +338,10 @@ 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) + panic!( + "You can't have more than {} fields in an embed!", + Embed::FIELDS_LEN_INTERVAL.max_allowed + ) } self.fields.push(EmbedField::new(name, value, inline)); @@ -828,7 +834,8 @@ impl DiscordApiCompatible for Message { interval_check( &Message::ACTION_ROW_COUNT_INTERVAL, &self.action_rows.len(), - "Action row count")?; + "Action row count", + )?; self.embeds .iter() @@ -843,18 +850,34 @@ impl DiscordApiCompatible for Message { 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() { field.check_compatibility(context)?; @@ -865,22 +888,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 +} From e830190df61afb6ecb51c4b51d2f25bf79c52e0e Mon Sep 17 00:00:00 2001 From: BioTomateDE Date: Tue, 27 Jan 2026 22:17:47 +0000 Subject: [PATCH 2/7] lints --- examples/example.rs | 4 ++-- src/client.rs | 18 ++++++++++-------- src/models.rs | 44 +++++++++++++++++++++++++++++--------------- 3 files changed, 41 insertions(+), 25 deletions(-) diff --git a/examples/example.rs b/examples/example.rs index 4c715f4..66283a8 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -1,7 +1,7 @@ use webhook::client::{WebhookClient, WebhookResult}; use webhook::models::NonLinkButtonStyle; -const IMAGE_URL: &'static str = +const IMAGE_URL: &str = "https://cdn.discordapp.com/avatars/312157715449249795/a_b8b3b0c35f3dee2b6586a0dd58697e29.png"; #[tokio::main] @@ -42,7 +42,7 @@ async fn main() -> WebhookResult<()> { // 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); diff --git a/src/client.rs b/src/client.rs index b17d747..39bb490 100644 --- a/src/client.rs +++ b/src/client.rs @@ -110,10 +110,10 @@ mod tests { assert!( msg_pred(&err.to_string()), "Unexpected error message {}", - err.to_string() + err, ) } - Ok(_) => assert!(false, "Error is expected"), + Ok(()) => panic!("Expected error"), }; } @@ -134,7 +134,7 @@ mod tests { 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); } } @@ -406,11 +406,13 @@ 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| { diff --git a/src/models.rs b/src/models.rs index 77eb951..f4e3240 100644 --- a/src/models.rs +++ b/src/models.rs @@ -163,6 +163,12 @@ pub struct Message { pub action_rows: Vec, } +impl Default for Message { + fn default() -> Self { + Self::new() + } +} + impl Message { pub fn new() -> Self { Self { @@ -257,6 +263,12 @@ pub struct Embed { pub fields: Vec, } +impl Default for Embed { + fn default() -> Self { + Self::new() + } +} + impl Embed { pub fn new() -> Self { Self { @@ -469,9 +481,8 @@ impl AllowedMentions { replied_user: bool, ) -> Self { let mut parse_strings: Vec = vec![]; - if parse.is_some() { + if let Some(parse) = parse { parse - .unwrap() .into_iter() .for_each(|x| parse_strings.push(resolve_allowed_mention_name(x))) } @@ -792,7 +803,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() { @@ -806,13 +817,13 @@ impl DiscordApiCompatible for Button { | Some(ButtonStyles::Primary) | Some(ButtonStyles::Success) | Some(ButtonStyles::Secondary) => { - return if let Some(id) = self.custom_id.as_ref() { + 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()) - }; + } } - }; + } } } @@ -823,9 +834,10 @@ 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(()) } } @@ -837,13 +849,15 @@ impl DiscordApiCompatible for Message { "Action row count", )?; - self.embeds - .iter() - .fold(Ok(()), |acc, emb| acc.and(emb.check_compatibility(context)))?; + for embed in &self.embeds { + embed.check_compatibility(context)?; + } - self.action_rows - .iter() - .fold(Ok(()), |acc, row| acc.and(row.check_compatibility(context))) + for action_row in &self.action_rows { + action_row.check_compatibility(context)?; + } + + Ok(()) } } From b97cdde5904417f7f8e30978a719969a8ad65141 Mon Sep 17 00:00:00 2001 From: BioTomateDE Date: Tue, 27 Jan 2026 22:27:43 +0000 Subject: [PATCH 3/7] .env is already excluded by default --- Cargo.toml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8f2a399..10d9bd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,12 +10,8 @@ keywords = ["discord", "discord-api", "webhook", "discord-webhook"] authors = ["Thomas"] publish = true -exclude = [ - "examples/*", - ".env" -] +exclude = ["examples/*"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] default = ["client"] @@ -24,7 +20,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 +33,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" From 19cf301f3b04c4ea09062182e0396d78a0b7fb08 Mon Sep 17 00:00:00 2001 From: BioTomateDE Date: Tue, 27 Jan 2026 22:30:55 +0000 Subject: [PATCH 4/7] Update from 2018 to 2024 edition --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 10d9bd6..d4df421 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "webhook" version = "2.1.2" -edition = "2018" +edition = "2024" description = "Discord Webhook API Wrapper" readme = "README.md" repository = "https://github.com/thoo0224/webhook-rs" From 626e6619a7ca5f4d9ac42cea42036f583478a5f9 Mon Sep 17 00:00:00 2001 From: BioTomateDE Date: Tue, 27 Jan 2026 22:32:01 +0000 Subject: [PATCH 5/7] actually use the example function application_webhook_example --- examples/example.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/example.rs b/examples/example.rs index 66283a8..306e996 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -36,11 +36,11 @@ async fn main() -> WebhookResult<()> { }) .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 webhook_info = client.get_information().await?; From 99f2dd0592b26f83a9e603a931282abf8f52a3bc Mon Sep 17 00:00:00 2001 From: BioTomateDE Date: Tue, 27 Jan 2026 22:47:47 +0000 Subject: [PATCH 6/7] More linting --- Cargo.toml | 6 +-- src/client.rs | 57 +++++++++++----------- src/models.rs | 132 ++++++++++++++++++++++++++++---------------------- 3 files changed, 103 insertions(+), 92 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d4df421..6138138 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,16 @@ [package] name = "webhook" version = "2.1.2" +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 - +categories = ["api-bindings"] exclude = ["examples/*"] - [features] default = ["client"] client = ["hyper", "hyper-tls"] diff --git a/src/client.rs b/src/client.rs index 39bb490..c2ef2e2 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) @@ -107,17 +108,13 @@ mod tests { message_build(&mut message); match message.check_compatibility(&mut MessageContext::new()) { Err(err) => { - assert!( - msg_pred(&err.to_string()), - "Unexpected error message {}", - err, - ) + assert!(msg_pred(&err), "Unexpected error message {err}"); } 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(); @@ -142,7 +139,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 +156,7 @@ mod tests { }) }) }, - contains_all_predicate(vec!["twice"]), + contains_all_predicate(&["twice"]), ); } @@ -179,7 +176,7 @@ mod tests { }) }) }, - contains_all_predicate(vec!["twice"]), + contains_all_predicate(&["twice"]), ); } @@ -187,7 +184,7 @@ mod tests { 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"]), ); } @@ -195,7 +192,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"]), ); } @@ -208,7 +205,7 @@ mod tests { } message }, - contains_all_predicate(vec!["interval", "row"]), + contains_all_predicate(&["interval", "row"]), ); } @@ -224,7 +221,7 @@ mod tests { }) }) }, - contains_all_predicate(vec!["interval", "label"]), + contains_all_predicate(&["interval", "label"]), ); } @@ -236,7 +233,7 @@ mod tests { row.regular_button(|btn| btn.style(NonLinkButtonStyle::Primary)) }) }, - contains_all_predicate(vec!["custom id"]), + contains_all_predicate(&["custom id"]), ); } @@ -251,7 +248,7 @@ mod tests { }) }) }, - contains_all_predicate(vec!["interval", "custom id"]), + contains_all_predicate(&["interval", "custom id"]), ); } @@ -269,7 +266,7 @@ mod tests { row }) }, - contains_all_predicate(vec!["interval", "button"]), + contains_all_predicate(&["interval", "button"]), ); } @@ -323,7 +320,7 @@ mod tests { 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"]), ) } @@ -335,7 +332,7 @@ mod tests { 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"]), ) } @@ -351,7 +348,7 @@ mod tests { ) }) }, - contains_all_predicate(vec!["interval", "embed", "author", "name", "length"]), + contains_all_predicate(&["interval", "embed", "author", "name", "length"]), ) } @@ -366,7 +363,7 @@ mod tests { ) }) }, - contains_all_predicate(vec!["interval", "embed", "footer", "text", "length"]), + contains_all_predicate(&["interval", "embed", "footer", "text", "length"]), ) } @@ -382,7 +379,7 @@ mod tests { ) }) }, - contains_all_predicate(vec!["interval", "embed", "field", "name", "length"]), + contains_all_predicate(&["interval", "embed", "field", "name", "length"]), ) } @@ -398,7 +395,7 @@ mod tests { ) }) }, - contains_all_predicate(vec!["interval", "embed", "field", "value", "length"]), + contains_all_predicate(&["interval", "embed", "field", "value", "length"]), ) } @@ -424,12 +421,12 @@ mod tests { embed.description(&"a".repeat(Embed::DESCRIPTION_LEN_INTERVAL.max_allowed)) }) }, - contains_all_predicate(vec!["interval", "character", "count", "embed"]), + 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| { diff --git a/src/models.rs b/src/models.rs index f4e3240..0dc8562 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, } @@ -63,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! @@ -84,7 +92,7 @@ impl MessageContext { )?; 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(()) } @@ -95,8 +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()); @@ -112,14 +120,6 @@ impl MessageContext { 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 @@ -145,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; } @@ -170,7 +170,8 @@ impl Default for Message { } impl Message { - pub fn new() -> Self { + #[must_use] + pub const fn new() -> Self { Self { content: None, username: None, @@ -270,6 +271,7 @@ impl Default for Embed { } impl Embed { + #[must_use] pub fn new() -> Self { Self { title: None, @@ -349,12 +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 @@ -374,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(), @@ -392,6 +394,7 @@ pub struct EmbedFooter { } impl EmbedFooter { + #[must_use] pub fn new(text: &str, icon_url: Option) -> Self { Self { text: text.to_owned(), @@ -411,6 +414,7 @@ pub struct EmbedUrlSource { } impl EmbedUrlSource { + #[must_use] pub fn new(url: &str) -> Self { Self { url: url.to_owned(), @@ -425,6 +429,7 @@ pub struct EmbedProvider { } impl EmbedProvider { + #[must_use] pub fn new(name: &str, url: &str) -> Self { Self { name: name.to_owned(), @@ -441,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(), @@ -451,6 +457,7 @@ impl EmbedAuthor { interval_member!(NAME_LEN_INTERVAL, usize, 0, 256); } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum AllowedMention { RoleMention, UserMention, @@ -474,6 +481,7 @@ pub struct AllowedMentions { } impl AllowedMentions { + #[must_use] pub fn new( parse: Option>, roles: Option>, @@ -482,9 +490,9 @@ impl AllowedMentions { ) -> Self { let mut parse_strings: Vec = vec![]; if let Some(parse) = parse { - parse - .into_iter() - .for_each(|x| parse_strings.push(resolve_allowed_mention_name(x))) + for x in parse { + parse_strings.push(resolve_allowed_mention_name(x)); + } } Self { @@ -504,12 +512,9 @@ enum NonCompositeComponent { } 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), } } } @@ -522,8 +527,9 @@ pub struct ActionRow { } impl ActionRow { - fn new() -> ActionRow { - ActionRow { + #[must_use] + const fn new() -> Self { + Self { component_type: 1, components: vec![], } @@ -564,12 +570,12 @@ pub enum NonLinkButtonStyle { } impl NonLinkButtonStyle { - fn get_button_style(&self) -> ButtonStyles { + const fn get_button_style(&self) -> ButtonStyles { match *self { - NonLinkButtonStyle::Primary => ButtonStyles::Primary, - NonLinkButtonStyle::Secondary => ButtonStyles::Secondary, - NonLinkButtonStyle::Success => ButtonStyles::Success, - NonLinkButtonStyle::Danger => ButtonStyles::Danger, + Self::Primary => ButtonStyles::Primary, + Self::Secondary => ButtonStyles::Secondary, + Self::Success => ButtonStyles::Success, + Self::Danger => ButtonStyles::Danger, } } } @@ -592,11 +598,11 @@ 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) } @@ -623,7 +629,8 @@ struct Button { } impl Button { - fn new( + #[must_use] + const fn new( style: Option, label: Option, emoji: Option, @@ -652,8 +659,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, @@ -673,13 +685,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) => { @@ -707,8 +719,9 @@ pub struct LinkButton { } impl LinkButton { - fn new() -> Self { - LinkButton { + #[must_use] + const fn new() -> Self { + Self { button_base: ButtonCommonBase::new(None, None, None), url: None, } @@ -729,8 +742,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, @@ -792,7 +806,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), } } } @@ -813,10 +827,12 @@ 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) => { + Some( + ButtonStyles::Danger + | ButtonStyles::Primary + | ButtonStyles::Success + | ButtonStyles::Secondary, + ) => { if let Some(id) = self.custom_id.as_ref() { context.register_button(id) } else { @@ -844,7 +860,7 @@ impl DiscordApiCompatible for ActionRow { 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", )?; @@ -893,7 +909,7 @@ impl DiscordApiCompatible for Embed { .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(()) From 6b90bef503906ed3bef6ec71e121db7b87d064da Mon Sep 17 00:00:00 2001 From: BioTomateDE Date: Tue, 27 Jan 2026 22:57:10 +0000 Subject: [PATCH 7/7] More derives --- src/client.rs | 18 +++++---------- src/lib.rs | 10 +++++++++ src/models.rs | 61 +++++++++++++++++++++++++++------------------------ 3 files changed, 48 insertions(+), 41 deletions(-) diff --git a/src/client.rs b/src/client.rs index c2ef2e2..dd00517 100644 --- a/src/client.rs +++ b/src/client.rs @@ -99,16 +99,16 @@ mod tests { 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), "Unexpected error message {err}"); + assert!(message_pred(&err), "Unexpected error message {err}"); } Ok(()) => panic!("Expected error"), } @@ -124,10 +124,7 @@ 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()) { @@ -438,10 +435,7 @@ mod tests { }) } - 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 8838136..dc8da03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,3 +2,13 @@ pub mod models; #[cfg(feature = "client")] pub mod client; + +// # TODOs +// +// ## Webhook related +// * Attachments +// * Components +// +// ## Organization related +// * Update `hyper` crate +// * diff --git a/src/models.rs b/src/models.rs index 0dc8562..1f32510 100644 --- a/src/models.rs +++ b/src/models.rs @@ -245,7 +245,7 @@ impl Message { } } -#[derive(Serialize, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct Embed { pub title: Option, #[serde(rename = "type")] @@ -367,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, @@ -387,7 +387,7 @@ 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, @@ -408,7 +408,7 @@ 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, } @@ -422,7 +422,7 @@ impl EmbedUrlSource { } } -#[derive(Serialize, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct EmbedProvider { pub name: String, pub url: String, @@ -438,7 +438,7 @@ impl EmbedProvider { } } -#[derive(Serialize, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct EmbedAuthor { pub name: String, pub url: Option, @@ -464,17 +464,19 @@ pub enum AllowedMention { 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, @@ -488,10 +490,10 @@ impl AllowedMentions { users: Option>, replied_user: bool, ) -> Self { - let mut parse_strings: Vec = vec![]; + let mut parse_strings: Vec<&str> = vec![]; if let Some(parse) = parse { for x in parse { - parse_strings.push(resolve_allowed_mention_name(x)); + parse_strings.push(x.resolve()); } } @@ -506,7 +508,7 @@ 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), } @@ -519,7 +521,7 @@ impl Serialize for NonCompositeComponent { } } -#[derive(Serialize, Debug)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct ActionRow { #[serde(rename = "type")] pub component_type: u8, @@ -561,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, @@ -570,8 +572,8 @@ pub enum NonLinkButtonStyle { } impl NonLinkButtonStyle { - const fn get_button_style(&self) -> ButtonStyles { - match *self { + const fn get_button_style(self) -> ButtonStyles { + match self { Self::Primary => ButtonStyles::Primary, Self::Secondary => ButtonStyles::Secondary, Self::Success => ButtonStyles::Success, @@ -580,10 +582,10 @@ impl NonLinkButtonStyle { } } -// 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, @@ -608,7 +610,7 @@ impl Serialize for ButtonStyles { } } -#[derive(Serialize, Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct PartialEmoji { pub id: Snowflake, pub name: String, @@ -616,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, @@ -651,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, @@ -712,7 +714,7 @@ macro_rules! button_base_delegation { }; } -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct LinkButton { button_base: ButtonCommonBase, url: Option, @@ -735,6 +737,7 @@ impl LinkButton { button_base_delegation!(button_base); } +#[derive(Debug, Clone, PartialEq, Eq)] pub struct RegularButton { button_base: ButtonCommonBase, custom_id: Option, @@ -784,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,