From 5d33055c95a1058f650038338bb3dd0c60a4eba1 Mon Sep 17 00:00:00 2001 From: ITOH Date: Sun, 29 May 2022 15:15:04 +0200 Subject: [PATCH 01/11] feat(util): add component builders --- util/src/builder/component/action_row.rs | 245 +++++++++++ util/src/builder/component/button.rs | 381 ++++++++++++++++++ util/src/builder/component/components.rs | 185 +++++++++ util/src/builder/component/mod.rs | 15 + util/src/builder/component/select_menu.rs | 351 ++++++++++++++++ .../builder/component/select_menu_option.rs | 294 ++++++++++++++ util/src/builder/component/text_input.rs | 325 +++++++++++++++ util/src/builder/mod.rs | 1 + validate/src/component.rs | 28 ++ 9 files changed, 1825 insertions(+) create mode 100644 util/src/builder/component/action_row.rs create mode 100644 util/src/builder/component/button.rs create mode 100644 util/src/builder/component/components.rs create mode 100644 util/src/builder/component/mod.rs create mode 100644 util/src/builder/component/select_menu.rs create mode 100644 util/src/builder/component/select_menu_option.rs create mode 100644 util/src/builder/component/text_input.rs diff --git a/util/src/builder/component/action_row.rs b/util/src/builder/component/action_row.rs new file mode 100644 index 00000000000..773fec1286a --- /dev/null +++ b/util/src/builder/component/action_row.rs @@ -0,0 +1,245 @@ +//! Create an [`ActionRow`] with a builder. +//! +//! # Example +//! ``` +//! use twilight_model::application::component::Component; +//! use twilight_util::builder::component::{ActionRowBuilder, ButtonBuilder}; +//! +//! # fn main() -> Result<(), Box> { +//! let action_row = ActionRowBuilder::new() +//! .add_component( +//! Component::Button( +//! ButtonBuilder::primary("button-1".to_owned()) +//! .label("Button".to_owned()) +//! .validate()?.build() +//! ) +//! ) +//! .validate()?.build(); +//! # Ok(()) } +//! ``` + +use twilight_model::application::component::{action_row::ActionRow, Component}; +use twilight_validate::component::{action_row as validate_action_row, ComponentValidationError}; + +/// Create an [`ActionRow`] with a builder. +/// +/// # Example +/// ``` +/// use twilight_model::application::component::Component; +/// use twilight_util::builder::component::{ActionRowBuilder, ButtonBuilder}; +/// +/// # fn main() -> Result<(), Box> { +/// let action_row = ActionRowBuilder::new() +/// .add_component( +/// Component::Button( +/// ButtonBuilder::primary("button-1".to_owned()) +/// .label("Button".to_owned()) +/// .validate()?.build() +/// ) +/// ) +/// .validate()?.build(); +/// # Ok(()) } +/// ``` +#[derive(Clone, Debug, Eq, PartialEq)] +#[must_use = "builders have no effect if unused"] +pub struct ActionRowBuilder(ActionRow); + +impl ActionRowBuilder { + /// Create a new builder to construct a [`SelectMenu`]. + pub const fn new() -> Self { + Self(ActionRow { + components: Vec::new(), + }) + } + + /// Consume the builder, returning a [`SelectMenu`]. + #[allow(clippy::missing_const_for_fn)] + #[must_use = "builders have no effect if unused"] + pub fn build(self) -> ActionRow { + self.0 + } + + /// Add a component to this action row. + /// + /// # Examples + /// + /// ```rust + /// use twilight_model::application::component::Component; + /// use twilight_util::builder::component::{ActionRowBuilder, ButtonBuilder}; + /// + /// # fn main() -> Result<(), Box> { + /// let action_row = ActionRowBuilder::new() + /// .add_component( + /// Component::Button( + /// ButtonBuilder::primary("button-1".to_owned()) + /// .label("Button".to_owned()) + /// .build() + /// ) + /// ) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + #[allow(clippy::missing_const_for_fn)] + #[must_use = "builders have no effect if unused"] + pub fn add_component(mut self, component: Component) -> Self { + self.0.components.push(component); + + self + } + + /// Add multiple components to this action row. + /// + /// # Examples + /// + /// ```rust + /// use twilight_model::application::component::Component; + /// use twilight_util::builder::component::{ActionRowBuilder, ButtonBuilder}; + /// + /// # fn main() -> Result<(), Box> { + /// let action_row = ActionRowBuilder::new() + /// .add_components( + /// &mut vec![ + /// Component::Button( + /// ButtonBuilder::primary("button-1".to_owned()) + /// .label("First".to_owned()) + /// .build() + /// ), + /// Component::Button( + /// ButtonBuilder::secondary("button-2".to_string()) + /// .label("Second".to_owned()) + /// .build() + /// ) + /// ] + /// ) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + #[allow(clippy::missing_const_for_fn)] + #[must_use = "builders have no effect if unused"] + pub fn add_components(mut self, components: &mut Vec) -> Self { + self.0.components.append(components); + + self + } + + /// Ensure the action row is valid. + /// + /// # Errors + /// + /// Refer to the documentation of [`twilight_validate::component::action_row`] for + /// possible errors. + pub fn validate(self) -> Result { + if let Err(source) = validate_action_row(&self.0) { + return Err(source); + } + + Ok(self) + } +} + +impl TryFrom for ActionRow { + type Error = ComponentValidationError; + + /// Convert a select menu builder into a select menu, validating its contents. + /// + /// This is equivalent to calling [`SelectMenuBuilder::validate`], then + /// [`SelectMenuBuilder::build`]. + fn try_from(builder: ActionRowBuilder) -> Result { + Ok(builder.validate()?.build()) + } +} + +#[cfg(test)] +mod test { + use super::ActionRowBuilder; + use static_assertions::assert_impl_all; + use std::{convert::TryFrom, fmt::Debug}; + use twilight_model::application::component::{ + button::ButtonStyle, ActionRow, Button, Component, + }; + + assert_impl_all!(ActionRowBuilder: Clone, Debug, Eq, PartialEq, Send, Sync); + assert_impl_all!(ActionRow: TryFrom); + + #[test] + fn test_action_row_builder() { + let expected = ActionRow { + components: Vec::new(), + }; + let actual = ActionRowBuilder::new().build(); + + assert_eq!(actual, expected); + } + + #[test] + fn test_action_row_builder_add_component() { + let expected = ActionRow { + components: Vec::from([Component::Button(Button { + custom_id: Some("button".into()), + disabled: false, + emoji: None, + label: Some("label".into()), + style: ButtonStyle::Primary, + url: None, + })]), + }; + let actual = ActionRowBuilder::new() + .add_component(Component::Button(Button { + custom_id: Some("button".into()), + disabled: false, + emoji: None, + label: Some("label".into()), + style: ButtonStyle::Primary, + url: None, + })) + .build(); + + assert_eq!(actual, expected); + } + + #[test] + fn test_action_row_builder_add_components() { + let expected = ActionRow { + components: Vec::from([ + Component::Button(Button { + custom_id: Some("button".into()), + disabled: false, + emoji: None, + label: Some("label".into()), + style: ButtonStyle::Primary, + url: None, + }), + Component::Button(Button { + custom_id: Some("button-2".into()), + disabled: false, + emoji: None, + label: Some("label".into()), + style: ButtonStyle::Primary, + url: None, + }), + ]), + }; + let actual = ActionRowBuilder::new() + .add_components(&mut Vec::from([ + Component::Button(Button { + custom_id: Some("button".into()), + disabled: false, + emoji: None, + label: Some("label".into()), + style: ButtonStyle::Primary, + url: None, + }), + Component::Button(Button { + custom_id: Some("button-2".into()), + disabled: false, + emoji: None, + label: Some("label".into()), + style: ButtonStyle::Primary, + url: None, + }), + ])) + .build(); + + assert_eq!(actual, expected); + } +} diff --git a/util/src/builder/component/button.rs b/util/src/builder/component/button.rs new file mode 100644 index 00000000000..13db92da77a --- /dev/null +++ b/util/src/builder/component/button.rs @@ -0,0 +1,381 @@ +//! Create a [`Button`] with a builder. +//! +//! # Example +//! ``` +//! use twilight_model::application::component::Component; +//! use twilight_util::builder::component::ButtonBuilder; +//! +//! # fn main() -> Result<(), Box> { +//! let component = Component::Button( +//! ButtonBuilder::primary("button_id".to_string()) +//! .label("Button label".to_string()) +//! .validate()?.build() +//! ); +//! # Ok(()) } +//! ``` + +use std::convert::TryFrom; + +use twilight_model::{ + application::component::{button::ButtonStyle, Button}, + channel::ReactionType, +}; +use twilight_validate::component::{button as validate_button, ComponentValidationError}; + +/// Create a [`Button`] with a builder. +/// +/// # Example +/// ``` +/// use twilight_model::application::component::Component; +/// use twilight_util::builder::component::ButtonBuilder; +/// # fn main() -> Result<(), Box> { +/// let component = Component::Button( +/// ButtonBuilder::primary("button_id".to_string()) +/// .label("Button label".to_string()) +/// .validate()?.build() +/// ); +/// # Ok(()) } +/// ``` +#[derive(Clone, Debug, Eq, PartialEq)] +#[must_use = "builders have no effect if unused"] +pub struct ButtonBuilder(Button); + +impl ButtonBuilder { + /// Create a new builder to construct a [`ButtonStyle::Primary`] styled [`Button`]. + pub const fn primary(custom_id: String) -> Self { + Self(Button { + style: ButtonStyle::Primary, + emoji: None, + label: None, + custom_id: Some(custom_id), + url: None, + disabled: false, + }) + } + + /// Create a new builder to construct a [`ButtonStyle::Secondary`] styled [`Button`]. + pub const fn secondary(custom_id: String) -> Self { + Self(Button { + style: ButtonStyle::Secondary, + emoji: None, + label: None, + custom_id: Some(custom_id), + url: None, + disabled: false, + }) + } + + /// Create a new builder to construct a [`ButtonStyle::Success`] styled [`Button`]. + pub const fn success(custom_id: String) -> Self { + Self(Button { + style: ButtonStyle::Success, + emoji: None, + label: None, + custom_id: Some(custom_id), + url: None, + disabled: false, + }) + } + + /// Create a new builder to construct a [`ButtonStyle::Danger`] styled [`Button`]. + pub const fn danger(custom_id: String) -> Self { + Self(Button { + style: ButtonStyle::Danger, + emoji: None, + label: None, + custom_id: Some(custom_id), + url: None, + disabled: false, + }) + } + + /// Create a new builder to construct a [`ButtonStyle::Link`] styled [`Button`]. + pub const fn link(url: String) -> Self { + Self(Button { + style: ButtonStyle::Link, + emoji: None, + label: None, + custom_id: None, + url: Some(url), + disabled: false, + }) + } + + /// Consume the builder, returning a [`Button`]. + #[must_use = "builders have no effect if unused"] + pub fn build(self) -> Button { + self.0 + } + + /// Set whether the button is disabled or not. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::ButtonBuilder; + /// + /// # fn main() -> Result<(), Box> { + /// let button = ButtonBuilder::primary("unique-id".into()) + /// .label("disabled button".into()) + /// .disable(true) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + pub const fn disable(mut self, disabled: bool) -> Self { + self.0.disabled = disabled; + + self + } + + /// Set the emoji of the button. + /// + /// # Examples + /// + /// ```rust + /// use twilight_model::channel::ReactionType; + /// use twilight_util::builder::component::ButtonBuilder; + /// + /// # fn main() -> Result<(), Box> { + /// let button = ButtonBuilder::primary("unique-id".into()) + /// .emoji(ReactionType::Unicode { + /// name: "🙂".into() + /// }) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + #[allow(clippy::missing_const_for_fn)] + pub fn emoji(mut self, emoji: ReactionType) -> Self { + self.0.emoji = Some(emoji); + + self + } + + /// Set the label of the button. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::ButtonBuilder; + /// + /// # fn main() -> Result<(), Box> { + /// let button = ButtonBuilder::primary("unique-id".into()) + /// .label("twilight".into()) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + #[allow(clippy::missing_const_for_fn)] + pub fn label(mut self, label: String) -> Self { + self.0.label = Some(label); + + self + } + + /// Consume the builder, ensure that the button is valid and if so it returns a [`Button`]. + /// + /// # Errors + /// + /// Refer to the documentation of [`twilight_validate::component::button`] for + /// possible errors. + pub fn validate(self) -> Result { + if let Err(source) = validate_button(&self.0) { + return Err(source); + } + + Ok(self) + } +} + +impl TryFrom for Button { + type Error = ComponentValidationError; + + /// Convert a button builder into a button, validating its contents. + /// + /// This is equivalent to calling [`ButtonBuilder::validate`], then + /// [`ButtonBuilder::build`]. + fn try_from(builder: ButtonBuilder) -> Result { + Ok(builder.validate()?.build()) + } +} + +#[cfg(test)] +mod tests { + use super::ButtonBuilder; + use static_assertions::assert_impl_all; + use std::{convert::TryFrom, fmt::Debug}; + use twilight_model::{ + application::component::{button::ButtonStyle, Button}, + channel::ReactionType, + }; + + assert_impl_all!(ButtonBuilder: Clone, Debug, Eq, PartialEq, Send, Sync); + assert_impl_all!(Button: TryFrom); + + #[test] + fn test_builder_primary() { + let button = ButtonBuilder::primary("primary-button".to_owned()) + .label("primary button".to_owned()) + .build(); + + let expected = Button { + style: ButtonStyle::Primary, + emoji: None, + label: Some("primary button".to_owned()), + custom_id: Some("primary-button".to_owned()), + url: None, + disabled: false, + }; + + assert_eq!(button, expected); + } + + #[test] + fn test_builder_secondary() { + let button = ButtonBuilder::secondary("secondary-button".to_owned()) + .label("secondary button".to_owned()) + .build(); + + let expected = Button { + style: ButtonStyle::Secondary, + emoji: None, + label: Some("secondary button".to_owned()), + custom_id: Some("secondary-button".to_owned()), + url: None, + disabled: false, + }; + + assert_eq!(button, expected); + } + + #[test] + fn test_builder_success() { + let button = ButtonBuilder::success("success-button".to_owned()) + .label("success button".to_owned()) + .build(); + + let expected = Button { + style: ButtonStyle::Success, + emoji: None, + label: Some("success button".to_owned()), + custom_id: Some("success-button".to_owned()), + url: None, + disabled: false, + }; + + assert_eq!(button, expected); + } + + #[test] + fn test_builder_danger() { + let button = ButtonBuilder::danger("danger-button".to_owned()) + .label("danger button".to_owned()) + .build(); + + let expected = Button { + style: ButtonStyle::Danger, + emoji: None, + label: Some("danger button".to_owned()), + custom_id: Some("danger-button".to_owned()), + url: None, + disabled: false, + }; + + assert_eq!(button, expected); + } + + #[test] + fn test_builder_link() { + let button = ButtonBuilder::link("https://twilight.rs".to_owned()) + .label("link button".to_owned()) + .build(); + + let expected = Button { + style: ButtonStyle::Link, + emoji: None, + label: Some("link button".to_owned()), + custom_id: None, + url: Some("https://twilight.rs".to_owned()), + disabled: false, + }; + + assert_eq!(button, expected); + } + + #[test] + fn test_builder_disabled_button() { + let button = ButtonBuilder::primary("disabled-button".to_owned()) + .label("disabled button".to_owned()) + .disable(true) + .build(); + + let expected = Button { + style: ButtonStyle::Primary, + emoji: None, + label: Some("disabled button".to_owned()), + custom_id: Some("disabled-button".to_owned()), + url: None, + disabled: true, + }; + + assert_eq!(button, expected); + } + + #[test] + fn test_builder_explicit_enabled_button() { + let button = ButtonBuilder::primary("enabled-button".to_owned()) + .label("enabled button".to_owned()) + .disable(false) + .build(); + + let expected = Button { + style: ButtonStyle::Primary, + emoji: None, + label: Some("enabled button".to_owned()), + custom_id: Some("enabled-button".to_owned()), + url: None, + disabled: false, + }; + + assert_eq!(button, expected); + } + + #[test] + fn test_builder_with_emoji() { + let button = ButtonBuilder::primary("emoji-button".to_owned()) + .emoji(ReactionType::Unicode { + name: "\u{1f9ea}".to_owned(), + }) + .build(); + + let expected = Button { + style: ButtonStyle::Primary, + emoji: Some(ReactionType::Unicode { + name: "\u{1f9ea}".to_owned(), + }), + label: None, + custom_id: Some("emoji-button".to_owned()), + url: None, + disabled: false, + }; + + assert_eq!(button, expected); + } + + #[test] + fn test_builder_try_from() { + let button = Button::try_from( + ButtonBuilder::primary("primary-button".to_owned()).label("primary button".to_owned()), + ) + .unwrap(); + + let expected = Button { + style: ButtonStyle::Primary, + emoji: None, + label: Some("primary button".to_owned()), + custom_id: Some("primary-button".to_owned()), + url: None, + disabled: false, + }; + + assert_eq!(button, expected); + } +} diff --git a/util/src/builder/component/components.rs b/util/src/builder/component/components.rs new file mode 100644 index 00000000000..a8bbf9a8906 --- /dev/null +++ b/util/src/builder/component/components.rs @@ -0,0 +1,185 @@ +use twilight_model::application::component::{ActionRow, Button, Component, SelectMenu, TextInput}; +use twilight_validate::component::{ + action_row as validate_action_row, ComponentValidationError, ACTION_ROW_COMPONENT_COUNT, + COMPONENT_COUNT, +}; + +/// +#[derive(Clone, Debug)] +#[must_use = "builders have no effect if unused"] +pub struct ComponentsBuilder(Vec); + +impl ComponentsBuilder { + /// Create a new builder to construct a Vec<[`Component`]>. + pub const fn new() -> Self { + Self(Vec::new()) + } + + /// Add a new action row to this builder. + /// + /// If the builder is already full, + /// the action row won't be added. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::ComponentsBuilder; + /// + /// # fn main() -> Result<(), Box> { + /// let components = ComponentsBuilder::new() + /// .action_row(Vec::new()) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + pub fn action_row(mut self, components: Vec) -> Self { + if self.is_full() { + return self; + } + + match self.0.iter_mut().last() { + Some(action_row) if action_row.components.is_empty() => { + action_row.components = components; + } + _ => { + self.0.push(ActionRow { components }); + } + } + + self + } + + /// Consume the builder, returning a Vec<[`Component`]>. + #[allow(clippy::missing_const_for_fn)] + #[must_use = "builders have no effect if unused"] + pub fn build(self) -> Vec { + self.0 + .into_iter() + .map(|action_row| Component::ActionRow(action_row)) + .collect() + } + + /// Add a button to this builder. + /// + /// If there is an action row available the button will be added to it + /// else a new action row will be created. + /// + /// If all action rows are full the button won't be added. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::{ComponentsBuilder, ButtonBuilder}; + /// + /// # fn main() -> Result<(), Box> { + /// let components = ComponentsBuilder::new() + /// .button( + /// ButtonBuilder::primary("button-1".to_owned()) + /// .label("Button".to_owned()) + /// .build() + /// ) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + pub fn button(mut self, button: Button) -> Self { + let action_row: Option<&mut ActionRow> = self.0.iter_mut().last(); + + match action_row { + Some(action_row) => { + if action_row + .components + .iter() + .any(|c| !matches!(c, Component::Button(_))) + || action_row.components.len() == ACTION_ROW_COMPONENT_COUNT + { + if self.is_full() { + return self; + } + + return self.action_row(Vec::from([Component::Button(button)])); + } + + action_row.components.push(Component::Button(button)); + self + } + None => self.action_row(Vec::from([Component::Button(button)])), + } + } + + fn is_full(&self) -> bool { + self.0.len() == COMPONENT_COUNT + } + + /// Add a select menu to this builder. + /// + /// If there is an empty action row available the select menu will be added to it + /// else a new action row will be created. + /// + /// If all action rows are full the action row won't be added. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::{ComponentsBuilder, SelectMenuBuilder, SelectMenuOptionBuilder}; + /// + /// # fn main() -> Result<(), Box> { + /// let components = ComponentsBuilder::new() + /// .select_menu( + /// SelectMenuBuilder::new("characters".to_owned()) + /// .add_options( + /// &mut vec![ + /// SelectMenuOptionBuilder::new("twilight-sparkle".to_string(), "Twilight Sparkle".to_string()) + /// .default(true) + /// .build(), + /// SelectMenuOptionBuilder::new("rarity".to_string(), "Rarity".to_string()) + /// .build(), + /// ] + /// ).build() + /// ) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + pub fn select_menu(self, select_menu: SelectMenu) -> Self { + self.action_row(Vec::from([Component::SelectMenu(select_menu)])) + } + + /// Add a text input to this builder. + /// + /// If there is an action row available the text input will be added to it + /// else a new action row will be created. + /// + /// If all action rows are full the text input won't be added. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::{ComponentsBuilder, TextInputBuilder}; + /// + /// # fn main() -> Result<(), Box> { + /// let components = ComponentsBuilder::new() + /// .text_input( + /// TextInputBuilder::short("input-1".to_owned(), "Input One".to_owned()) + /// .build() + /// ) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + pub fn text_input(self, text_input: TextInput) -> Self { + self.action_row(Vec::from([Component::TextInput(text_input)])) + } + + /// Consume the builder, ensure that the action rows and their components are valid. + /// + /// # Errors + /// + /// Refer to the documentation of [`twilight_validate::component::validate_action_row`] for + /// possible errors. + pub fn validate(self) -> Result { + for action_row in self.0.iter() { + if let Err(source) = validate_action_row(action_row) { + return Err(source); + } + } + + Ok(self) + } +} diff --git a/util/src/builder/component/mod.rs b/util/src/builder/component/mod.rs new file mode 100644 index 00000000000..dd770d8b520 --- /dev/null +++ b/util/src/builder/component/mod.rs @@ -0,0 +1,15 @@ +//! Component builder thing + +mod action_row; +mod button; +mod components; +mod select_menu; +mod select_menu_option; +mod text_input; + +pub use action_row::ActionRowBuilder; +pub use button::ButtonBuilder; +pub use components::ComponentsBuilder; +pub use select_menu::SelectMenuBuilder; +pub use select_menu_option::SelectMenuOptionBuilder; +pub use text_input::TextInputBuilder; diff --git a/util/src/builder/component/select_menu.rs b/util/src/builder/component/select_menu.rs new file mode 100644 index 00000000000..380badc3c8c --- /dev/null +++ b/util/src/builder/component/select_menu.rs @@ -0,0 +1,351 @@ +//! Create a [`SelectMenu`] with a builder. +//! +//! # Example +//! ``` +//! use twilight_model::{application::component::Component, channel::ReactionType, id::Id}; +//! use twilight_util::builder::component::{SelectMenuBuilder, SelectMenuOptionBuilder}; +//! # fn main() -> Result<(), Box> { +//! let component = Component::SelectMenu( +//! SelectMenuBuilder::new("characters".to_owned()) +//! .add_options( +//! &mut vec![ +//! SelectMenuOptionBuilder::new("twilight-sparkle".to_string(), "Twilight Sparkle".to_string()) +//! .default(true) +//! .emoji(ReactionType::Custom { +//! animated: false, +//! id: Id::new(754728776402993173), +//! name: Some("sparkle".to_string()), +//! }) +//! .build(), +//! SelectMenuOptionBuilder::new("rarity".to_string(), "Rarity".to_string()) +//! .emoji(ReactionType::Custom { +//! animated: false, +//! id: Id::new(765306914153299978), +//! name: Some("rarsmile".to_string()), +//! }) +//! .build(), +//! ] +//! ).validate()?.build() +//! ); +//! # Ok(()) } +//! ``` + +use twilight_model::application::component::{select_menu::SelectMenuOption, SelectMenu}; +use twilight_validate::component::{select_menu as validate_select_menu, ComponentValidationError}; + +/// Create a [`SelectMenu`] with a builder. +/// +/// # Example +/// ``` +/// use twilight_model::{application::component::Component, channel::ReactionType, id::Id}; +/// use twilight_util::builder::component::{SelectMenuBuilder, SelectMenuOptionBuilder}; +/// # fn main() -> Result<(), Box> { +/// let component = Component::SelectMenu( +/// SelectMenuBuilder::new("characters".to_owned()) +/// .add_options( +/// &mut vec![ +/// SelectMenuOptionBuilder::new("twilight-sparkle".to_string(), "Twilight Sparkle".to_string()) +/// .default(true) +/// .emoji(ReactionType::Custom { +/// animated: false, +/// id: Id::new(754728776402993173), +/// name: Some("sparkle".to_string()), +/// }) +/// .build(), +/// SelectMenuOptionBuilder::new("rarity".to_string(), "Rarity".to_string()) +/// .emoji(ReactionType::Custom { +/// animated: false, +/// id: Id::new(765306914153299978), +/// name: Some("rarsmile".to_string()), +/// }) +/// .build(), +/// ] +/// ).validate()?.build() +/// ); +/// # Ok(()) } +/// ``` +#[derive(Clone, Debug, Eq, PartialEq)] +#[must_use = "builders have no effect if unused"] +pub struct SelectMenuBuilder(SelectMenu); + +impl SelectMenuBuilder { + /// Create a new builder to construct a [`SelectMenu`]. + pub const fn new(custom_id: String) -> Self { + Self(SelectMenu { + custom_id, + disabled: false, + max_values: None, + min_values: None, + options: Vec::new(), + placeholder: None, + }) + } + + /// Consume the builder, returning a [`SelectMenu`]. + #[allow(clippy::missing_const_for_fn)] + #[must_use = "builders have no effect if unused"] + pub fn build(self) -> SelectMenu { + self.0 + } + + /// Set the minimum values for this select menu. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::SelectMenuBuilder; + /// + /// # fn main() -> Result<(), Box> { + /// let option = SelectMenuBuilder::new("menu".into()) + /// .min_values(2) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + #[allow(clippy::missing_const_for_fn)] + #[must_use = "builders have no effect if unused"] + pub fn min_values(mut self, min_values: u8) -> Self { + self.0.min_values = Some(min_values); + + self + } + + /// Set the maximum values for this select menu. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::SelectMenuBuilder; + /// + /// # fn main() -> Result<(), Box> { + /// let option = SelectMenuBuilder::new("menu".into()) + /// .max_values(10) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + #[must_use = "builders have no effect if unused"] + pub const fn max_values(mut self, max_values: u8) -> Self { + self.0.max_values = Some(max_values); + + self + } + + /// Set whether the select menu is enabled or not. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::SelectMenuBuilder; + /// + /// # fn main() -> Result<(), Box> { + /// let option = SelectMenuBuilder::new("menu".into()) + /// .disable(true) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + #[allow(clippy::missing_const_for_fn)] + #[must_use = "builders have no effect if unused"] + pub fn disable(mut self, disabled: bool) -> Self { + self.0.disabled = disabled; + + self + } + + /// Set the placeholder for this select menu. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::SelectMenuBuilder; + /// + /// # fn main() -> Result<(), Box> { + /// let option = SelectMenuBuilder::new("menu".into()) + /// .placeholder("this is a select menu".into()) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + #[allow(clippy::missing_const_for_fn)] + #[must_use = "builders have no effect if unused"] + pub fn placeholder(mut self, placeholder: String) -> Self { + self.0.placeholder = Some(placeholder); + + self + } + + /// Add an option to this select menu. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::{SelectMenuBuilder, SelectMenuOptionBuilder}; + /// + /// # fn main() -> Result<(), Box> { + /// let option = SelectMenuBuilder::new("menu".into()) + /// .add_option( + /// SelectMenuOptionBuilder::new("rarity".to_string(), "Rarity".to_string()) + /// .build() + /// ) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + #[allow(clippy::missing_const_for_fn)] + #[must_use = "builders have no effect if unused"] + pub fn add_option(mut self, option: SelectMenuOption) -> Self { + self.0.options.push(option); + + self + } + + /// Add multiple options to this select menu. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::{SelectMenuBuilder, SelectMenuOptionBuilder}; + /// + /// # fn main() -> Result<(), Box> { + /// let option = SelectMenuBuilder::new("menu".into()) + /// .add_options( + /// &mut vec![ + /// SelectMenuOptionBuilder::new("twilight-sparkle".to_string(), "Twilight Sparkle".to_string()) + /// .build(), + /// SelectMenuOptionBuilder::new("rarity".to_string(), "Rarity".to_string()) + /// .build(), + /// ] + /// ) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + #[allow(clippy::missing_const_for_fn)] + #[must_use = "builders have no effect if unused"] + pub fn add_options(mut self, options: &mut Vec) -> Self { + self.0.options.append(options); + + self + } + + /// Consume the builder, ensure that the option is valid and if so it returns a [`SelectMenu`]. + /// + /// # Errors + /// + /// Refer to the documentation of [`twilight_validate::component::select_menu`] for + /// possible errors. + pub fn validate(self) -> Result { + if let Err(source) = validate_select_menu(&self.0) { + return Err(source); + } + + Ok(self) + } +} + +impl TryFrom for SelectMenu { + type Error = ComponentValidationError; + + /// Convert a select menu builder into a select menu, validating its contents. + /// + /// This is equivalent to calling [`SelectMenuBuilder::validate`], then + /// [`SelectMenuBuilder::build`]. + fn try_from(builder: SelectMenuBuilder) -> Result { + Ok(builder.validate()?.build()) + } +} + +#[cfg(test)] +mod test { + use super::SelectMenuBuilder; + use static_assertions::assert_impl_all; + use std::{convert::TryFrom, fmt::Debug}; + use twilight_model::application::component::SelectMenu; + + assert_impl_all!(SelectMenuBuilder: Clone, Debug, Eq, PartialEq, Send, Sync); + assert_impl_all!(SelectMenu: TryFrom); + + #[test] + fn test_select_menu_builder() { + let select_menu = SelectMenuBuilder::new("a-menu".to_owned()).build(); + + let expected = SelectMenu { + custom_id: "a-menu".to_owned(), + disabled: false, + max_values: None, + min_values: None, + options: Vec::new(), + placeholder: None, + }; + + assert_eq!(select_menu, expected); + } + + #[test] + fn test_select_menu_builder_disabled() { + let select_menu = SelectMenuBuilder::new("a-menu".to_owned()) + .disable(true) + .build(); + + let expected = SelectMenu { + custom_id: "a-menu".to_owned(), + disabled: true, + max_values: None, + min_values: None, + options: Vec::new(), + placeholder: None, + }; + + assert_eq!(select_menu, expected); + } + + #[test] + fn test_select_menu_builder_explicit_enabled() { + let select_menu = SelectMenuBuilder::new("a-menu".to_owned()) + .disable(false) + .build(); + + let expected = SelectMenu { + custom_id: "a-menu".to_owned(), + disabled: false, + max_values: None, + min_values: None, + options: Vec::new(), + placeholder: None, + }; + + assert_eq!(select_menu, expected); + } + + #[test] + fn test_select_menu_builder_limited_values() { + let select_menu = SelectMenuBuilder::new("a-menu".to_owned()) + .max_values(10) + .min_values(2) + .build(); + + let expected = SelectMenu { + custom_id: "a-menu".to_owned(), + disabled: false, + max_values: Some(10), + min_values: Some(2), + options: Vec::new(), + placeholder: None, + }; + + assert_eq!(select_menu, expected); + } + + #[test] + fn test_select_menu_builder_placeholder() { + let select_menu = SelectMenuBuilder::new("a-menu".to_owned()) + .placeholder("I'm a placeholder".to_owned()) + .build(); + + let expected = SelectMenu { + custom_id: "a-menu".to_owned(), + disabled: false, + max_values: None, + min_values: None, + options: Vec::new(), + placeholder: Some("I'm a placeholder".to_owned()), + }; + + assert_eq!(select_menu, expected); + } +} diff --git a/util/src/builder/component/select_menu_option.rs b/util/src/builder/component/select_menu_option.rs new file mode 100644 index 00000000000..f65fa575805 --- /dev/null +++ b/util/src/builder/component/select_menu_option.rs @@ -0,0 +1,294 @@ +//! Create a [`SelectMenuOption`] with a builder. +//! +//! # Example +//! ``` +//! use twilight_model::{application::component::Component, channel::ReactionType, id::Id}; +//! use twilight_util::builder::component::{SelectMenuBuilder, SelectMenuOptionBuilder}; +//! +//! # fn main() -> Result<(), Box> { +//! let component = Component::SelectMenu( +//! SelectMenuBuilder::new("characters".to_owned()) +//! .add_options( +//! &mut vec![ +//! SelectMenuOptionBuilder::new("twilight-sparkle".to_string(), "Twilight Sparkle".to_string()) +//! .default(true) +//! .emoji(ReactionType::Custom { +//! animated: false, +//! id: Id::new(754728776402993173), +//! name: Some("sparkle".to_string()), +//! }) +//! .build(), +//! SelectMenuOptionBuilder::new("rarity".to_string(), "Rarity".to_string()) +//! .emoji(ReactionType::Custom { +//! animated: false, +//! id: Id::new(765306914153299978), +//! name: Some("rarsmile".to_string()), +//! }) +//! .build(), +//! ] +//! ).validate()?.build() +//! ); +//! # Ok(()) } +//! ``` + +use std::convert::TryFrom; + +use twilight_model::{ + application::component::select_menu::SelectMenuOption, channel::ReactionType, +}; +use twilight_validate::component::{ + select_menu_option as validate_select_menu_option, ComponentValidationError, +}; + +/// Create a [`SelectMenuOption`] with a builder. +/// +/// # Example +/// ``` +/// use twilight_model::{application::component::Component, channel::ReactionType, id::Id}; +/// use twilight_util::builder::component::{SelectMenuBuilder, SelectMenuOptionBuilder}; +/// # fn main() -> Result<(), Box> { +/// let component = Component::SelectMenu( +/// SelectMenuBuilder::new("characters".to_owned()) +/// .add_options( +/// &mut vec![ +/// SelectMenuOptionBuilder::new("twilight-sparkle".to_string(), "Twilight Sparkle".to_string()) +/// .default(true) +/// .emoji(ReactionType::Custom { +/// animated: false, +/// id: Id::new(754728776402993173), +/// name: Some("sparkle".to_string()), +/// }) +/// .build(), +/// SelectMenuOptionBuilder::new("rarity".to_string(), "Rarity".to_string()) +/// .emoji(ReactionType::Custom { +/// animated: false, +/// id: Id::new(765306914153299978), +/// name: Some("rarsmile".to_string()), +/// }) +/// .build(), +/// ] +/// ).validate()?.build() +/// ); +/// # Ok(()) } +/// ``` +#[derive(Clone, Debug, Eq, PartialEq)] +#[must_use = "builders have no effect if unused"] +pub struct SelectMenuOptionBuilder(SelectMenuOption); + +impl SelectMenuOptionBuilder { + /// Create a new builder to construct a [`SelectMenuOption`]. + pub const fn new(value: String, label: String) -> Self { + Self(SelectMenuOption { + default: false, + description: None, + emoji: None, + label, + value, + }) + } + + /// Consume the builder, returning a [`SelectMenuOption`]. + #[allow(clippy::missing_const_for_fn)] + #[must_use = "builders have no effect if unused"] + pub fn build(self) -> SelectMenuOption { + self.0 + } + + /// Set whether this option is selected by default. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::SelectMenuOptionBuilder; + /// + /// # fn main() -> Result<(), Box> { + /// let option = SelectMenuOptionBuilder::new("option-1".into(), "Option One".into()) + /// .default(true) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + pub const fn default(mut self, default: bool) -> Self { + self.0.default = default; + + self + } + + /// Set the description of this option. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::SelectMenuOptionBuilder; + /// + /// # fn main() -> Result<(), Box> { + /// let option = SelectMenuOptionBuilder::new("option-1".into(), "Option One".into()) + /// .description("The first option.".into()) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + #[allow(clippy::missing_const_for_fn)] + pub fn description(mut self, description: String) -> Self { + self.0.description = Some(description); + + self + } + + /// Set the emoji of this option. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::SelectMenuOptionBuilder; + /// use twilight_model::channel::ReactionType; + /// + /// # fn main() -> Result<(), Box> { + /// let option = SelectMenuOptionBuilder::new("option-1".into(), "Option One".into()) + /// .emoji(ReactionType::Unicode { + /// name: "1️⃣".into() + /// }) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + #[allow(clippy::missing_const_for_fn)] + pub fn emoji(mut self, emoji: ReactionType) -> Self { + self.0.emoji = Some(emoji); + + self + } + + /// Consume the builder, ensure that the option is valid and if so it returns a [`SelectMenuOption`]. + /// + /// # Errors + /// + /// Refer to the documentation of [`twilight_validate::component::select_menu_option`] for + /// possible errors. + pub fn validate(self) -> Result { + if let Err(source) = validate_select_menu_option(&self.0) { + return Err(source); + } + + Ok(self) + } +} + +impl TryFrom for SelectMenuOption { + type Error = ComponentValidationError; + + /// Convert a `SelectMenuOptionBuilder` into a `SelectMenuOption`. + /// + /// This is equivalent to calling [`SelectMenuOptionBuilder::validate`] + /// then [`SelectMenuOptionBuilder::build`]. + fn try_from(builder: SelectMenuOptionBuilder) -> Result { + Ok(builder.validate()?.build()) + } +} + +#[cfg(test)] +mod tests { + use super::SelectMenuOptionBuilder; + use static_assertions::assert_impl_all; + use std::{convert::TryFrom, fmt::Debug}; + use twilight_model::{ + application::component::select_menu::SelectMenuOption, channel::ReactionType, + }; + + assert_impl_all!( + SelectMenuOptionBuilder: Clone, + Debug, + Eq, + PartialEq, + Send, + Sync + ); + assert_impl_all!(SelectMenuOption: TryFrom); + + #[test] + fn test_normal() { + let select_menu = + SelectMenuOptionBuilder::new("value".to_owned(), "label".to_owned()).build(); + + let expected = SelectMenuOption { + default: false, + description: None, + emoji: None, + label: "label".to_owned(), + value: "value".to_owned(), + }; + + assert_eq!(select_menu, expected); + } + + #[test] + fn test_description() { + let select_menu = SelectMenuOptionBuilder::new("value".to_owned(), "label".to_owned()) + .description("description".to_owned()) + .build(); + + let expected = SelectMenuOption { + default: false, + description: Some("description".to_owned()), + emoji: None, + label: "label".to_owned(), + value: "value".to_owned(), + }; + + assert_eq!(select_menu, expected); + } + + #[test] + fn test_default() { + let select_menu = SelectMenuOptionBuilder::new("value".to_owned(), "label".to_owned()) + .default(true) + .build(); + + let expected = SelectMenuOption { + default: true, + description: None, + emoji: None, + label: "label".to_owned(), + value: "value".to_owned(), + }; + + assert_eq!(select_menu, expected); + } + + #[test] + fn test_emoji() { + let select_menu = SelectMenuOptionBuilder::new("value".to_owned(), "label".to_owned()) + .emoji(ReactionType::Unicode { + name: "\u{1f9ea}".to_owned(), + }) + .build(); + + let expected = SelectMenuOption { + default: false, + description: None, + emoji: Some(ReactionType::Unicode { + name: "\u{1f9ea}".to_owned(), + }), + label: "label".to_owned(), + value: "value".to_owned(), + }; + + assert_eq!(select_menu, expected); + } + + #[test] + fn test_builder_try_from() { + let select_menu = SelectMenuOption::try_from( + SelectMenuOptionBuilder::new("value".to_owned(), "label".to_owned()) + .description("testing".to_owned()), + ) + .unwrap(); + + let expected = SelectMenuOption { + default: false, + description: Some("testing".to_owned()), + emoji: None, + label: "label".to_owned(), + value: "value".to_owned(), + }; + + assert_eq!(select_menu, expected); + } +} diff --git a/util/src/builder/component/text_input.rs b/util/src/builder/component/text_input.rs new file mode 100644 index 00000000000..ffcac3ccd95 --- /dev/null +++ b/util/src/builder/component/text_input.rs @@ -0,0 +1,325 @@ +//! Create a [`TextInput`] with a builder. +//! +//! # Example +//! ``` +//! use twilight_model::application::component::Component; +//! use twilight_util::builder::component::TextInputBuilder; +//! # fn main() -> Result<(), Box> { +//! let component = Component::TextInput( +//! TextInputBuilder::paragraph("input-1".to_owned(), "Input".to_owned()) +//! .min_length(20) +//! .required(true) +//! .validate()?.build() +//! ); +//! # Ok(()) } +//! ``` + +use std::convert::TryFrom; + +use twilight_model::application::component::{text_input::TextInputStyle, TextInput}; +use twilight_validate::component::{text_input as validate_text_input, ComponentValidationError}; + +/// Create a [`TextInput`] with a builder. +/// +/// # Example +/// ``` +/// use twilight_model::application::component::Component; +/// use twilight_util::builder::component::TextInputBuilder; +/// # fn main() -> Result<(), Box> { +/// let component = Component::TextInput( +/// TextInputBuilder::paragraph("input-1".to_owned(), "Input".to_owned()) +/// .min_length(20) +/// .required(true) +/// .validate()?.build() +/// ); +/// # Ok(()) } +/// ``` + +#[derive(Clone, Debug, Eq, PartialEq)] +#[must_use = "builders have no effect if unused"] +pub struct TextInputBuilder(TextInput); + +impl TextInputBuilder { + /// Create a new builder to construct a [`TextInputStyle::Short`] styled [`TextInput`]. + pub const fn short(custom_id: String, label: String) -> Self { + Self(TextInput { + custom_id, + label, + max_length: None, + min_length: None, + placeholder: None, + required: None, + style: TextInputStyle::Short, + value: None, + }) + } + + /// Create a new builder to construct a [`TextInputStyle::Paragraph`] styled [`TextInput`]. + pub const fn paragraph(custom_id: String, label: String) -> Self { + Self(TextInput { + custom_id, + label, + max_length: None, + min_length: None, + placeholder: None, + required: None, + style: TextInputStyle::Paragraph, + value: None, + }) + } + + /// Consume the builder, returning a [`TextInput`]. + #[allow(clippy::missing_const_for_fn)] + #[must_use = "builders have no effect if unused"] + pub fn build(self) -> TextInput { + self.0 + } + + /// Set the maximum amount of characters allowed to be entered in this text input. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::TextInputBuilder; + /// + /// # fn main() -> Result<(), Box> { + /// let option = TextInputBuilder::short("input-1".into(), "Input One".into()) + /// .max_length(100) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + pub const fn max_length(mut self, max_length: u16) -> Self { + self.0.max_length = Some(max_length); + + self + } + + /// Set the minimum amount of characters necessary to be entered in this text input. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::TextInputBuilder; + /// + /// # fn main() -> Result<(), Box> { + /// let option = TextInputBuilder::short("input-1".into(), "Input One".into()) + /// .min_length(10) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + pub const fn min_length(mut self, min_length: u16) -> Self { + self.0.min_length = Some(min_length); + + self + } + + /// Set the placeholder for this text input. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::TextInputBuilder; + /// + /// # fn main() -> Result<(), Box> { + /// let option = TextInputBuilder::short("input-1".into(), "Input One".into()) + /// .placeholder("This is the first input".into()) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + pub fn placeholder(mut self, placeholder: String) -> Self { + self.0.placeholder = Some(placeholder); + + self + } + + /// Set whether this text input is required or not. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::TextInputBuilder; + /// + /// # fn main() -> Result<(), Box> { + /// let option = TextInputBuilder::short("input-1".into(), "Input One".into()) + /// .required(true) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + pub const fn required(mut self, required: bool) -> Self { + self.0.required = Some(required); + + self + } + + /// Set the pre-filled value for this text input. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::TextInputBuilder; + /// + /// # fn main() -> Result<(), Box> { + /// let option = TextInputBuilder::short("input-1".into(), "Input One".into()) + /// .value("This is the first input".into()) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + pub fn value(mut self, value: String) -> Self { + self.0.value = Some(value); + + self + } + + /// Consume the builder, ensure that the option is valid and if so it returns a [`SelectMenuOption`]. + /// + /// # Errors + /// + /// Refer to the documentation of [`twilight_validate::component::select_menu_option`] for + /// possible errors. + pub fn validate(self) -> Result { + if let Err(source) = validate_text_input(&self.0) { + return Err(source); + } + + Ok(self) + } +} + +impl TryFrom for TextInput { + type Error = ComponentValidationError; + + /// Convert a `TextInputBuilder` into a `TextInput`. + /// + /// This is equivalent to calling [`TextInputBuilder::validate`] + /// then [`TextInputBuilder::build`]. + fn try_from(builder: TextInputBuilder) -> Result { + Ok(builder.validate()?.build()) + } +} + +#[cfg(test)] +mod tests { + use super::TextInputBuilder; + use static_assertions::assert_impl_all; + use std::{convert::TryFrom, fmt::Debug}; + use twilight_model::application::component::{text_input::TextInputStyle, TextInput}; + + assert_impl_all!(TextInputBuilder: Clone, Debug, Eq, PartialEq, Send, Sync); + assert_impl_all!(TextInput: TryFrom); + + #[test] + fn test_text_input_builder_short() { + let expected = TextInput { + custom_id: "input".into(), + label: "label".into(), + max_length: None, + min_length: None, + placeholder: None, + required: None, + style: TextInputStyle::Short, + value: None, + }; + + let actual = TextInputBuilder::short("input".to_owned(), "label".to_owned()).build(); + + assert_eq!(actual, expected); + } + + #[test] + fn test_text_input_builder_paragraph() { + let expected = TextInput { + custom_id: "input".into(), + label: "label".into(), + max_length: None, + min_length: None, + placeholder: None, + required: None, + style: TextInputStyle::Paragraph, + value: None, + }; + + let actual = TextInputBuilder::paragraph("input".to_owned(), "label".to_owned()).build(); + + assert_eq!(actual, expected); + } + + #[test] + fn test_text_input_builder_max_length() { + let expected = TextInput { + custom_id: "input".into(), + label: "label".into(), + max_length: Some(100), + min_length: None, + placeholder: None, + required: None, + style: TextInputStyle::Short, + value: None, + }; + + let actual = TextInputBuilder::short("input".to_owned(), "label".to_owned()) + .max_length(100) + .build(); + + assert_eq!(actual, expected); + } + + #[test] + fn test_text_input_builder_min_length() { + let expected = TextInput { + custom_id: "input".into(), + label: "label".into(), + max_length: None, + min_length: Some(10), + placeholder: None, + required: None, + style: TextInputStyle::Short, + value: None, + }; + + let actual = TextInputBuilder::short("input".to_owned(), "label".to_owned()) + .min_length(10) + .build(); + + assert_eq!(actual, expected); + } + + #[test] + fn test_text_input_builder_placeholder() { + let expected = TextInput { + custom_id: "input".into(), + label: "label".into(), + max_length: None, + min_length: None, + placeholder: Some("Enter some text".into()), + required: None, + style: TextInputStyle::Short, + value: None, + }; + + let actual = TextInputBuilder::short("input".to_owned(), "label".to_owned()) + .placeholder("Enter some text".into()) + .build(); + + assert_eq!(actual, expected); + } + + #[test] + fn test_text_input_builder_required() { + let expected = TextInput { + custom_id: "input".into(), + label: "label".into(), + max_length: None, + min_length: None, + placeholder: None, + required: Some(true), + style: TextInputStyle::Short, + value: None, + }; + + let actual = TextInputBuilder::short("input".to_owned(), "label".to_owned()) + .required(true) + .build(); + + assert_eq!(actual, expected); + } +} diff --git a/util/src/builder/mod.rs b/util/src/builder/mod.rs index 63027759425..3b59957c602 100644 --- a/util/src/builder/mod.rs +++ b/util/src/builder/mod.rs @@ -2,6 +2,7 @@ #![allow(clippy::module_name_repetitions)] pub mod command; +pub mod component; pub mod embed; mod interaction_response_data; diff --git a/validate/src/component.rs b/validate/src/component.rs index 3afb1c264c8..2e4ff8ab8e8 100644 --- a/validate/src/component.rs +++ b/validate/src/component.rs @@ -685,6 +685,34 @@ pub fn select_menu(select_menu: &SelectMenu) -> Result<(), ComponentValidationEr Ok(()) } +/// Ensure that a select menu option is correct. +/// +/// # Errors +/// +/// Returns an error of type [`SelectOptionDescriptionLength`] if a provided +/// select option description is too long. +/// +/// Returns an error of type [`SelectOptionLabelLength`] if a provided select +/// option label is too long. +/// +/// Returns an error of type [`SelectOptionValueLength`] error type if +/// a provided select option value is too long. +/// +/// [`SelectOptionDescriptionLength`]: ComponentValidationErrorType::SelectOptionDescriptionLength +/// [`SelectOptionLabelLength`]: ComponentValidationErrorType::SelectOptionLabelLength +/// [`SelectOptionValueLength`]: ComponentValidationErrorType::SelectOptionValueLength +pub fn select_menu_option( + select_menu_option: &SelectMenuOption, +) -> Result<(), ComponentValidationError> { + self::component_select_option_label(&select_menu_option.label)?; + self::component_select_option_value(&select_menu_option.value)?; + + if let Some(description) = select_menu_option.description.as_ref() { + self::component_option_description(description)?; + } + + Ok(()) +} /// Ensure that a text input is correct. /// /// # Errors From cfeaf507617e5914e5dd17bfe883b3fe1b0e3af9 Mon Sep 17 00:00:00 2001 From: ITOH Date: Sun, 29 May 2022 16:22:47 +0200 Subject: [PATCH 02/11] more stuff --- util/src/builder/component/action_row.rs | 18 +- util/src/builder/component/button.rs | 70 ++-- util/src/builder/component/components.rs | 303 +++++++++++++++++- util/src/builder/component/select_menu.rs | 28 +- .../builder/component/select_menu_option.rs | 46 +-- util/src/builder/component/text_input.rs | 16 +- 6 files changed, 380 insertions(+), 101 deletions(-) diff --git a/util/src/builder/component/action_row.rs b/util/src/builder/component/action_row.rs index 773fec1286a..67c2976ba6f 100644 --- a/util/src/builder/component/action_row.rs +++ b/util/src/builder/component/action_row.rs @@ -9,8 +9,8 @@ //! let action_row = ActionRowBuilder::new() //! .add_component( //! Component::Button( -//! ButtonBuilder::primary("button-1".to_owned()) -//! .label("Button".to_owned()) +//! ButtonBuilder::primary("button-1".to_string()) +//! .label("Button".to_string()) //! .validate()?.build() //! ) //! ) @@ -32,8 +32,8 @@ use twilight_validate::component::{action_row as validate_action_row, ComponentV /// let action_row = ActionRowBuilder::new() /// .add_component( /// Component::Button( -/// ButtonBuilder::primary("button-1".to_owned()) -/// .label("Button".to_owned()) +/// ButtonBuilder::primary("button-1".to_string()) +/// .label("Button".to_string()) /// .validate()?.build() /// ) /// ) @@ -71,8 +71,8 @@ impl ActionRowBuilder { /// let action_row = ActionRowBuilder::new() /// .add_component( /// Component::Button( - /// ButtonBuilder::primary("button-1".to_owned()) - /// .label("Button".to_owned()) + /// ButtonBuilder::primary("button-1".to_string()) + /// .label("Button".to_string()) /// .build() /// ) /// ) @@ -100,13 +100,13 @@ impl ActionRowBuilder { /// .add_components( /// &mut vec![ /// Component::Button( - /// ButtonBuilder::primary("button-1".to_owned()) - /// .label("First".to_owned()) + /// ButtonBuilder::primary("button-1".to_string()) + /// .label("First".to_string()) /// .build() /// ), /// Component::Button( /// ButtonBuilder::secondary("button-2".to_string()) - /// .label("Second".to_owned()) + /// .label("Second".to_string()) /// .build() /// ) /// ] diff --git a/util/src/builder/component/button.rs b/util/src/builder/component/button.rs index 13db92da77a..2b63fc7c945 100644 --- a/util/src/builder/component/button.rs +++ b/util/src/builder/component/button.rs @@ -212,15 +212,15 @@ mod tests { #[test] fn test_builder_primary() { - let button = ButtonBuilder::primary("primary-button".to_owned()) - .label("primary button".to_owned()) + let button = ButtonBuilder::primary("primary-button".to_string()) + .label("primary button".to_string()) .build(); let expected = Button { style: ButtonStyle::Primary, emoji: None, - label: Some("primary button".to_owned()), - custom_id: Some("primary-button".to_owned()), + label: Some("primary button".to_string()), + custom_id: Some("primary-button".to_string()), url: None, disabled: false, }; @@ -230,15 +230,15 @@ mod tests { #[test] fn test_builder_secondary() { - let button = ButtonBuilder::secondary("secondary-button".to_owned()) - .label("secondary button".to_owned()) + let button = ButtonBuilder::secondary("secondary-button".to_string()) + .label("secondary button".to_string()) .build(); let expected = Button { style: ButtonStyle::Secondary, emoji: None, - label: Some("secondary button".to_owned()), - custom_id: Some("secondary-button".to_owned()), + label: Some("secondary button".to_string()), + custom_id: Some("secondary-button".to_string()), url: None, disabled: false, }; @@ -248,15 +248,15 @@ mod tests { #[test] fn test_builder_success() { - let button = ButtonBuilder::success("success-button".to_owned()) - .label("success button".to_owned()) + let button = ButtonBuilder::success("success-button".to_string()) + .label("success button".to_string()) .build(); let expected = Button { style: ButtonStyle::Success, emoji: None, - label: Some("success button".to_owned()), - custom_id: Some("success-button".to_owned()), + label: Some("success button".to_string()), + custom_id: Some("success-button".to_string()), url: None, disabled: false, }; @@ -266,15 +266,15 @@ mod tests { #[test] fn test_builder_danger() { - let button = ButtonBuilder::danger("danger-button".to_owned()) - .label("danger button".to_owned()) + let button = ButtonBuilder::danger("danger-button".to_string()) + .label("danger button".to_string()) .build(); let expected = Button { style: ButtonStyle::Danger, emoji: None, - label: Some("danger button".to_owned()), - custom_id: Some("danger-button".to_owned()), + label: Some("danger button".to_string()), + custom_id: Some("danger-button".to_string()), url: None, disabled: false, }; @@ -284,16 +284,16 @@ mod tests { #[test] fn test_builder_link() { - let button = ButtonBuilder::link("https://twilight.rs".to_owned()) - .label("link button".to_owned()) + let button = ButtonBuilder::link("https://twilight.rs".to_string()) + .label("link button".to_string()) .build(); let expected = Button { style: ButtonStyle::Link, emoji: None, - label: Some("link button".to_owned()), + label: Some("link button".to_string()), custom_id: None, - url: Some("https://twilight.rs".to_owned()), + url: Some("https://twilight.rs".to_string()), disabled: false, }; @@ -302,16 +302,16 @@ mod tests { #[test] fn test_builder_disabled_button() { - let button = ButtonBuilder::primary("disabled-button".to_owned()) - .label("disabled button".to_owned()) + let button = ButtonBuilder::primary("disabled-button".to_string()) + .label("disabled button".to_string()) .disable(true) .build(); let expected = Button { style: ButtonStyle::Primary, emoji: None, - label: Some("disabled button".to_owned()), - custom_id: Some("disabled-button".to_owned()), + label: Some("disabled button".to_string()), + custom_id: Some("disabled-button".to_string()), url: None, disabled: true, }; @@ -321,16 +321,16 @@ mod tests { #[test] fn test_builder_explicit_enabled_button() { - let button = ButtonBuilder::primary("enabled-button".to_owned()) - .label("enabled button".to_owned()) + let button = ButtonBuilder::primary("enabled-button".to_string()) + .label("enabled button".to_string()) .disable(false) .build(); let expected = Button { style: ButtonStyle::Primary, emoji: None, - label: Some("enabled button".to_owned()), - custom_id: Some("enabled-button".to_owned()), + label: Some("enabled button".to_string()), + custom_id: Some("enabled-button".to_string()), url: None, disabled: false, }; @@ -340,19 +340,19 @@ mod tests { #[test] fn test_builder_with_emoji() { - let button = ButtonBuilder::primary("emoji-button".to_owned()) + let button = ButtonBuilder::primary("emoji-button".to_string()) .emoji(ReactionType::Unicode { - name: "\u{1f9ea}".to_owned(), + name: "\u{1f9ea}".to_string(), }) .build(); let expected = Button { style: ButtonStyle::Primary, emoji: Some(ReactionType::Unicode { - name: "\u{1f9ea}".to_owned(), + name: "\u{1f9ea}".to_string(), }), label: None, - custom_id: Some("emoji-button".to_owned()), + custom_id: Some("emoji-button".to_string()), url: None, disabled: false, }; @@ -363,15 +363,15 @@ mod tests { #[test] fn test_builder_try_from() { let button = Button::try_from( - ButtonBuilder::primary("primary-button".to_owned()).label("primary button".to_owned()), + ButtonBuilder::primary("primary-button".to_string()).label("primary button".to_owned()), ) .unwrap(); let expected = Button { style: ButtonStyle::Primary, emoji: None, - label: Some("primary button".to_owned()), - custom_id: Some("primary-button".to_owned()), + label: Some("primary button".to_string()), + custom_id: Some("primary-button".to_string()), url: None, disabled: false, }; diff --git a/util/src/builder/component/components.rs b/util/src/builder/component/components.rs index a8bbf9a8906..812f7703f6b 100644 --- a/util/src/builder/component/components.rs +++ b/util/src/builder/component/components.rs @@ -1,11 +1,43 @@ +//! Create a [`Vec`] with a builder. +//! +//! # Example +//! ``` +//! use twilight_util::builder::component::{ButtonBuilder, ComponentsBuilder}; +//! +//! # fn main() -> Result<(), Box> { +//! let components = ComponentsBuilder::new() +//! .button( +//! ButtonBuilder::primary("button-1".to_string()) +//! .label("Button".to_string()) +//! .build() +//! ) +//! .validate()?.build(); +//! # Ok(()) } +//! ``` + use twilight_model::application::component::{ActionRow, Button, Component, SelectMenu, TextInput}; use twilight_validate::component::{ action_row as validate_action_row, ComponentValidationError, ACTION_ROW_COMPONENT_COUNT, COMPONENT_COUNT, }; +/// Create a [`Vec`] with a builder. +/// +/// # Example +/// ``` +/// use twilight_util::builder::component::{ButtonBuilder, ComponentsBuilder}; /// -#[derive(Clone, Debug)] +/// # fn main() -> Result<(), Box> { +/// let components = ComponentsBuilder::new() +/// .button( +/// ButtonBuilder::primary("button-1".to_string()) +/// .label("Button".to_string()) +/// .build() +/// ) +/// .validate()?.build(); +/// # Ok(()) } +/// ``` +#[derive(Clone, Debug, Eq, PartialEq)] #[must_use = "builders have no effect if unused"] pub struct ComponentsBuilder(Vec); @@ -23,15 +55,36 @@ impl ComponentsBuilder { /// # Examples /// /// ```rust - /// use twilight_util::builder::component::ComponentsBuilder; + /// use twilight_model::application::component::Component; + /// use twilight_util::builder::component::{ActionRowBuilder, ButtonBuilder, ComponentsBuilder}; /// /// # fn main() -> Result<(), Box> { /// let components = ComponentsBuilder::new() - /// .action_row(Vec::new()) + /// .action_row( + /// ActionRowBuilder::new() + /// .add_component( + /// Component::Button( + /// ButtonBuilder::primary("button-1".to_string()) + /// .label("Button".to_string()) + /// .build() + /// ) + /// ) + /// .build() + /// ) /// .validate()?.build(); /// # Ok(()) } /// ``` - pub fn action_row(mut self, components: Vec) -> Self { + pub fn action_row(mut self, action_row: ActionRow) -> Self { + if self.is_full() { + return self; + } + + self.0.push(action_row); + + self + } + + fn action_row_components(mut self, components: Vec) -> Self { if self.is_full() { return self; } @@ -73,8 +126,8 @@ impl ComponentsBuilder { /// # fn main() -> Result<(), Box> { /// let components = ComponentsBuilder::new() /// .button( - /// ButtonBuilder::primary("button-1".to_owned()) - /// .label("Button".to_owned()) + /// ButtonBuilder::primary("button-1".to_string()) + /// .label("Button".to_string()) /// .build() /// ) /// .validate()?.build(); @@ -95,13 +148,13 @@ impl ComponentsBuilder { return self; } - return self.action_row(Vec::from([Component::Button(button)])); + return self.action_row_components(Vec::from([Component::Button(button)])); } action_row.components.push(Component::Button(button)); self } - None => self.action_row(Vec::from([Component::Button(button)])), + None => self.action_row_components(Vec::from([Component::Button(button)])), } } @@ -124,7 +177,7 @@ impl ComponentsBuilder { /// # fn main() -> Result<(), Box> { /// let components = ComponentsBuilder::new() /// .select_menu( - /// SelectMenuBuilder::new("characters".to_owned()) + /// SelectMenuBuilder::new("characters".to_string()) /// .add_options( /// &mut vec![ /// SelectMenuOptionBuilder::new("twilight-sparkle".to_string(), "Twilight Sparkle".to_string()) @@ -139,7 +192,7 @@ impl ComponentsBuilder { /// # Ok(()) } /// ``` pub fn select_menu(self, select_menu: SelectMenu) -> Self { - self.action_row(Vec::from([Component::SelectMenu(select_menu)])) + self.action_row_components(Vec::from([Component::SelectMenu(select_menu)])) } /// Add a text input to this builder. @@ -157,14 +210,14 @@ impl ComponentsBuilder { /// # fn main() -> Result<(), Box> { /// let components = ComponentsBuilder::new() /// .text_input( - /// TextInputBuilder::short("input-1".to_owned(), "Input One".to_owned()) + /// TextInputBuilder::short("input-1".to_string(), "Input One".to_owned()) /// .build() /// ) /// .validate()?.build(); /// # Ok(()) } /// ``` pub fn text_input(self, text_input: TextInput) -> Self { - self.action_row(Vec::from([Component::TextInput(text_input)])) + self.action_row_components(Vec::from([Component::TextInput(text_input)])) } /// Consume the builder, ensure that the action rows and their components are valid. @@ -183,3 +236,229 @@ impl ComponentsBuilder { Ok(self) } } + +impl TryFrom for Vec { + type Error = ComponentValidationError; + + /// Convert a components builder into a `Vec`, validating its contents. + /// + /// This is equivalent to calling [`Components::validate`], then + /// [`ComponentsBuilder::build`]. + fn try_from(builder: ComponentsBuilder) -> Result { + Ok(builder.validate()?.build()) + } +} + +#[cfg(test)] +mod test { + use super::ComponentsBuilder; + use static_assertions::assert_impl_all; + use std::{convert::TryFrom, fmt::Debug}; + use twilight_model::application::component::{ + button::ButtonStyle, text_input::TextInputStyle, ActionRow, Button, Component, SelectMenu, + TextInput, + }; + + assert_impl_all!(ComponentsBuilder: Clone, Debug, Eq, PartialEq, Send, Sync); + assert_impl_all!(Vec: TryFrom); + + fn action_row(components: Vec) -> ActionRow { + ActionRow { components } + } + + fn button(custom_id: &str) -> Button { + Button { + custom_id: Some(custom_id.into()), + disabled: false, + emoji: None, + label: Some("label".into()), + style: ButtonStyle::Primary, + url: None, + } + } + + fn select_menu(custom_id: &str) -> SelectMenu { + SelectMenu { + custom_id: custom_id.into(), + disabled: false, + min_values: None, + max_values: None, + options: Vec::new(), + placeholder: None, + } + } + + fn text_input(custom_id: &str) -> TextInput { + TextInput { + custom_id: custom_id.into(), + label: "label".into(), + max_length: None, + min_length: None, + placeholder: None, + required: None, + style: TextInputStyle::Short, + value: None, + } + } + + #[test] + fn builder() { + let expected: Vec = Vec::new(); + let actual = ComponentsBuilder::new().build(); + + assert_eq!(actual, expected); + } + + #[test] + fn one_action_row() { + let expected = Vec::from([Component::ActionRow(action_row(Vec::new()))]); + let actual = ComponentsBuilder::new() + .action_row(action_row(Vec::new())) + .build(); + + assert_eq!(actual, expected); + } + + #[test] + fn multiple_action_rows() { + let expected = Vec::from([ + Component::ActionRow(action_row(Vec::new())), + Component::ActionRow(action_row(Vec::new())), + ]); + let actual = ComponentsBuilder::new() + .action_row(action_row(Vec::new())) + .action_row(action_row(Vec::new())) + .build(); + + assert_eq!(actual, expected); + } + + #[test] + fn one_button() { + let expected = Vec::from([Component::ActionRow(ActionRow { + components: Vec::from([Component::Button(button("button"))]), + })]); + let actual = ComponentsBuilder::new().button(button("button")).build(); + + assert_eq!(actual, expected); + } + + #[test] + fn multiple_buttons() { + let expected = Vec::from([Component::ActionRow(ActionRow { + components: Vec::from([ + Component::Button(button("button-1")), + Component::Button(button("button-2")), + ]), + })]); + let actual = ComponentsBuilder::new() + .button(button("button-1")) + .button(button("button-2")) + .build(); + + assert_eq!(actual, expected); + } + + #[test] + fn button_with_existing_action_row() { + let expected = Vec::from([Component::ActionRow(ActionRow { + components: Vec::from([Component::Button(button("button"))]), + })]); + let actual = ComponentsBuilder::new() + .action_row(ActionRow { + components: Vec::new(), + }) + .button(button("button")) + .build(); + + assert_eq!(actual, expected); + } + + #[test] + fn overflowing_buttons() { + let expected = Vec::from([ + Component::ActionRow(ActionRow { + components: Vec::from([ + Component::Button(button("button-1")), + Component::Button(button("button-2")), + Component::Button(button("button-3")), + Component::Button(button("button-4")), + Component::Button(button("button-5")), + ]), + }), + Component::ActionRow(ActionRow { + components: Vec::from([Component::Button(button("button-6"))]), + }), + ]); + let actual = ComponentsBuilder::new() + .button(button("button-1")) + .button(button("button-2")) + .button(button("button-3")) + .button(button("button-4")) + .button(button("button-5")) + .button(button("button-6")) + .build(); + + assert_eq!(actual, expected); + } + + #[test] + fn one_select_menu() { + let expected = Vec::from([Component::ActionRow(action_row(Vec::from([ + Component::SelectMenu(select_menu("select")), + ])))]); + let actual = ComponentsBuilder::new() + .select_menu(select_menu("select")) + .build(); + + assert_eq!(actual, expected); + } + + #[test] + fn multiple_select_menus() { + let expected = Vec::from([ + Component::ActionRow(action_row(Vec::from([Component::SelectMenu(select_menu( + "select-1", + ))]))), + Component::ActionRow(action_row(Vec::from([Component::SelectMenu(select_menu( + "select-2", + ))]))), + ]); + let actual = ComponentsBuilder::new() + .select_menu(select_menu("select-1")) + .select_menu(select_menu("select-2")) + .build(); + + assert_eq!(actual, expected); + } + + #[test] + fn one_text_input() { + let expected = Vec::from([Component::ActionRow(action_row(Vec::from([ + Component::TextInput(text_input("input")), + ])))]); + let actual = ComponentsBuilder::new() + .text_input(text_input("input")) + .build(); + + assert_eq!(actual, expected); + } + + #[test] + fn multiple_text_inputs() { + let expected = Vec::from([ + Component::ActionRow(action_row(Vec::from([Component::TextInput(text_input( + "input-1", + ))]))), + Component::ActionRow(action_row(Vec::from([Component::TextInput(text_input( + "input-2", + ))]))), + ]); + let actual = ComponentsBuilder::new() + .text_input(text_input("input-1")) + .text_input(text_input("input-2")) + .build(); + + assert_eq!(actual, expected); + } +} diff --git a/util/src/builder/component/select_menu.rs b/util/src/builder/component/select_menu.rs index 380badc3c8c..7f2ff800578 100644 --- a/util/src/builder/component/select_menu.rs +++ b/util/src/builder/component/select_menu.rs @@ -6,7 +6,7 @@ //! use twilight_util::builder::component::{SelectMenuBuilder, SelectMenuOptionBuilder}; //! # fn main() -> Result<(), Box> { //! let component = Component::SelectMenu( -//! SelectMenuBuilder::new("characters".to_owned()) +//! SelectMenuBuilder::new("characters".to_string()) //! .add_options( //! &mut vec![ //! SelectMenuOptionBuilder::new("twilight-sparkle".to_string(), "Twilight Sparkle".to_string()) @@ -41,7 +41,7 @@ use twilight_validate::component::{select_menu as validate_select_menu, Componen /// use twilight_util::builder::component::{SelectMenuBuilder, SelectMenuOptionBuilder}; /// # fn main() -> Result<(), Box> { /// let component = Component::SelectMenu( -/// SelectMenuBuilder::new("characters".to_owned()) +/// SelectMenuBuilder::new("characters".to_string()) /// .add_options( /// &mut vec![ /// SelectMenuOptionBuilder::new("twilight-sparkle".to_string(), "Twilight Sparkle".to_string()) @@ -262,10 +262,10 @@ mod test { #[test] fn test_select_menu_builder() { - let select_menu = SelectMenuBuilder::new("a-menu".to_owned()).build(); + let select_menu = SelectMenuBuilder::new("a-menu".to_string()).build(); let expected = SelectMenu { - custom_id: "a-menu".to_owned(), + custom_id: "a-menu".to_string(), disabled: false, max_values: None, min_values: None, @@ -278,12 +278,12 @@ mod test { #[test] fn test_select_menu_builder_disabled() { - let select_menu = SelectMenuBuilder::new("a-menu".to_owned()) + let select_menu = SelectMenuBuilder::new("a-menu".to_string()) .disable(true) .build(); let expected = SelectMenu { - custom_id: "a-menu".to_owned(), + custom_id: "a-menu".to_string(), disabled: true, max_values: None, min_values: None, @@ -296,12 +296,12 @@ mod test { #[test] fn test_select_menu_builder_explicit_enabled() { - let select_menu = SelectMenuBuilder::new("a-menu".to_owned()) + let select_menu = SelectMenuBuilder::new("a-menu".to_string()) .disable(false) .build(); let expected = SelectMenu { - custom_id: "a-menu".to_owned(), + custom_id: "a-menu".to_string(), disabled: false, max_values: None, min_values: None, @@ -314,13 +314,13 @@ mod test { #[test] fn test_select_menu_builder_limited_values() { - let select_menu = SelectMenuBuilder::new("a-menu".to_owned()) + let select_menu = SelectMenuBuilder::new("a-menu".to_string()) .max_values(10) .min_values(2) .build(); let expected = SelectMenu { - custom_id: "a-menu".to_owned(), + custom_id: "a-menu".to_string(), disabled: false, max_values: Some(10), min_values: Some(2), @@ -333,17 +333,17 @@ mod test { #[test] fn test_select_menu_builder_placeholder() { - let select_menu = SelectMenuBuilder::new("a-menu".to_owned()) - .placeholder("I'm a placeholder".to_owned()) + let select_menu = SelectMenuBuilder::new("a-menu".to_string()) + .placeholder("I'm a placeholder".to_string()) .build(); let expected = SelectMenu { - custom_id: "a-menu".to_owned(), + custom_id: "a-menu".to_string(), disabled: false, max_values: None, min_values: None, options: Vec::new(), - placeholder: Some("I'm a placeholder".to_owned()), + placeholder: Some("I'm a placeholder".to_string()), }; assert_eq!(select_menu, expected); diff --git a/util/src/builder/component/select_menu_option.rs b/util/src/builder/component/select_menu_option.rs index f65fa575805..c6e257c95c3 100644 --- a/util/src/builder/component/select_menu_option.rs +++ b/util/src/builder/component/select_menu_option.rs @@ -7,7 +7,7 @@ //! //! # fn main() -> Result<(), Box> { //! let component = Component::SelectMenu( -//! SelectMenuBuilder::new("characters".to_owned()) +//! SelectMenuBuilder::new("characters".to_string()) //! .add_options( //! &mut vec![ //! SelectMenuOptionBuilder::new("twilight-sparkle".to_string(), "Twilight Sparkle".to_string()) @@ -48,7 +48,7 @@ use twilight_validate::component::{ /// use twilight_util::builder::component::{SelectMenuBuilder, SelectMenuOptionBuilder}; /// # fn main() -> Result<(), Box> { /// let component = Component::SelectMenu( -/// SelectMenuBuilder::new("characters".to_owned()) +/// SelectMenuBuilder::new("characters".to_string()) /// .add_options( /// &mut vec![ /// SelectMenuOptionBuilder::new("twilight-sparkle".to_string(), "Twilight Sparkle".to_string()) @@ -205,14 +205,14 @@ mod tests { #[test] fn test_normal() { let select_menu = - SelectMenuOptionBuilder::new("value".to_owned(), "label".to_owned()).build(); + SelectMenuOptionBuilder::new("value".to_string(), "label".to_owned()).build(); let expected = SelectMenuOption { default: false, description: None, emoji: None, - label: "label".to_owned(), - value: "value".to_owned(), + label: "label".to_string(), + value: "value".to_string(), }; assert_eq!(select_menu, expected); @@ -220,16 +220,16 @@ mod tests { #[test] fn test_description() { - let select_menu = SelectMenuOptionBuilder::new("value".to_owned(), "label".to_owned()) - .description("description".to_owned()) + let select_menu = SelectMenuOptionBuilder::new("value".to_string(), "label".to_owned()) + .description("description".to_string()) .build(); let expected = SelectMenuOption { default: false, - description: Some("description".to_owned()), + description: Some("description".to_string()), emoji: None, - label: "label".to_owned(), - value: "value".to_owned(), + label: "label".to_string(), + value: "value".to_string(), }; assert_eq!(select_menu, expected); @@ -237,7 +237,7 @@ mod tests { #[test] fn test_default() { - let select_menu = SelectMenuOptionBuilder::new("value".to_owned(), "label".to_owned()) + let select_menu = SelectMenuOptionBuilder::new("value".to_string(), "label".to_owned()) .default(true) .build(); @@ -245,8 +245,8 @@ mod tests { default: true, description: None, emoji: None, - label: "label".to_owned(), - value: "value".to_owned(), + label: "label".to_string(), + value: "value".to_string(), }; assert_eq!(select_menu, expected); @@ -254,9 +254,9 @@ mod tests { #[test] fn test_emoji() { - let select_menu = SelectMenuOptionBuilder::new("value".to_owned(), "label".to_owned()) + let select_menu = SelectMenuOptionBuilder::new("value".to_string(), "label".to_owned()) .emoji(ReactionType::Unicode { - name: "\u{1f9ea}".to_owned(), + name: "\u{1f9ea}".to_string(), }) .build(); @@ -264,10 +264,10 @@ mod tests { default: false, description: None, emoji: Some(ReactionType::Unicode { - name: "\u{1f9ea}".to_owned(), + name: "\u{1f9ea}".to_string(), }), - label: "label".to_owned(), - value: "value".to_owned(), + label: "label".to_string(), + value: "value".to_string(), }; assert_eq!(select_menu, expected); @@ -276,17 +276,17 @@ mod tests { #[test] fn test_builder_try_from() { let select_menu = SelectMenuOption::try_from( - SelectMenuOptionBuilder::new("value".to_owned(), "label".to_owned()) - .description("testing".to_owned()), + SelectMenuOptionBuilder::new("value".to_string(), "label".to_owned()) + .description("testing".to_string()), ) .unwrap(); let expected = SelectMenuOption { default: false, - description: Some("testing".to_owned()), + description: Some("testing".to_string()), emoji: None, - label: "label".to_owned(), - value: "value".to_owned(), + label: "label".to_string(), + value: "value".to_string(), }; assert_eq!(select_menu, expected); diff --git a/util/src/builder/component/text_input.rs b/util/src/builder/component/text_input.rs index ffcac3ccd95..89d0a8e683e 100644 --- a/util/src/builder/component/text_input.rs +++ b/util/src/builder/component/text_input.rs @@ -6,7 +6,7 @@ //! use twilight_util::builder::component::TextInputBuilder; //! # fn main() -> Result<(), Box> { //! let component = Component::TextInput( -//! TextInputBuilder::paragraph("input-1".to_owned(), "Input".to_owned()) +//! TextInputBuilder::paragraph("input-1".to_string(), "Input".to_owned()) //! .min_length(20) //! .required(true) //! .validate()?.build() @@ -27,7 +27,7 @@ use twilight_validate::component::{text_input as validate_text_input, ComponentV /// use twilight_util::builder::component::TextInputBuilder; /// # fn main() -> Result<(), Box> { /// let component = Component::TextInput( -/// TextInputBuilder::paragraph("input-1".to_owned(), "Input".to_owned()) +/// TextInputBuilder::paragraph("input-1".to_string(), "Input".to_owned()) /// .min_length(20) /// .required(true) /// .validate()?.build() @@ -220,7 +220,7 @@ mod tests { value: None, }; - let actual = TextInputBuilder::short("input".to_owned(), "label".to_owned()).build(); + let actual = TextInputBuilder::short("input".to_string(), "label".to_owned()).build(); assert_eq!(actual, expected); } @@ -238,7 +238,7 @@ mod tests { value: None, }; - let actual = TextInputBuilder::paragraph("input".to_owned(), "label".to_owned()).build(); + let actual = TextInputBuilder::paragraph("input".to_string(), "label".to_owned()).build(); assert_eq!(actual, expected); } @@ -256,7 +256,7 @@ mod tests { value: None, }; - let actual = TextInputBuilder::short("input".to_owned(), "label".to_owned()) + let actual = TextInputBuilder::short("input".to_string(), "label".to_owned()) .max_length(100) .build(); @@ -276,7 +276,7 @@ mod tests { value: None, }; - let actual = TextInputBuilder::short("input".to_owned(), "label".to_owned()) + let actual = TextInputBuilder::short("input".to_string(), "label".to_owned()) .min_length(10) .build(); @@ -296,7 +296,7 @@ mod tests { value: None, }; - let actual = TextInputBuilder::short("input".to_owned(), "label".to_owned()) + let actual = TextInputBuilder::short("input".to_string(), "label".to_owned()) .placeholder("Enter some text".into()) .build(); @@ -316,7 +316,7 @@ mod tests { value: None, }; - let actual = TextInputBuilder::short("input".to_owned(), "label".to_owned()) + let actual = TextInputBuilder::short("input".to_string(), "label".to_owned()) .required(true) .build(); From 67631cdb2b98fbc010e3e208c94408f3ee87ac87 Mon Sep 17 00:00:00 2001 From: ITOH Date: Sun, 29 May 2022 16:26:11 +0200 Subject: [PATCH 03/11] use select menu options function --- validate/src/component.rs | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/validate/src/component.rs b/validate/src/component.rs index 2e4ff8ab8e8..b085154b39e 100644 --- a/validate/src/component.rs +++ b/validate/src/component.rs @@ -674,12 +674,7 @@ pub fn select_menu(select_menu: &SelectMenu) -> Result<(), ComponentValidationEr } for option in &select_menu.options { - self::component_select_option_label(&option.label)?; - self::component_select_option_value(&option.value)?; - - if let Some(description) = option.description.as_ref() { - self::component_option_description(description)?; - } + self::select_menu_option(option)?; } Ok(()) @@ -701,15 +696,13 @@ pub fn select_menu(select_menu: &SelectMenu) -> Result<(), ComponentValidationEr /// [`SelectOptionDescriptionLength`]: ComponentValidationErrorType::SelectOptionDescriptionLength /// [`SelectOptionLabelLength`]: ComponentValidationErrorType::SelectOptionLabelLength /// [`SelectOptionValueLength`]: ComponentValidationErrorType::SelectOptionValueLength -pub fn select_menu_option( - select_menu_option: &SelectMenuOption, -) -> Result<(), ComponentValidationError> { - self::component_select_option_label(&select_menu_option.label)?; - self::component_select_option_value(&select_menu_option.value)?; +pub fn select_menu_option(select_menu_option: &SelectMenuOption) -> Result<(), ComponentValidationError> { + self::component_select_option_label(&select_menu_option.label)?; + self::component_select_option_value(&select_menu_option.value)?; - if let Some(description) = select_menu_option.description.as_ref() { - self::component_option_description(description)?; - } + if let Some(description) = select_menu_option.description.as_ref() { + self::component_option_description(description)?; + } Ok(()) } From 30b5a6a3ce1f0e4aea87c25f20a3ff2f48965aa0 Mon Sep 17 00:00:00 2001 From: ITOH Date: Sun, 29 May 2022 16:42:23 +0200 Subject: [PATCH 04/11] m --- util/src/builder/component/action_row.rs | 28 ++++---- util/src/builder/component/button.rs | 28 ++++---- util/src/builder/component/components.rs | 64 +++++++++---------- util/src/builder/component/select_menu.rs | 28 ++++---- .../builder/component/select_menu_option.rs | 28 ++++---- util/src/builder/component/text_input.rs | 28 ++++---- 6 files changed, 102 insertions(+), 102 deletions(-) diff --git a/util/src/builder/component/action_row.rs b/util/src/builder/component/action_row.rs index 67c2976ba6f..4c8333ab880 100644 --- a/util/src/builder/component/action_row.rs +++ b/util/src/builder/component/action_row.rs @@ -59,6 +59,20 @@ impl ActionRowBuilder { self.0 } + /// Ensure the action row is valid. + /// + /// # Errors + /// + /// Refer to the documentation of [`twilight_validate::component::action_row`] for + /// possible errors. + pub fn validate(self) -> Result { + if let Err(source) = validate_action_row(&self.0) { + return Err(source); + } + + Ok(self) + } + /// Add a component to this action row. /// /// # Examples @@ -121,20 +135,6 @@ impl ActionRowBuilder { self } - - /// Ensure the action row is valid. - /// - /// # Errors - /// - /// Refer to the documentation of [`twilight_validate::component::action_row`] for - /// possible errors. - pub fn validate(self) -> Result { - if let Err(source) = validate_action_row(&self.0) { - return Err(source); - } - - Ok(self) - } } impl TryFrom for ActionRow { diff --git a/util/src/builder/component/button.rs b/util/src/builder/component/button.rs index 2b63fc7c945..8467c9ce681 100644 --- a/util/src/builder/component/button.rs +++ b/util/src/builder/component/button.rs @@ -107,6 +107,20 @@ impl ButtonBuilder { self.0 } + /// Ensure the button is valid. + /// + /// # Errors + /// + /// Refer to the documentation of [`twilight_validate::component::button`] for + /// possible errors. + pub fn validate(self) -> Result { + if let Err(source) = validate_button(&self.0) { + return Err(source); + } + + Ok(self) + } + /// Set whether the button is disabled or not. /// /// # Examples @@ -169,20 +183,6 @@ impl ButtonBuilder { self } - - /// Consume the builder, ensure that the button is valid and if so it returns a [`Button`]. - /// - /// # Errors - /// - /// Refer to the documentation of [`twilight_validate::component::button`] for - /// possible errors. - pub fn validate(self) -> Result { - if let Err(source) = validate_button(&self.0) { - return Err(source); - } - - Ok(self) - } } impl TryFrom for Button { diff --git a/util/src/builder/component/components.rs b/util/src/builder/component/components.rs index 812f7703f6b..1ade72082e1 100644 --- a/util/src/builder/component/components.rs +++ b/util/src/builder/component/components.rs @@ -84,23 +84,6 @@ impl ComponentsBuilder { self } - fn action_row_components(mut self, components: Vec) -> Self { - if self.is_full() { - return self; - } - - match self.0.iter_mut().last() { - Some(action_row) if action_row.components.is_empty() => { - action_row.components = components; - } - _ => { - self.0.push(ActionRow { components }); - } - } - - self - } - /// Consume the builder, returning a Vec<[`Component`]>. #[allow(clippy::missing_const_for_fn)] #[must_use = "builders have no effect if unused"] @@ -111,6 +94,22 @@ impl ComponentsBuilder { .collect() } + /// Ensure the Components are valid. + /// + /// # Errors + /// + /// Refer to the documentation of [`twilight_validate::component::validate_action_row`] for + /// possible errors. + pub fn validate(self) -> Result { + for action_row in self.0.iter() { + if let Err(source) = validate_action_row(action_row) { + return Err(source); + } + } + + Ok(self) + } + /// Add a button to this builder. /// /// If there is an action row available the button will be added to it @@ -158,10 +157,6 @@ impl ComponentsBuilder { } } - fn is_full(&self) -> bool { - self.0.len() == COMPONENT_COUNT - } - /// Add a select menu to this builder. /// /// If there is an empty action row available the select menu will be added to it @@ -220,20 +215,25 @@ impl ComponentsBuilder { self.action_row_components(Vec::from([Component::TextInput(text_input)])) } - /// Consume the builder, ensure that the action rows and their components are valid. - /// - /// # Errors - /// - /// Refer to the documentation of [`twilight_validate::component::validate_action_row`] for - /// possible errors. - pub fn validate(self) -> Result { - for action_row in self.0.iter() { - if let Err(source) = validate_action_row(action_row) { - return Err(source); + fn action_row_components(mut self, components: Vec) -> Self { + if self.is_full() { + return self; + } + + match self.0.iter_mut().last() { + Some(action_row) if action_row.components.is_empty() => { + action_row.components = components; + } + _ => { + self.0.push(ActionRow { components }); } } - Ok(self) + self + } + + fn is_full(&self) -> bool { + self.0.len() == COMPONENT_COUNT } } diff --git a/util/src/builder/component/select_menu.rs b/util/src/builder/component/select_menu.rs index 7f2ff800578..9c3ceca822b 100644 --- a/util/src/builder/component/select_menu.rs +++ b/util/src/builder/component/select_menu.rs @@ -88,6 +88,20 @@ impl SelectMenuBuilder { self.0 } + /// Ensure the select menu is valid. + /// + /// # Errors + /// + /// Refer to the documentation of [`twilight_validate::component::select_menu`] for + /// possible errors. + pub fn validate(self) -> Result { + if let Err(source) = validate_select_menu(&self.0) { + return Err(source); + } + + Ok(self) + } + /// Set the minimum values for this select menu. /// /// # Examples @@ -222,20 +236,6 @@ impl SelectMenuBuilder { self } - - /// Consume the builder, ensure that the option is valid and if so it returns a [`SelectMenu`]. - /// - /// # Errors - /// - /// Refer to the documentation of [`twilight_validate::component::select_menu`] for - /// possible errors. - pub fn validate(self) -> Result { - if let Err(source) = validate_select_menu(&self.0) { - return Err(source); - } - - Ok(self) - } } impl TryFrom for SelectMenu { diff --git a/util/src/builder/component/select_menu_option.rs b/util/src/builder/component/select_menu_option.rs index c6e257c95c3..7cf1179fee8 100644 --- a/util/src/builder/component/select_menu_option.rs +++ b/util/src/builder/component/select_menu_option.rs @@ -94,6 +94,20 @@ impl SelectMenuOptionBuilder { self.0 } + /// Ensure the select menu option is valid. + /// + /// # Errors + /// + /// Refer to the documentation of [`twilight_validate::component::select_menu_option`] for + /// possible errors. + pub fn validate(self) -> Result { + if let Err(source) = validate_select_menu_option(&self.0) { + return Err(source); + } + + Ok(self) + } + /// Set whether this option is selected by default. /// /// # Examples @@ -155,20 +169,6 @@ impl SelectMenuOptionBuilder { self } - - /// Consume the builder, ensure that the option is valid and if so it returns a [`SelectMenuOption`]. - /// - /// # Errors - /// - /// Refer to the documentation of [`twilight_validate::component::select_menu_option`] for - /// possible errors. - pub fn validate(self) -> Result { - if let Err(source) = validate_select_menu_option(&self.0) { - return Err(source); - } - - Ok(self) - } } impl TryFrom for SelectMenuOption { diff --git a/util/src/builder/component/text_input.rs b/util/src/builder/component/text_input.rs index 89d0a8e683e..00e3dadf639 100644 --- a/util/src/builder/component/text_input.rs +++ b/util/src/builder/component/text_input.rs @@ -75,6 +75,20 @@ impl TextInputBuilder { self.0 } + /// Ensure the text input is valid. + /// + /// # Errors + /// + /// Refer to the documentation of [`twilight_validate::component::select_menu_option`] for + /// possible errors. + pub fn validate(self) -> Result { + if let Err(source) = validate_text_input(&self.0) { + return Err(source); + } + + Ok(self) + } + /// Set the maximum amount of characters allowed to be entered in this text input. /// /// # Examples @@ -169,20 +183,6 @@ impl TextInputBuilder { self } - - /// Consume the builder, ensure that the option is valid and if so it returns a [`SelectMenuOption`]. - /// - /// # Errors - /// - /// Refer to the documentation of [`twilight_validate::component::select_menu_option`] for - /// possible errors. - pub fn validate(self) -> Result { - if let Err(source) = validate_text_input(&self.0) { - return Err(source); - } - - Ok(self) - } } impl TryFrom for TextInput { From 537b00c5d8fe3ccce02bd8574accab74362aad5e Mon Sep 17 00:00:00 2001 From: ITOH Date: Sun, 29 May 2022 16:51:30 +0200 Subject: [PATCH 05/11] clippy --- util/src/builder/component/button.rs | 1 + util/src/builder/component/components.rs | 4 ++-- util/src/builder/component/text_input.rs | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/util/src/builder/component/button.rs b/util/src/builder/component/button.rs index 8467c9ce681..cb1bbc3b7fe 100644 --- a/util/src/builder/component/button.rs +++ b/util/src/builder/component/button.rs @@ -103,6 +103,7 @@ impl ButtonBuilder { /// Consume the builder, returning a [`Button`]. #[must_use = "builders have no effect if unused"] + #[allow(clippy::missing_const_for_fn)] pub fn build(self) -> Button { self.0 } diff --git a/util/src/builder/component/components.rs b/util/src/builder/component/components.rs index 1ade72082e1..41d2f6e3032 100644 --- a/util/src/builder/component/components.rs +++ b/util/src/builder/component/components.rs @@ -90,7 +90,7 @@ impl ComponentsBuilder { pub fn build(self) -> Vec { self.0 .into_iter() - .map(|action_row| Component::ActionRow(action_row)) + .map(Component::ActionRow) .collect() } @@ -101,7 +101,7 @@ impl ComponentsBuilder { /// Refer to the documentation of [`twilight_validate::component::validate_action_row`] for /// possible errors. pub fn validate(self) -> Result { - for action_row in self.0.iter() { + for action_row in &self.0 { if let Err(source) = validate_action_row(action_row) { return Err(source); } diff --git a/util/src/builder/component/text_input.rs b/util/src/builder/component/text_input.rs index 00e3dadf639..876f921beab 100644 --- a/util/src/builder/component/text_input.rs +++ b/util/src/builder/component/text_input.rs @@ -140,6 +140,7 @@ impl TextInputBuilder { /// .validate()?.build(); /// # Ok(()) } /// ``` + #[allow(clippy::missing_const_for_fn)] pub fn placeholder(mut self, placeholder: String) -> Self { self.0.placeholder = Some(placeholder); @@ -178,6 +179,7 @@ impl TextInputBuilder { /// .validate()?.build(); /// # Ok(()) } /// ``` + #[allow(clippy::missing_const_for_fn)] pub fn value(mut self, value: String) -> Self { self.0.value = Some(value); From 18f9f4b8f8f916f77fbc231ff9e061e10fe9843a Mon Sep 17 00:00:00 2001 From: ITOH Date: Sun, 29 May 2022 16:52:08 +0200 Subject: [PATCH 06/11] fmt --- util/src/builder/component/components.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/util/src/builder/component/components.rs b/util/src/builder/component/components.rs index 41d2f6e3032..83c904a60f3 100644 --- a/util/src/builder/component/components.rs +++ b/util/src/builder/component/components.rs @@ -88,10 +88,7 @@ impl ComponentsBuilder { #[allow(clippy::missing_const_for_fn)] #[must_use = "builders have no effect if unused"] pub fn build(self) -> Vec { - self.0 - .into_iter() - .map(Component::ActionRow) - .collect() + self.0.into_iter().map(Component::ActionRow).collect() } /// Ensure the Components are valid. From fc3a105a8ccb9004a0fa2a2e10e9658701186363 Mon Sep 17 00:00:00 2001 From: ITOH Date: Sun, 29 May 2022 16:54:40 +0200 Subject: [PATCH 07/11] more fmt --- validate/src/component.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/validate/src/component.rs b/validate/src/component.rs index b085154b39e..a3cebee3ceb 100644 --- a/validate/src/component.rs +++ b/validate/src/component.rs @@ -696,13 +696,15 @@ pub fn select_menu(select_menu: &SelectMenu) -> Result<(), ComponentValidationEr /// [`SelectOptionDescriptionLength`]: ComponentValidationErrorType::SelectOptionDescriptionLength /// [`SelectOptionLabelLength`]: ComponentValidationErrorType::SelectOptionLabelLength /// [`SelectOptionValueLength`]: ComponentValidationErrorType::SelectOptionValueLength -pub fn select_menu_option(select_menu_option: &SelectMenuOption) -> Result<(), ComponentValidationError> { - self::component_select_option_label(&select_menu_option.label)?; - self::component_select_option_value(&select_menu_option.value)?; +pub fn select_menu_option( + select_menu_option: &SelectMenuOption, +) -> Result<(), ComponentValidationError> { + self::component_select_option_label(&select_menu_option.label)?; + self::component_select_option_value(&select_menu_option.value)?; - if let Some(description) = select_menu_option.description.as_ref() { - self::component_option_description(description)?; - } + if let Some(description) = select_menu_option.description.as_ref() { + self::component_option_description(description)?; + } Ok(()) } From bba61176472039dfd39c0df8bc11e72a69b303f6 Mon Sep 17 00:00:00 2001 From: ITOH Date: Sun, 29 May 2022 17:08:24 +0200 Subject: [PATCH 08/11] more clippy --- util/src/builder/component/components.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/src/builder/component/components.rs b/util/src/builder/component/components.rs index 83c904a60f3..03589398b06 100644 --- a/util/src/builder/component/components.rs +++ b/util/src/builder/component/components.rs @@ -259,7 +259,7 @@ mod test { assert_impl_all!(ComponentsBuilder: Clone, Debug, Eq, PartialEq, Send, Sync); assert_impl_all!(Vec: TryFrom); - fn action_row(components: Vec) -> ActionRow { + const fn action_row(components: Vec) -> ActionRow { ActionRow { components } } From 038c97e161f0b469626f90fa335df448e4b56d59 Mon Sep 17 00:00:00 2001 From: ITOH Date: Sun, 29 May 2022 18:02:48 +0200 Subject: [PATCH 09/11] remove invalid must uses --- util/src/builder/component/action_row.rs | 2 -- util/src/builder/component/select_menu.rs | 6 ------ 2 files changed, 8 deletions(-) diff --git a/util/src/builder/component/action_row.rs b/util/src/builder/component/action_row.rs index 4c8333ab880..575fec00f1b 100644 --- a/util/src/builder/component/action_row.rs +++ b/util/src/builder/component/action_row.rs @@ -94,7 +94,6 @@ impl ActionRowBuilder { /// # Ok(()) } /// ``` #[allow(clippy::missing_const_for_fn)] - #[must_use = "builders have no effect if unused"] pub fn add_component(mut self, component: Component) -> Self { self.0.components.push(component); @@ -129,7 +128,6 @@ impl ActionRowBuilder { /// # Ok(()) } /// ``` #[allow(clippy::missing_const_for_fn)] - #[must_use = "builders have no effect if unused"] pub fn add_components(mut self, components: &mut Vec) -> Self { self.0.components.append(components); diff --git a/util/src/builder/component/select_menu.rs b/util/src/builder/component/select_menu.rs index 9c3ceca822b..1bd010de4e0 100644 --- a/util/src/builder/component/select_menu.rs +++ b/util/src/builder/component/select_menu.rs @@ -116,7 +116,6 @@ impl SelectMenuBuilder { /// # Ok(()) } /// ``` #[allow(clippy::missing_const_for_fn)] - #[must_use = "builders have no effect if unused"] pub fn min_values(mut self, min_values: u8) -> Self { self.0.min_values = Some(min_values); @@ -136,7 +135,6 @@ impl SelectMenuBuilder { /// .validate()?.build(); /// # Ok(()) } /// ``` - #[must_use = "builders have no effect if unused"] pub const fn max_values(mut self, max_values: u8) -> Self { self.0.max_values = Some(max_values); @@ -157,7 +155,6 @@ impl SelectMenuBuilder { /// # Ok(()) } /// ``` #[allow(clippy::missing_const_for_fn)] - #[must_use = "builders have no effect if unused"] pub fn disable(mut self, disabled: bool) -> Self { self.0.disabled = disabled; @@ -178,7 +175,6 @@ impl SelectMenuBuilder { /// # Ok(()) } /// ``` #[allow(clippy::missing_const_for_fn)] - #[must_use = "builders have no effect if unused"] pub fn placeholder(mut self, placeholder: String) -> Self { self.0.placeholder = Some(placeholder); @@ -202,7 +198,6 @@ impl SelectMenuBuilder { /// # Ok(()) } /// ``` #[allow(clippy::missing_const_for_fn)] - #[must_use = "builders have no effect if unused"] pub fn add_option(mut self, option: SelectMenuOption) -> Self { self.0.options.push(option); @@ -230,7 +225,6 @@ impl SelectMenuBuilder { /// # Ok(()) } /// ``` #[allow(clippy::missing_const_for_fn)] - #[must_use = "builders have no effect if unused"] pub fn add_options(mut self, options: &mut Vec) -> Self { self.0.options.append(options); From 555944e524779a5262597aa43d0a5697023fe0d8 Mon Sep 17 00:00:00 2001 From: ITOH Date: Tue, 31 May 2022 22:26:04 +0200 Subject: [PATCH 10/11] more --- util/src/builder/component/action_row.rs | 251 ++++++++++++++++++++-- util/src/builder/component/button.rs | 173 ++++++++++----- util/src/builder/component/components.rs | 15 +- util/src/builder/component/select_menu.rs | 126 ++++++++--- util/src/builder/component/text_input.rs | 82 ++++++- 5 files changed, 538 insertions(+), 109 deletions(-) diff --git a/util/src/builder/component/action_row.rs b/util/src/builder/component/action_row.rs index 575fec00f1b..381b4797a32 100644 --- a/util/src/builder/component/action_row.rs +++ b/util/src/builder/component/action_row.rs @@ -7,7 +7,7 @@ //! //! # fn main() -> Result<(), Box> { //! let action_row = ActionRowBuilder::new() -//! .add_component( +//! .component( //! Component::Button( //! ButtonBuilder::primary("button-1".to_string()) //! .label("Button".to_string()) @@ -18,7 +18,10 @@ //! # Ok(()) } //! ``` -use twilight_model::application::component::{action_row::ActionRow, Component}; +use twilight_model::application::component::{ + action_row::ActionRow, button::Button, select_menu::SelectMenu, text_input::TextInput, + Component, +}; use twilight_validate::component::{action_row as validate_action_row, ComponentValidationError}; /// Create an [`ActionRow`] with a builder. @@ -30,7 +33,7 @@ use twilight_validate::component::{action_row as validate_action_row, ComponentV /// /// # fn main() -> Result<(), Box> { /// let action_row = ActionRowBuilder::new() -/// .add_component( +/// .component( /// Component::Button( /// ButtonBuilder::primary("button-1".to_string()) /// .label("Button".to_string()) @@ -45,14 +48,14 @@ use twilight_validate::component::{action_row as validate_action_row, ComponentV pub struct ActionRowBuilder(ActionRow); impl ActionRowBuilder { - /// Create a new builder to construct a [`SelectMenu`]. + /// Create a new builder to construct an [`ActionRow`]. pub const fn new() -> Self { Self(ActionRow { components: Vec::new(), }) } - /// Consume the builder, returning a [`SelectMenu`]. + /// Consume the builder, returning an [`ActionRow`]. #[allow(clippy::missing_const_for_fn)] #[must_use = "builders have no effect if unused"] pub fn build(self) -> ActionRow { @@ -73,6 +76,13 @@ impl ActionRowBuilder { Ok(self) } + /// Consume the builder, returning an action row wrapped in + /// [`Component::ActionRow`]. + #[must_use = "builders have no effect if unused"] + pub fn into_component(self) -> Component { + Component::ActionRow(self.build()) + } + /// Add a component to this action row. /// /// # Examples @@ -83,7 +93,7 @@ impl ActionRowBuilder { /// /// # fn main() -> Result<(), Box> { /// let action_row = ActionRowBuilder::new() - /// .add_component( + /// .component( /// Component::Button( /// ButtonBuilder::primary("button-1".to_string()) /// .label("Button".to_string()) @@ -94,7 +104,7 @@ impl ActionRowBuilder { /// # Ok(()) } /// ``` #[allow(clippy::missing_const_for_fn)] - pub fn add_component(mut self, component: Component) -> Self { + pub fn component(mut self, component: Component) -> Self { self.0.components.push(component); self @@ -110,7 +120,7 @@ impl ActionRowBuilder { /// /// # fn main() -> Result<(), Box> { /// let action_row = ActionRowBuilder::new() - /// .add_components( + /// .components( /// &mut vec![ /// Component::Button( /// ButtonBuilder::primary("button-1".to_string()) @@ -128,49 +138,144 @@ impl ActionRowBuilder { /// # Ok(()) } /// ``` #[allow(clippy::missing_const_for_fn)] - pub fn add_components(mut self, components: &mut Vec) -> Self { + pub fn components(mut self, components: &mut Vec) -> Self { self.0.components.append(components); self } + + /// Add a button to this action row. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::{ActionRowBuilder, ButtonBuilder}; + /// + /// # fn main() -> Result<(), Box> { + /// let action_row = ActionRowBuilder::new() + /// .button( + /// ButtonBuilder::primary("button-1".to_string()) + /// .label("Button".to_string()) + /// .build() + /// ) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + pub fn button(self, button: Button) -> Self { + self.component(Component::Button(button)) + } + + /// Add a select menu to this action row. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::{ActionRowBuilder, SelectMenuBuilder, SelectMenuOptionBuilder}; + /// + /// # fn main() -> Result<(), Box> { + /// let action_row = ActionRowBuilder::new() + /// .select_menu( + /// SelectMenuBuilder::new("characters".to_string()) + /// .add_options( + /// &mut vec![ + /// SelectMenuOptionBuilder::new("twilight-sparkle".to_string(), "Twilight Sparkle".to_string()) + /// .default(true) + /// .build(), + /// SelectMenuOptionBuilder::new("rarity".to_string(), "Rarity".to_string()) + /// .build(), + /// ] + /// ).build() + /// ) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + pub fn select_menu(self, select_menu: SelectMenu) -> Self { + self.component(Component::SelectMenu(select_menu)) + } + + /// Add a text input to this action row. + /// + /// # Examples + /// + /// ```rust + /// use twilight_util::builder::component::{ActionRowBuilder, TextInputBuilder}; + /// + /// # fn main() -> Result<(), Box> { + /// let action_row = ActionRowBuilder::new() + /// .text_input( + /// TextInputBuilder::short("input-1".to_string(), "Input One".to_owned()) + /// .build() + /// ) + /// .validate()?.build(); + /// # Ok(()) } + /// ``` + pub fn text_input(self, text_input: TextInput) -> Self { + self.component(Component::TextInput(text_input)) + } } impl TryFrom for ActionRow { type Error = ComponentValidationError; - /// Convert a select menu builder into a select menu, validating its contents. + /// Convert an action row builder into an action row, validating its contents. /// - /// This is equivalent to calling [`SelectMenuBuilder::validate`], then - /// [`SelectMenuBuilder::build`]. + /// This is equivalent to calling [`ActionRowBuilder::validate`], then + /// [`ActionRowBuilder::build`]. fn try_from(builder: ActionRowBuilder) -> Result { Ok(builder.validate()?.build()) } } +impl TryFrom for Component { + type Error = ComponentValidationError; + + /// Convert an action row builder into a component, validating its contents. + /// + /// This is equivalent to calling [`ActionRowBuilder::validate`], then + /// [`ActionRowBuilder::into_component`]. + fn try_from(builder: ActionRowBuilder) -> Result { + Ok(builder.validate()?.into_component()) + } +} + #[cfg(test)] mod test { use super::ActionRowBuilder; use static_assertions::assert_impl_all; use std::{convert::TryFrom, fmt::Debug}; use twilight_model::application::component::{ - button::ButtonStyle, ActionRow, Button, Component, + button::ButtonStyle, text_input::TextInputStyle, ActionRow, Button, Component, SelectMenu, + TextInput, }; assert_impl_all!(ActionRowBuilder: Clone, Debug, Eq, PartialEq, Send, Sync); assert_impl_all!(ActionRow: TryFrom); + assert_impl_all!(Component: TryFrom); #[test] - fn test_action_row_builder() { + fn builder() { let expected = ActionRow { components: Vec::new(), }; + let actual = ActionRowBuilder::new().build(); assert_eq!(actual, expected); } #[test] - fn test_action_row_builder_add_component() { + fn into_component() { + let expected = Component::ActionRow(ActionRow { + components: Vec::new(), + }); + + let actual = ActionRowBuilder::new().into_component(); + + assert_eq!(actual, expected); + } + + #[test] + fn component() { let expected = ActionRow { components: Vec::from([Component::Button(Button { custom_id: Some("button".into()), @@ -181,8 +286,9 @@ mod test { url: None, })]), }; + let actual = ActionRowBuilder::new() - .add_component(Component::Button(Button { + .component(Component::Button(Button { custom_id: Some("button".into()), disabled: false, emoji: None, @@ -196,7 +302,7 @@ mod test { } #[test] - fn test_action_row_builder_add_components() { + fn components() { let expected = ActionRow { components: Vec::from([ Component::Button(Button { @@ -217,8 +323,9 @@ mod test { }), ]), }; + let actual = ActionRowBuilder::new() - .add_components(&mut Vec::from([ + .components(&mut Vec::from([ Component::Button(Button { custom_id: Some("button".into()), disabled: false, @@ -240,4 +347,112 @@ mod test { assert_eq!(actual, expected); } + + #[test] + fn button() { + let expected = ActionRow { + components: Vec::from([Component::Button(Button { + custom_id: Some("button".into()), + disabled: false, + emoji: None, + label: Some("label".into()), + style: ButtonStyle::Primary, + url: None, + })]), + }; + + let actual = ActionRowBuilder::new() + .button(Button { + custom_id: Some("button".into()), + disabled: false, + emoji: None, + label: Some("label".into()), + style: ButtonStyle::Primary, + url: None, + }) + .build(); + + assert_eq!(actual, expected); + } + + #[test] + fn select_menu() { + let expected = ActionRow { + components: Vec::from([Component::SelectMenu(SelectMenu { + custom_id: "select_menu".into(), + disabled: false, + max_values: None, + min_values: None, + options: Vec::new(), + placeholder: None, + })]), + }; + + let actual = ActionRowBuilder::new() + .select_menu(SelectMenu { + custom_id: "select_menu".into(), + disabled: false, + max_values: None, + min_values: None, + options: Vec::new(), + placeholder: None, + }) + .build(); + + assert_eq!(actual, expected); + } + + #[test] + fn input_text() { + let expected = ActionRow { + components: Vec::from([Component::TextInput(TextInput { + custom_id: "input_text".into(), + label: "label".into(), + max_length: None, + min_length: None, + placeholder: None, + style: TextInputStyle::Short, + required: None, + value: None, + })]), + }; + + let actual = ActionRowBuilder::new() + .text_input(TextInput { + custom_id: "input_text".into(), + label: "label".into(), + max_length: None, + min_length: None, + placeholder: None, + style: TextInputStyle::Short, + required: None, + value: None, + }) + .build(); + + assert_eq!(actual, expected); + } + + #[test] + fn action_row_try_from() { + let expected = ActionRow { + components: Vec::new(), + }; + + let actual = ActionRow::try_from(ActionRowBuilder::new()).unwrap(); + + assert_eq!(actual, expected); + } + + #[test] + + fn component_try_from() { + let expected = Component::ActionRow(ActionRow { + components: Vec::new(), + }); + + let actual = Component::try_from(ActionRowBuilder::new()).unwrap(); + + assert_eq!(actual, expected); + } } diff --git a/util/src/builder/component/button.rs b/util/src/builder/component/button.rs index cb1bbc3b7fe..fe1191a2a8f 100644 --- a/util/src/builder/component/button.rs +++ b/util/src/builder/component/button.rs @@ -17,7 +17,7 @@ use std::convert::TryFrom; use twilight_model::{ - application::component::{button::ButtonStyle, Button}, + application::component::{button::ButtonStyle, Button, Component}, channel::ReactionType, }; use twilight_validate::component::{button as validate_button, ComponentValidationError}; @@ -122,6 +122,13 @@ impl ButtonBuilder { Ok(self) } + /// Consume the builder, returning a button wrapped in + /// [`Component::Button`]. + #[must_use = "builders have no effect if unused"] + pub fn into_component(self) -> Component { + Component::Button(self.build()) + } + /// Set whether the button is disabled or not. /// /// # Examples @@ -198,25 +205,34 @@ impl TryFrom for Button { } } +impl TryFrom for Component { + type Error = ComponentValidationError; + + /// Convert a button builder into a component, validating its contents. + /// + /// This is equivalent to calling [`ButtonBuilder::validate`], then + /// [`ButtonBuilder::into_component`]. + fn try_from(builder: ButtonBuilder) -> Result { + Ok(builder.validate()?.into_component()) + } +} + #[cfg(test)] mod tests { use super::ButtonBuilder; use static_assertions::assert_impl_all; use std::{convert::TryFrom, fmt::Debug}; use twilight_model::{ - application::component::{button::ButtonStyle, Button}, + application::component::{button::ButtonStyle, Button, Component}, channel::ReactionType, }; assert_impl_all!(ButtonBuilder: Clone, Debug, Eq, PartialEq, Send, Sync); assert_impl_all!(Button: TryFrom); + assert_impl_all!(Component: TryFrom); #[test] - fn test_builder_primary() { - let button = ButtonBuilder::primary("primary-button".to_string()) - .label("primary button".to_string()) - .build(); - + fn primary() { let expected = Button { style: ButtonStyle::Primary, emoji: None, @@ -226,15 +242,15 @@ mod tests { disabled: false, }; - assert_eq!(button, expected); + let actual = ButtonBuilder::primary("primary-button".to_string()) + .label("primary button".to_string()) + .build(); + + assert_eq!(actual, expected); } #[test] - fn test_builder_secondary() { - let button = ButtonBuilder::secondary("secondary-button".to_string()) - .label("secondary button".to_string()) - .build(); - + fn secondary() { let expected = Button { style: ButtonStyle::Secondary, emoji: None, @@ -244,15 +260,15 @@ mod tests { disabled: false, }; - assert_eq!(button, expected); + let actual = ButtonBuilder::secondary("secondary-button".to_string()) + .label("secondary button".to_string()) + .build(); + + assert_eq!(actual, expected); } #[test] - fn test_builder_success() { - let button = ButtonBuilder::success("success-button".to_string()) - .label("success button".to_string()) - .build(); - + fn success() { let expected = Button { style: ButtonStyle::Success, emoji: None, @@ -262,15 +278,15 @@ mod tests { disabled: false, }; - assert_eq!(button, expected); + let actual = ButtonBuilder::success("success-button".to_string()) + .label("success button".to_string()) + .build(); + + assert_eq!(actual, expected); } #[test] - fn test_builder_danger() { - let button = ButtonBuilder::danger("danger-button".to_string()) - .label("danger button".to_string()) - .build(); - + fn danger() { let expected = Button { style: ButtonStyle::Danger, emoji: None, @@ -280,15 +296,15 @@ mod tests { disabled: false, }; - assert_eq!(button, expected); + let actual = ButtonBuilder::danger("danger-button".to_string()) + .label("danger button".to_string()) + .build(); + + assert_eq!(actual, expected); } #[test] - fn test_builder_link() { - let button = ButtonBuilder::link("https://twilight.rs".to_string()) - .label("link button".to_string()) - .build(); - + fn link() { let expected = Button { style: ButtonStyle::Link, emoji: None, @@ -298,16 +314,33 @@ mod tests { disabled: false, }; - assert_eq!(button, expected); + let actual = ButtonBuilder::link("https://twilight.rs".to_string()) + .label("link button".to_string()) + .build(); + + assert_eq!(actual, expected); } #[test] - fn test_builder_disabled_button() { - let button = ButtonBuilder::primary("disabled-button".to_string()) - .label("disabled button".to_string()) - .disable(true) - .build(); + fn into_component() { + let expected = Component::Button(Button { + style: ButtonStyle::Primary, + emoji: None, + label: Some("primary button".to_string()), + custom_id: Some("primary-button".to_string()), + url: None, + disabled: false, + }); + + let actual = ButtonBuilder::primary("primary-button".to_string()) + .label("primary button".to_string()) + .into_component(); + + assert_eq!(actual, expected); + } + #[test] + fn disabled_button() { let expected = Button { style: ButtonStyle::Primary, emoji: None, @@ -317,16 +350,16 @@ mod tests { disabled: true, }; - assert_eq!(button, expected); + let actual = ButtonBuilder::primary("disabled-button".to_string()) + .label("disabled button".to_string()) + .disable(true) + .build(); + + assert_eq!(actual, expected); } #[test] - fn test_builder_explicit_enabled_button() { - let button = ButtonBuilder::primary("enabled-button".to_string()) - .label("enabled button".to_string()) - .disable(false) - .build(); - + fn explicit_enabled_button() { let expected = Button { style: ButtonStyle::Primary, emoji: None, @@ -336,17 +369,16 @@ mod tests { disabled: false, }; - assert_eq!(button, expected); + let actual = ButtonBuilder::primary("enabled-button".to_string()) + .label("enabled button".to_string()) + .disable(false) + .build(); + + assert_eq!(actual, expected); } #[test] - fn test_builder_with_emoji() { - let button = ButtonBuilder::primary("emoji-button".to_string()) - .emoji(ReactionType::Unicode { - name: "\u{1f9ea}".to_string(), - }) - .build(); - + fn with_emoji() { let expected = Button { style: ButtonStyle::Primary, emoji: Some(ReactionType::Unicode { @@ -358,25 +390,50 @@ mod tests { disabled: false, }; - assert_eq!(button, expected); + let actual = ButtonBuilder::primary("emoji-button".to_string()) + .emoji(ReactionType::Unicode { + name: "\u{1f9ea}".to_string(), + }) + .build(); + + assert_eq!(actual, expected); } #[test] - fn test_builder_try_from() { - let button = Button::try_from( + fn button_try_from() { + let expected = Button { + style: ButtonStyle::Primary, + emoji: None, + label: Some("primary button".to_string()), + custom_id: Some("primary-button".to_string()), + url: None, + disabled: false, + }; + + let actual = Button::try_from( ButtonBuilder::primary("primary-button".to_string()).label("primary button".to_owned()), ) .unwrap(); - let expected = Button { + assert_eq!(actual, expected); + } + + #[test] + fn component_try_from() { + let expected = Component::Button(Button { style: ButtonStyle::Primary, emoji: None, label: Some("primary button".to_string()), custom_id: Some("primary-button".to_string()), url: None, disabled: false, - }; + }); + + let actual = Component::try_from( + ButtonBuilder::primary("primary-button".to_string()).label("primary button".to_owned()), + ) + .unwrap(); - assert_eq!(button, expected); + assert_eq!(actual, expected); } } diff --git a/util/src/builder/component/components.rs b/util/src/builder/component/components.rs index 03589398b06..54203e8f357 100644 --- a/util/src/builder/component/components.rs +++ b/util/src/builder/component/components.rs @@ -62,12 +62,10 @@ impl ComponentsBuilder { /// let components = ComponentsBuilder::new() /// .action_row( /// ActionRowBuilder::new() - /// .add_component( - /// Component::Button( + /// .button( /// ButtonBuilder::primary("button-1".to_string()) /// .label("Button".to_string()) /// .build() - /// ) /// ) /// .build() /// ) @@ -301,6 +299,7 @@ mod test { #[test] fn builder() { let expected: Vec = Vec::new(); + let actual = ComponentsBuilder::new().build(); assert_eq!(actual, expected); @@ -309,6 +308,7 @@ mod test { #[test] fn one_action_row() { let expected = Vec::from([Component::ActionRow(action_row(Vec::new()))]); + let actual = ComponentsBuilder::new() .action_row(action_row(Vec::new())) .build(); @@ -322,6 +322,7 @@ mod test { Component::ActionRow(action_row(Vec::new())), Component::ActionRow(action_row(Vec::new())), ]); + let actual = ComponentsBuilder::new() .action_row(action_row(Vec::new())) .action_row(action_row(Vec::new())) @@ -335,6 +336,7 @@ mod test { let expected = Vec::from([Component::ActionRow(ActionRow { components: Vec::from([Component::Button(button("button"))]), })]); + let actual = ComponentsBuilder::new().button(button("button")).build(); assert_eq!(actual, expected); @@ -348,6 +350,7 @@ mod test { Component::Button(button("button-2")), ]), })]); + let actual = ComponentsBuilder::new() .button(button("button-1")) .button(button("button-2")) @@ -361,6 +364,7 @@ mod test { let expected = Vec::from([Component::ActionRow(ActionRow { components: Vec::from([Component::Button(button("button"))]), })]); + let actual = ComponentsBuilder::new() .action_row(ActionRow { components: Vec::new(), @@ -387,6 +391,7 @@ mod test { components: Vec::from([Component::Button(button("button-6"))]), }), ]); + let actual = ComponentsBuilder::new() .button(button("button-1")) .button(button("button-2")) @@ -404,6 +409,7 @@ mod test { let expected = Vec::from([Component::ActionRow(action_row(Vec::from([ Component::SelectMenu(select_menu("select")), ])))]); + let actual = ComponentsBuilder::new() .select_menu(select_menu("select")) .build(); @@ -421,6 +427,7 @@ mod test { "select-2", ))]))), ]); + let actual = ComponentsBuilder::new() .select_menu(select_menu("select-1")) .select_menu(select_menu("select-2")) @@ -434,6 +441,7 @@ mod test { let expected = Vec::from([Component::ActionRow(action_row(Vec::from([ Component::TextInput(text_input("input")), ])))]); + let actual = ComponentsBuilder::new() .text_input(text_input("input")) .build(); @@ -451,6 +459,7 @@ mod test { "input-2", ))]))), ]); + let actual = ComponentsBuilder::new() .text_input(text_input("input-1")) .text_input(text_input("input-2")) diff --git a/util/src/builder/component/select_menu.rs b/util/src/builder/component/select_menu.rs index 1bd010de4e0..1e54536bff2 100644 --- a/util/src/builder/component/select_menu.rs +++ b/util/src/builder/component/select_menu.rs @@ -30,7 +30,9 @@ //! # Ok(()) } //! ``` -use twilight_model::application::component::{select_menu::SelectMenuOption, SelectMenu}; +use twilight_model::application::component::{ + select_menu::SelectMenuOption, Component, SelectMenu, +}; use twilight_validate::component::{select_menu as validate_select_menu, ComponentValidationError}; /// Create a [`SelectMenu`] with a builder. @@ -102,6 +104,13 @@ impl SelectMenuBuilder { Ok(self) } + /// Consume the builder, returning a select menu wrapped in + /// [`Component::SelectMenu`] + #[must_use = "builders have no effect if unused"] + pub fn into_component(self) -> Component { + Component::SelectMenu(self.0) + } + /// Set the minimum values for this select menu. /// /// # Examples @@ -244,20 +253,31 @@ impl TryFrom for SelectMenu { } } +impl TryFrom for Component { + type Error = ComponentValidationError; + + /// Convert a select menu builder into a component, validating its contents. + /// + /// This is equivalent to calling [`SelectMenuBuilder::validate`], then + /// [`SelectMenuBuilder::into_component`]. + fn try_from(builder: SelectMenuBuilder) -> Result { + Ok(builder.validate()?.into_component()) + } +} + #[cfg(test)] mod test { use super::SelectMenuBuilder; use static_assertions::assert_impl_all; use std::{convert::TryFrom, fmt::Debug}; - use twilight_model::application::component::SelectMenu; + use twilight_model::application::component::{Component, SelectMenu}; assert_impl_all!(SelectMenuBuilder: Clone, Debug, Eq, PartialEq, Send, Sync); assert_impl_all!(SelectMenu: TryFrom); + assert_impl_all!(Component: TryFrom); #[test] - fn test_select_menu_builder() { - let select_menu = SelectMenuBuilder::new("a-menu".to_string()).build(); - + fn builder() { let expected = SelectMenu { custom_id: "a-menu".to_string(), disabled: false, @@ -267,15 +287,29 @@ mod test { placeholder: None, }; - assert_eq!(select_menu, expected); + let actual = SelectMenuBuilder::new("a-menu".to_string()).build(); + + assert_eq!(actual, expected); } #[test] - fn test_select_menu_builder_disabled() { - let select_menu = SelectMenuBuilder::new("a-menu".to_string()) - .disable(true) - .build(); + fn into_component() { + let expected = Component::SelectMenu(SelectMenu { + custom_id: "a-menu".to_string(), + disabled: false, + max_values: None, + min_values: None, + options: Vec::new(), + placeholder: None, + }); + + let actual = SelectMenuBuilder::new("a-menu".to_string()).into_component(); + assert_eq!(actual, expected); + } + + #[test] + fn disabled() { let expected = SelectMenu { custom_id: "a-menu".to_string(), disabled: true, @@ -285,15 +319,15 @@ mod test { placeholder: None, }; - assert_eq!(select_menu, expected); + let actual = SelectMenuBuilder::new("a-menu".to_string()) + .disable(true) + .build(); + + assert_eq!(actual, expected); } #[test] - fn test_select_menu_builder_explicit_enabled() { - let select_menu = SelectMenuBuilder::new("a-menu".to_string()) - .disable(false) - .build(); - + fn explicit_enabled() { let expected = SelectMenu { custom_id: "a-menu".to_string(), disabled: false, @@ -303,16 +337,15 @@ mod test { placeholder: None, }; - assert_eq!(select_menu, expected); + let actual = SelectMenuBuilder::new("a-menu".to_string()) + .disable(false) + .build(); + + assert_eq!(actual, expected); } #[test] - fn test_select_menu_builder_limited_values() { - let select_menu = SelectMenuBuilder::new("a-menu".to_string()) - .max_values(10) - .min_values(2) - .build(); - + fn limited_values() { let expected = SelectMenu { custom_id: "a-menu".to_string(), disabled: false, @@ -322,24 +355,61 @@ mod test { placeholder: None, }; - assert_eq!(select_menu, expected); + let actual = SelectMenuBuilder::new("a-menu".to_string()) + .max_values(10) + .min_values(2) + .build(); + + assert_eq!(actual, expected); } #[test] - fn test_select_menu_builder_placeholder() { - let select_menu = SelectMenuBuilder::new("a-menu".to_string()) + fn placeholder() { + let expected = SelectMenu { + custom_id: "a-menu".to_string(), + disabled: false, + max_values: None, + min_values: None, + options: Vec::new(), + placeholder: Some("I'm a placeholder".to_string()), + }; + + let actual = SelectMenuBuilder::new("a-menu".to_string()) .placeholder("I'm a placeholder".to_string()) .build(); + assert_eq!(actual, expected); + } + + #[test] + fn select_menu_try_from() { let expected = SelectMenu { custom_id: "a-menu".to_string(), disabled: false, max_values: None, min_values: None, options: Vec::new(), - placeholder: Some("I'm a placeholder".to_string()), + placeholder: None, }; - assert_eq!(select_menu, expected); + let actual = SelectMenu::try_from(SelectMenuBuilder::new("a-menu".into())).unwrap(); + + assert_eq!(actual, expected); + } + + #[test] + fn component_try_from() { + let expected = Component::SelectMenu(SelectMenu { + custom_id: "a-menu".to_string(), + disabled: false, + max_values: None, + min_values: None, + options: Vec::new(), + placeholder: None, + }); + + let actual = Component::try_from(SelectMenuBuilder::new("a-menu".into())).unwrap(); + + assert_eq!(actual, expected); } } diff --git a/util/src/builder/component/text_input.rs b/util/src/builder/component/text_input.rs index 876f921beab..4492b3fd4d7 100644 --- a/util/src/builder/component/text_input.rs +++ b/util/src/builder/component/text_input.rs @@ -16,7 +16,7 @@ use std::convert::TryFrom; -use twilight_model::application::component::{text_input::TextInputStyle, TextInput}; +use twilight_model::application::component::{text_input::TextInputStyle, Component, TextInput}; use twilight_validate::component::{text_input as validate_text_input, ComponentValidationError}; /// Create a [`TextInput`] with a builder. @@ -89,6 +89,13 @@ impl TextInputBuilder { Ok(self) } + /// Consume the builder, returning a text input wrapped in + /// [`Component::TextInput`] + #[must_use = "builders have no effect if unused"] + pub fn into_component(self) -> Component { + Component::TextInput(self.0) + } + /// Set the maximum amount of characters allowed to be entered in this text input. /// /// # Examples @@ -199,15 +206,48 @@ impl TryFrom for TextInput { } } +impl TryFrom for Component { + type Error = ComponentValidationError; + + /// Convert a `TextInputBuilder` into a `TextInput`. + /// + /// This is equivalent to calling [`TextInputBuilder::validate`] + /// then [`TextInputBuilder::into_component`]. + fn try_from(builder: TextInputBuilder) -> Result { + Ok(builder.validate()?.into_component()) + } +} + #[cfg(test)] mod tests { use super::TextInputBuilder; use static_assertions::assert_impl_all; use std::{convert::TryFrom, fmt::Debug}; - use twilight_model::application::component::{text_input::TextInputStyle, TextInput}; + use twilight_model::application::component::{ + text_input::TextInputStyle, Component, TextInput, + }; assert_impl_all!(TextInputBuilder: Clone, Debug, Eq, PartialEq, Send, Sync); assert_impl_all!(TextInput: TryFrom); + assert_impl_all!(Component: TryFrom); + + #[test] + fn into_component() { + let expected = Component::TextInput(TextInput { + custom_id: "input".into(), + label: "label".into(), + max_length: None, + min_length: None, + placeholder: None, + required: None, + style: TextInputStyle::Short, + value: None, + }); + + let actual = TextInputBuilder::short("input".to_string(), "label".into()).into_component(); + + assert_eq!(actual, expected); + } #[test] fn test_text_input_builder_short() { @@ -324,4 +364,42 @@ mod tests { assert_eq!(actual, expected); } + + #[test] + fn text_input_try_from() { + let expected = TextInput { + custom_id: "input".into(), + label: "label".into(), + max_length: None, + min_length: None, + placeholder: None, + required: None, + style: TextInputStyle::Short, + value: None, + }; + + let actual = + TextInput::try_from(TextInputBuilder::short("input".into(), "label".into())).unwrap(); + + assert_eq!(actual, expected); + } + + #[test] + fn component_try_from() { + let expected = Component::TextInput(TextInput { + custom_id: "input".into(), + label: "label".into(), + max_length: None, + min_length: None, + placeholder: None, + required: None, + style: TextInputStyle::Short, + value: None, + }); + + let actual = + Component::try_from(TextInputBuilder::short("input".into(), "label".into())).unwrap(); + + assert_eq!(actual, expected); + } } From 5622e71bb5cec82ddacf5f2050fb4bcaef8b099d Mon Sep 17 00:00:00 2001 From: ITOH Date: Wed, 1 Jun 2022 15:42:50 +0200 Subject: [PATCH 11/11] clippy --- util/src/builder/component/action_row.rs | 1 + util/src/builder/component/button.rs | 1 + util/src/builder/component/select_menu.rs | 3 ++- util/src/builder/component/text_input.rs | 3 ++- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/util/src/builder/component/action_row.rs b/util/src/builder/component/action_row.rs index 381b4797a32..484d0ce4c93 100644 --- a/util/src/builder/component/action_row.rs +++ b/util/src/builder/component/action_row.rs @@ -78,6 +78,7 @@ impl ActionRowBuilder { /// Consume the builder, returning an action row wrapped in /// [`Component::ActionRow`]. + #[allow(clippy::missing_const_for_fn)] #[must_use = "builders have no effect if unused"] pub fn into_component(self) -> Component { Component::ActionRow(self.build()) diff --git a/util/src/builder/component/button.rs b/util/src/builder/component/button.rs index fe1191a2a8f..0a1d5c49b25 100644 --- a/util/src/builder/component/button.rs +++ b/util/src/builder/component/button.rs @@ -124,6 +124,7 @@ impl ButtonBuilder { /// Consume the builder, returning a button wrapped in /// [`Component::Button`]. + #[allow(clippy::missing_const_for_fn)] #[must_use = "builders have no effect if unused"] pub fn into_component(self) -> Component { Component::Button(self.build()) diff --git a/util/src/builder/component/select_menu.rs b/util/src/builder/component/select_menu.rs index 1e54536bff2..296a2db1773 100644 --- a/util/src/builder/component/select_menu.rs +++ b/util/src/builder/component/select_menu.rs @@ -106,9 +106,10 @@ impl SelectMenuBuilder { /// Consume the builder, returning a select menu wrapped in /// [`Component::SelectMenu`] + #[allow(clippy::missing_const_for_fn)] #[must_use = "builders have no effect if unused"] pub fn into_component(self) -> Component { - Component::SelectMenu(self.0) + Component::SelectMenu(self.build()) } /// Set the minimum values for this select menu. diff --git a/util/src/builder/component/text_input.rs b/util/src/builder/component/text_input.rs index 4492b3fd4d7..4405a42e3ba 100644 --- a/util/src/builder/component/text_input.rs +++ b/util/src/builder/component/text_input.rs @@ -91,9 +91,10 @@ impl TextInputBuilder { /// Consume the builder, returning a text input wrapped in /// [`Component::TextInput`] + #[allow(clippy::missing_const_for_fn)] #[must_use = "builders have no effect if unused"] pub fn into_component(self) -> Component { - Component::TextInput(self.0) + Component::TextInput(self.build()) } /// Set the maximum amount of characters allowed to be entered in this text input.