diff --git a/book/src/chapter_1_crates/section_7_first_party/section_4_util.md b/book/src/chapter_1_crates/section_7_first_party/section_4_util.md index eb92bf47b5..9ef9c8b5cd 100644 --- a/book/src/chapter_1_crates/section_7_first_party/section_4_util.md +++ b/book/src/chapter_1_crates/section_7_first_party/section_4_util.md @@ -12,10 +12,12 @@ enabled via feature flags. ### Builder The `builder` feature enables builders for large structs. At the time of -writing, it contains the following builders: +writing, it contains the following: + - [`CommandBuilder`] - [`EmbedBuilder`] -- [`InteractionResponseData`] +- [`interaction_response`] builders module +- [`message`] component builders module #### Command example @@ -145,4 +147,5 @@ let timestamp = user.timestamp(); [`CommandBuilder`]: https://api.twilight.rs/twilight_util/builder/command/struct.CommandBuilder.html [`EmbedBuilder`]: https://api.twilight.rs/twilight_util/builder/embed/struct.EmbedBuilder.html -[`InteractionResponseDataBuilder`]: https://api.twilight.rs/twilight_util/builder/struct.InteractionResponseDataBuilder.html +[`interaction_response`]: https://api.twilight.rs/twilight_util/builder/interaction_response/index.html +[`message`]: https://api.twilight.rs/twilight_util/builder/message/index.html diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 0bd091331a..81a579b3b1 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -29,6 +29,7 @@ twilight-http = { path = "../twilight-http" } twilight-lavalink = { path = "../twilight-lavalink" } twilight-model = { path = "../twilight-model" } twilight-standby = { path = "../twilight-standby" } +twilight-util = { features = ["builder"], path = "../twilight-util" } [[example]] name = "cache-optimization" diff --git a/examples/model-webhook-slash.rs b/examples/model-webhook-slash.rs index 095c16bb99..df091a65c8 100644 --- a/examples/model-webhook-slash.rs +++ b/examples/model-webhook-slash.rs @@ -15,8 +15,9 @@ use std::{future::Future, net::SocketAddr}; use tokio::net::TcpListener; use twilight_model::{ application::interaction::{Interaction, InteractionType, application_command::CommandData}, - http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType}, + http::interaction::{InteractionResponse, InteractionResponseType}, }; +use twilight_util::builder::interaction_response::ChannelMessageBuilder; /// Public key given from Discord. static PUB_KEY: Lazy = Lazy::new(|| { @@ -142,24 +143,14 @@ async fn handler(data: Box) -> anyhow::Result /// Example of a handler that returns the formatted version of the interaction. async fn debug(data: Box) -> anyhow::Result { - Ok(InteractionResponse { - kind: InteractionResponseType::ChannelMessageWithSource, - data: Some(InteractionResponseData { - content: Some(format!("```rust\n{data:?}\n```")), - ..Default::default() - }), - }) + Ok(ChannelMessageBuilder::new() + .content(format!("```rust\n{data:?}\n```")) + .build()) } /// Example of interaction that responds with a message saying "Vroom vroom". async fn vroom(_: Box) -> anyhow::Result { - Ok(InteractionResponse { - kind: InteractionResponseType::ChannelMessageWithSource, - data: Some(InteractionResponseData { - content: Some("Vroom vroom".to_owned()), - ..Default::default() - }), - }) + Ok(ChannelMessageBuilder::new().content("Vroom vroom").build()) } #[tokio::main] diff --git a/twilight-http/src/client/interaction.rs b/twilight-http/src/client/interaction.rs index a50689dac0..57a34e4b8d 100644 --- a/twilight-http/src/client/interaction.rs +++ b/twilight-http/src/client/interaction.rs @@ -63,14 +63,14 @@ impl<'a> InteractionClient<'a> { /// Respond to an interaction, by its ID and token. /// /// For variants of [`InteractionResponse`] that contain - /// [`InteractionResponseData`], there is an [associated builder] in the + /// [`InteractionResponseData`], there are [associated builders] in the /// [`twilight-util`] crate. /// /// This endpoint is not bound to the application's global rate limit. /// /// [`InteractionResponseData`]: twilight_model::http::interaction::InteractionResponseData /// [`twilight-util`]: https://docs.rs/twilight-util/latest/index.html - /// [associated builder]: https://docs.rs/twilight-util/latest/twilight_util/builder/struct.InteractionResponseDataBuilder.html + /// [associated builders]: https://docs.rs/twilight-util/latest/twilight_util/builder/interaction_response/index.html pub const fn create_response( &'a self, interaction_id: Id, diff --git a/twilight-util/src/builder/command.rs b/twilight-util/src/builder/command.rs index 6c9840cb4e..8d5223cf3e 100644 --- a/twilight-util/src/builder/command.rs +++ b/twilight-util/src/builder/command.rs @@ -89,7 +89,6 @@ impl CommandBuilder { } /// Consume the builder, returning a [`Command`]. - #[allow(clippy::missing_const_for_fn)] #[must_use = "must be built into a command"] pub fn build(self) -> Command { self.0 @@ -245,7 +244,6 @@ impl AttachmentBuilder { } /// Consume the builder, returning the built command option. - #[allow(clippy::missing_const_for_fn)] #[must_use = "should be used in a command builder"] pub fn build(self) -> CommandOption { self.0 @@ -329,7 +327,6 @@ impl BooleanBuilder { } /// Consume the builder, returning the built command option. - #[allow(clippy::missing_const_for_fn)] #[must_use = "should be used in a command builder"] pub fn build(self) -> CommandOption { self.0 @@ -413,7 +410,6 @@ impl ChannelBuilder { } /// Consume the builder, returning the built command option. - #[allow(clippy::missing_const_for_fn)] #[must_use = "should be used in a command builder"] pub fn build(self) -> CommandOption { self.0 @@ -505,7 +501,6 @@ impl IntegerBuilder { } /// Consume the builder, returning the built command option. - #[allow(clippy::missing_const_for_fn)] #[must_use = "should be used in a command builder"] pub fn build(self) -> CommandOption { self.0 @@ -674,7 +669,6 @@ impl MentionableBuilder { } /// Consume the builder, returning the built command option. - #[allow(clippy::missing_const_for_fn)] #[must_use = "should be used in a command builder"] pub fn build(self) -> CommandOption { self.0 @@ -758,7 +752,6 @@ impl NumberBuilder { } /// Consume the builder, returning the built command option. - #[allow(clippy::missing_const_for_fn)] #[must_use = "should be used in a command builder"] pub fn build(self) -> CommandOption { self.0 @@ -927,7 +920,6 @@ impl RoleBuilder { } /// Consume the builder, returning the built command option. - #[allow(clippy::missing_const_for_fn)] #[must_use = "should be used in a command builder"] pub fn build(self) -> CommandOption { self.0 @@ -1011,7 +1003,6 @@ impl StringBuilder { } /// Consume the builder, returning the built command option. - #[allow(clippy::missing_const_for_fn)] #[must_use = "should be used in a command builder"] pub fn build(self) -> CommandOption { self.0 @@ -1183,7 +1174,6 @@ impl SubCommandBuilder { } /// Consume the builder, returning the built command option. - #[allow(clippy::missing_const_for_fn)] #[must_use = "should be used in a command builder"] pub fn build(self) -> CommandOption { self.0 @@ -1275,7 +1265,6 @@ impl SubCommandGroupBuilder { } /// Consume the builder, returning the built command option. - #[allow(clippy::missing_const_for_fn)] #[must_use = "should be used in a command builder"] pub fn build(self) -> CommandOption { self.0 @@ -1359,7 +1348,6 @@ impl UserBuilder { } /// Consume the builder, returning the built command option. - #[allow(clippy::missing_const_for_fn)] #[must_use = "should be used in a command builder"] pub fn build(self) -> CommandOption { self.0 diff --git a/twilight-util/src/builder/embed/author.rs b/twilight-util/src/builder/embed/author.rs index 5ed69bd35c..de64ba10f8 100644 --- a/twilight-util/src/builder/embed/author.rs +++ b/twilight-util/src/builder/embed/author.rs @@ -24,7 +24,6 @@ impl EmbedAuthorBuilder { } /// Build into an embed author. - #[allow(clippy::missing_const_for_fn)] #[must_use = "should be used as part of an embed builder"] pub fn build(self) -> EmbedAuthor { self.0 diff --git a/twilight-util/src/builder/embed/field.rs b/twilight-util/src/builder/embed/field.rs index 2095fb1fdb..36b6d5b17b 100644 --- a/twilight-util/src/builder/embed/field.rs +++ b/twilight-util/src/builder/embed/field.rs @@ -34,7 +34,6 @@ impl EmbedFieldBuilder { } /// Build into an embed field. - #[allow(clippy::missing_const_for_fn)] #[must_use = "should be used as part of an embed builder"] pub fn build(self) -> EmbedField { self.0 diff --git a/twilight-util/src/builder/embed/footer.rs b/twilight-util/src/builder/embed/footer.rs index de53bd66b6..91b3f238a8 100644 --- a/twilight-util/src/builder/embed/footer.rs +++ b/twilight-util/src/builder/embed/footer.rs @@ -28,7 +28,6 @@ impl EmbedFooterBuilder { } /// Build into an embed footer. - #[allow(clippy::missing_const_for_fn)] #[must_use = "should be used as part of an embed builder"] pub fn build(self) -> EmbedFooter { self.0 diff --git a/twilight-util/src/builder/embed/mod.rs b/twilight-util/src/builder/embed/mod.rs index b6f288e98c..e65b9ee25a 100644 --- a/twilight-util/src/builder/embed/mod.rs +++ b/twilight-util/src/builder/embed/mod.rs @@ -79,7 +79,6 @@ impl EmbedBuilder { } /// Build this into an embed. - #[allow(clippy::missing_const_for_fn)] #[must_use = "should be used as part of something like a message"] pub fn build(self) -> Embed { self.0 @@ -237,7 +236,6 @@ impl EmbedBuilder { /// .build(); /// # Ok(()) } /// ``` - #[allow(clippy::missing_const_for_fn)] pub fn image(mut self, image_source: ImageSource) -> Self { self.0.image = Some(EmbedImage { height: None, @@ -267,7 +265,6 @@ impl EmbedBuilder { /// .build(); /// # Ok(()) } /// ``` - #[allow(clippy::missing_const_for_fn)] pub fn thumbnail(mut self, image_source: ImageSource) -> Self { self.0.thumbnail = Some(EmbedThumbnail { height: None, diff --git a/twilight-util/src/builder/interaction_response.rs b/twilight-util/src/builder/interaction_response.rs new file mode 100644 index 0000000000..14b4f8ded0 --- /dev/null +++ b/twilight-util/src/builder/interaction_response.rs @@ -0,0 +1,297 @@ +//! Create [`InteractionResponse`]s with builders. +//! +//! # Example +//! +//! ``` +//! use twilight_util::builder::interaction_response::ChannelMessageBuilder; +//! +//! ChannelMessageBuilder::new().content("Congrats on sending your command!"); +//! ``` + +use twilight_model::{ + application::command::CommandOptionChoice, + channel::message::{AllowedMentions, Component, Embed, MessageFlags}, + http::{ + attachment::Attachment, + interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType}, + }, + poll::Poll, +}; + +/// Builder for a [`InteractionResponse`] of type [`InteractionResponseType::ApplicationCommandAutocompleteResult`]. +#[derive(Clone, Debug)] +#[must_use = "builders have no effect if unused"] +pub struct AutocompleteBuilder(InteractionResponseData); + +impl AutocompleteBuilder { + /// Creates a new default builder. + pub fn new(choices: impl IntoIterator) -> Self { + Self(InteractionResponseData { + choices: Some(FromIterator::from_iter(choices)), + ..Default::default() + }) + } + + /// Builds the [`InteractionResponse`]. + pub fn build(self) -> InteractionResponse { + InteractionResponse { + kind: InteractionResponseType::ApplicationCommandAutocompleteResult, + data: Some(self.0), + } + } +} + +impl From for InteractionResponse { + fn from(builder: AutocompleteBuilder) -> Self { + builder.build() + } +} + +/// Builder for a [`InteractionResponse`] of type [`InteractionResponseType::ChannelMessageWithSource`]. +#[derive(Clone, Debug, Default)] +#[must_use = "builders have no effect if unused"] +pub struct ChannelMessageBuilder(InteractionResponseData); + +impl ChannelMessageBuilder { + /// Creates a new default builder. + pub fn new() -> Self { + Self(InteractionResponseData::default()) + } + + /// Builds the [`InteractionResponse`]. + pub fn build(self) -> InteractionResponse { + InteractionResponse { + kind: InteractionResponseType::ChannelMessageWithSource, + data: Some(self.0), + } + } + + /// Sets the allowed mentions filter. + /// + /// Defaults to no filter. + pub fn allowed_mentions(mut self, allowed_mentions: AllowedMentions) -> Self { + self.0.allowed_mentions = Some(allowed_mentions); + + self + } + + /// Sets the attachments. + /// + /// Defaults to no attachments. + pub fn attachments(mut self, attachments: impl IntoIterator) -> Self { + self.0.attachments = Some(FromIterator::from_iter(attachments)); + + self + } + + /// Sets the components. + /// + /// Defaults to no components. + pub fn components( + mut self, + components: impl IntoIterator>, + ) -> Self { + self.0.components = Some(components.into_iter().map(Into::into).collect()); + + self + } + + /// Sets the content. + /// + /// Defaults to no content. + pub fn content(mut self, content: impl Into) -> Self { + self.0.content = Some(content.into()); + + self + } + + /// Sets the embeds. + /// + /// Defaults to no embeds. + pub fn embeds(mut self, embeds: impl IntoIterator) -> Self { + self.0.embeds = Some(FromIterator::from_iter(embeds)); + + self + } + + /// Sets the message flags. + /// + /// Defaults to no flags. + pub fn flags(mut self, flags: MessageFlags) -> Self { + self.0.flags = Some(flags); + + self + } + + /// Sets whether TTS is used. + /// + /// Defaults to `false`. + pub fn tts(mut self, tts: bool) -> Self { + self.0.tts = Some(tts); + + self + } + + /// Sets the poll. + /// + /// Defaults to no poll. + pub fn poll(mut self, poll: Poll) -> Self { + self.0.poll = Some(poll); + + self + } +} + +impl From for InteractionResponse { + fn from(builder: ChannelMessageBuilder) -> Self { + builder.build() + } +} + +/// Builder for a [`InteractionResponse`] of type [`InteractionResponseType::Modal`]. +#[derive(Clone, Debug)] +#[must_use = "builders have no effect if unused"] +pub struct ModalBuilder(InteractionResponseData); + +impl ModalBuilder { + /// Creates a new default builder. + pub fn new( + custom_id: impl Into, + title: impl Into, + components: impl IntoIterator>, + ) -> Self { + Self(InteractionResponseData { + components: Some(components.into_iter().map(Into::into).collect()), + custom_id: Some(custom_id.into()), + title: Some(title.into()), + ..Default::default() + }) + } + + /// Builds the [`InteractionResponse`]. + pub fn build(self) -> InteractionResponse { + InteractionResponse { + kind: InteractionResponseType::Modal, + data: Some(self.0), + } + } +} + +impl From for InteractionResponse { + fn from(builder: ModalBuilder) -> Self { + builder.build() + } +} + +/// Builder for a [`InteractionResponse`] of type [`InteractionResponseType::UpdateMessage`]. +#[derive(Clone, Debug, Default)] +#[must_use = "builders have no effect if unused"] +pub struct UpdateMessageBuilder(InteractionResponseData); + +impl UpdateMessageBuilder { + /// Creates a new default builder. + pub fn new() -> Self { + Self(InteractionResponseData::default()) + } + + /// Builds the [`InteractionResponse`]. + pub fn build(self) -> InteractionResponse { + InteractionResponse { + kind: InteractionResponseType::UpdateMessage, + data: Some(self.0), + } + } + + /// Sets the allowed mentions filter. + /// + /// Defaults to no filter. + pub fn allowed_mentions(mut self, allowed_mentions: AllowedMentions) -> Self { + self.0.allowed_mentions = Some(allowed_mentions); + + self + } + + /// Sets the attachments. + /// + /// Defaults to no attachments. + pub fn attachments(mut self, attachments: impl IntoIterator) -> Self { + self.0.attachments = Some(FromIterator::from_iter(attachments)); + + self + } + + /// Sets the components. + /// + /// Defaults to no components. + pub fn components( + mut self, + components: impl IntoIterator>, + ) -> Self { + self.0.components = Some(components.into_iter().map(Into::into).collect()); + + self + } + + /// Sets the content. + /// + /// Defaults to no content. + pub fn content(mut self, content: impl Into) -> Self { + self.0.content = Some(content.into()); + + self + } + + /// Sets the embeds. + /// + /// Defaults to no embeds. + pub fn embeds(mut self, embeds: impl IntoIterator) -> Self { + self.0.embeds = Some(FromIterator::from_iter(embeds)); + + self + } + + /// Sets the message flags. + /// + /// Defaults to no flags. + pub fn flags(mut self, flags: MessageFlags) -> Self { + self.0.flags = Some(flags); + + self + } + + /// Sets whether TTS is used. + /// + /// Defaults to `false`. + pub fn tts(mut self, tts: bool) -> Self { + self.0.tts = Some(tts); + + self + } + + /// Sets the poll. + /// + /// Defaults to no poll. + pub fn poll(mut self, poll: Poll) -> Self { + self.0.poll = Some(poll); + + self + } +} + +impl From for InteractionResponse { + fn from(builder: UpdateMessageBuilder) -> Self { + builder.build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use static_assertions::assert_impl_all; + use std::fmt::Debug; + + assert_impl_all!(AutocompleteBuilder: Clone, Debug, Send, Sync); + assert_impl_all!(ChannelMessageBuilder: Clone, Debug, Default, Send, Sync); + assert_impl_all!(ModalBuilder: Clone, Debug, Send, Sync); + assert_impl_all!(UpdateMessageBuilder: Clone, Debug, Default, Send, Sync); +} diff --git a/twilight-util/src/builder/interaction_response_data.rs b/twilight-util/src/builder/interaction_response_data.rs index 0fb705eecd..4e01258d77 100644 --- a/twilight-util/src/builder/interaction_response_data.rs +++ b/twilight-util/src/builder/interaction_response_data.rs @@ -1,3 +1,5 @@ +#![allow(deprecated)] + use twilight_model::{ application::command::CommandOptionChoice, channel::message::{AllowedMentions, Component, Embed, MessageFlags}, @@ -38,6 +40,7 @@ use twilight_model::{ /// assert_eq!(interaction_response_data.components, Some(vec![component])); /// ``` #[derive(Clone, Debug)] +#[deprecated = "use the `interaction_response` builders instead"] #[must_use = "builders have no effect if unused"] pub struct InteractionResponseDataBuilder(InteractionResponseData); @@ -60,7 +63,6 @@ impl InteractionResponseDataBuilder { } /// Consume the builder, returning an [`InteractionResponseData`]. - #[allow(clippy::missing_const_for_fn)] #[must_use = "builders have no effect if unused"] pub fn build(self) -> InteractionResponseData { self.0 @@ -69,7 +71,6 @@ impl InteractionResponseDataBuilder { /// Set the [`AllowedMentions`] of the callback. /// /// Defaults to [`None`]. - #[allow(clippy::missing_const_for_fn)] pub fn allowed_mentions(mut self, allowed_mentions: AllowedMentions) -> Self { self.0.allowed_mentions = Some(allowed_mentions); diff --git a/twilight-util/src/builder/message/mod.rs b/twilight-util/src/builder/message/mod.rs index e70453b858..f254d27676 100644 --- a/twilight-util/src/builder/message/mod.rs +++ b/twilight-util/src/builder/message/mod.rs @@ -1,4 +1,6 @@ -//! Create message components with a builder. +//! Create message [`Component`]s with builders. +//! +//! [`Component`]: twilight_model::channel::message::Component mod action_row; mod button; diff --git a/twilight-util/src/builder/mod.rs b/twilight-util/src/builder/mod.rs index 96c29df3f5..6df05e3046 100644 --- a/twilight-util/src/builder/mod.rs +++ b/twilight-util/src/builder/mod.rs @@ -1,9 +1,12 @@ //! Builders for large structs. +#![expect(clippy::missing_const_for_fn)] pub mod command; pub mod embed; +pub mod interaction_response; pub mod message; mod interaction_response_data; +#[allow(deprecated)] pub use self::interaction_response_data::InteractionResponseDataBuilder;