mod component_v2;
use std::{
error::Error,
fmt::{Debug, Display, Formatter, Result as FmtResult},
};
use twilight_model::channel::message::component::{
ActionRow, Button, ButtonStyle, Component, ComponentType, SelectMenu, SelectMenuOption,
SelectMenuType, TextInput,
};
pub use component_v2::{
FILE_UPLOAD_MAXIMUM_VALUES_LIMIT, FILE_UPLOAD_MINIMUM_VALUES_LIMIT,
LABEL_DESCRIPTION_LENGTH_MAX, LABEL_LABEL_LENGTH_MAX,
MEDIA_GALLERY_ITEM_DESCRIPTION_LENGTH_MAX, MEDIA_GALLERY_ITEMS_MAX, MEDIA_GALLERY_ITEMS_MIN,
SECTION_COMPONENTS_MAX, SECTION_COMPONENTS_MIN, TEXT_DISPLAY_CONTENT_LENGTH_MAX,
THUMBNAIL_DESCRIPTION_LENGTH_MAX, component_v2, container, file_upload, label, media_gallery,
media_gallery_item, section, text_display, thumbnail,
};
pub const ACTION_ROW_COMPONENT_COUNT: usize = 5;
pub const COMPONENT_COUNT: usize = 5;
pub const COMPONENT_V2_COUNT: usize = 40;
pub const COMPONENT_CUSTOM_ID_LENGTH: usize = 100;
pub const COMPONENT_BUTTON_LABEL_LENGTH: usize = 80;
pub const SELECT_MAXIMUM_VALUES_LIMIT: usize = 25;
pub const SELECT_MAXIMUM_VALUES_REQUIREMENT: usize = 1;
pub const SELECT_MINIMUM_VALUES_LIMIT: usize = 25;
pub const SELECT_OPTION_COUNT: usize = 25;
pub const SELECT_OPTION_DESCRIPTION_LENGTH: usize = 100;
pub const SELECT_OPTION_LABEL_LENGTH: usize = 100;
pub const SELECT_OPTION_VALUE_LENGTH: usize = 100;
pub const SELECT_PLACEHOLDER_LENGTH: usize = 150;
pub const TEXT_INPUT_LABEL_MAX: usize = 45;
pub const TEXT_INPUT_LABEL_MIN: usize = 1;
pub const TEXT_INPUT_LENGTH_MAX: usize = 4000;
pub const TEXT_INPUT_LENGTH_MIN: usize = 1;
pub const TEXT_INPUT_PLACEHOLDER_MAX: usize = 100;
#[derive(Debug)]
pub struct ComponentValidationError {
kind: ComponentValidationErrorType,
}
impl ComponentValidationError {
#[must_use = "retrieving the type has no effect if left unused"]
pub const fn kind(&self) -> &ComponentValidationErrorType {
&self.kind
}
#[allow(clippy::unused_self)]
#[must_use = "consuming the error and retrieving the source has no effect if left unused"]
pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
None
}
#[must_use = "consuming the error into its parts has no effect if left unused"]
pub fn into_parts(
self,
) -> (
ComponentValidationErrorType,
Option<Box<dyn Error + Send + Sync>>,
) {
(self.kind, None)
}
}
impl Display for ComponentValidationError {
#[allow(clippy::too_many_lines)]
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match &self.kind {
ComponentValidationErrorType::ActionRowComponentCount { count } => {
f.write_str("an action row has ")?;
Display::fmt(&count, f)?;
f.write_str(" children, but the max is ")?;
Display::fmt(&ACTION_ROW_COMPONENT_COUNT, f)
}
ComponentValidationErrorType::ButtonConflict => {
f.write_str("button has both a custom id and url, which is never valid")
}
ComponentValidationErrorType::ButtonStyle { style } => {
f.write_str("button has a type of ")?;
Debug::fmt(style, f)?;
f.write_str(", which must have a ")?;
f.write_str(if *style == ButtonStyle::Link {
"url"
} else {
"custom id"
})?;
f.write_str(" configured")
}
ComponentValidationErrorType::ComponentCount { count } => {
Display::fmt(count, f)?;
f.write_str(" components were provided, but the max is ")?;
Display::fmt(&COMPONENT_COUNT, f)
}
ComponentValidationErrorType::ComponentCustomIdLength { chars } => {
f.write_str("a component's custom id is ")?;
Display::fmt(&chars, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&COMPONENT_CUSTOM_ID_LENGTH, f)
}
ComponentValidationErrorType::ComponentLabelLength { chars } => {
f.write_str("a component's label is ")?;
Display::fmt(&chars, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&COMPONENT_BUTTON_LABEL_LENGTH, f)
}
ComponentValidationErrorType::InvalidChildComponent { kind } => {
f.write_str("a '")?;
Display::fmt(&kind, f)?;
f.write_str(" component was provided, but can not be a child component")
}
ComponentValidationErrorType::InvalidRootComponent { kind } => {
f.write_str("a '")?;
Display::fmt(kind, f)?;
f.write_str("' component was provided, but can not be a root component")
}
ComponentValidationErrorType::SelectMaximumValuesCount { count } => {
f.write_str("maximum number of values that can be chosen is ")?;
Display::fmt(count, f)?;
f.write_str(", but must be greater than or equal to ")?;
Display::fmt(&SELECT_MAXIMUM_VALUES_REQUIREMENT, f)?;
f.write_str("and less than or equal to ")?;
Display::fmt(&SELECT_MAXIMUM_VALUES_LIMIT, f)
}
ComponentValidationErrorType::SelectMinimumValuesCount { count } => {
f.write_str("minimum number of values that must be chosen is ")?;
Display::fmt(count, f)?;
f.write_str(", but must be less than or equal to ")?;
Display::fmt(&SELECT_MINIMUM_VALUES_LIMIT, f)
}
ComponentValidationErrorType::SelectNotEnoughDefaultValues { provided, min } => {
f.write_str("a select menu provided ")?;
Display::fmt(provided, f)?;
f.write_str(" values, but it requires at least ")?;
Display::fmt(min, f)?;
f.write_str(" values")
}
ComponentValidationErrorType::SelectOptionsMissing => {
f.write_str("a text select menu doesn't specify the required options field")
}
ComponentValidationErrorType::SelectOptionDescriptionLength { chars } => {
f.write_str("a select menu option's description is ")?;
Display::fmt(&chars, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&SELECT_OPTION_DESCRIPTION_LENGTH, f)
}
ComponentValidationErrorType::SelectOptionLabelLength { chars } => {
f.write_str("a select menu option's label is ")?;
Display::fmt(&chars, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&SELECT_OPTION_LABEL_LENGTH, f)
}
ComponentValidationErrorType::SelectOptionValueLength { chars } => {
f.write_str("a select menu option's value is ")?;
Display::fmt(&chars, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&SELECT_OPTION_VALUE_LENGTH, f)
}
ComponentValidationErrorType::SelectPlaceholderLength { chars } => {
f.write_str("a select menu's placeholder is ")?;
Display::fmt(&chars, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&SELECT_PLACEHOLDER_LENGTH, f)
}
ComponentValidationErrorType::SelectOptionCount { count } => {
f.write_str("a select menu has ")?;
Display::fmt(&count, f)?;
f.write_str(" options, but the max is ")?;
Display::fmt(&SELECT_OPTION_COUNT, f)
}
ComponentValidationErrorType::SelectTooManyDefaultValues { provided, max } => {
f.write_str("a select menu provided ")?;
Display::fmt(provided, f)?;
f.write_str(" values, but it allows at most ")?;
Display::fmt(max, f)?;
f.write_str(" values")
}
ComponentValidationErrorType::SelectUnsupportedDefaultValues { kind } => {
f.write_str("a select menu has defined default_values, but its type, ")?;
Debug::fmt(kind, f)?;
f.write_str(", does not support them")
}
ComponentValidationErrorType::TextInputLabelLength { len: count } => {
f.write_str("a text input label length is ")?;
Display::fmt(count, f)?;
f.write_str(", but it must be at least ")?;
Display::fmt(&TEXT_INPUT_LABEL_MIN, f)?;
f.write_str(" and at most ")?;
Display::fmt(&TEXT_INPUT_LABEL_MAX, f)
}
ComponentValidationErrorType::TextInputMaxLength { len: count } => {
f.write_str("a text input max length is ")?;
Display::fmt(count, f)?;
f.write_str(", but it must be at least ")?;
Display::fmt(&TEXT_INPUT_LENGTH_MIN, f)?;
f.write_str(" and at most ")?;
Display::fmt(&TEXT_INPUT_LENGTH_MAX, f)
}
ComponentValidationErrorType::TextInputMinLength { len: count } => {
f.write_str("a text input min length is ")?;
Display::fmt(count, f)?;
f.write_str(", but it must be at most ")?;
Display::fmt(&TEXT_INPUT_LENGTH_MAX, f)
}
ComponentValidationErrorType::TextInputPlaceholderLength { chars } => {
f.write_str("a text input's placeholder is ")?;
Display::fmt(&chars, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&TEXT_INPUT_PLACEHOLDER_MAX, f)
}
ComponentValidationErrorType::TextInputValueLength { chars } => {
f.write_str("a text input's value is ")?;
Display::fmt(&chars, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&TEXT_INPUT_PLACEHOLDER_MAX, f)
}
ComponentValidationErrorType::TextInputDisallowedLabel => {
f.write_str("a text input contained a label when disallowed")
}
ComponentValidationErrorType::DisallowedV2Components => {
f.write_str("a V2 component was used in a component V1 message")
}
ComponentValidationErrorType::DisallowedChildren => {
f.write_str("a component contains a disallowed child component")
}
ComponentValidationErrorType::TextDisplayContentTooLong { len: count } => {
f.write_str("a text display content length is ")?;
Display::fmt(count, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&TEXT_DISPLAY_CONTENT_LENGTH_MAX, f)
}
ComponentValidationErrorType::MediaGalleryItemCountOutOfRange { count } => {
f.write_str("a media gallery has ")?;
Display::fmt(count, f)?;
f.write_str(" items, but the min and max are ")?;
Display::fmt(&MEDIA_GALLERY_ITEMS_MIN, f)?;
f.write_str(" and ")?;
Display::fmt(&MEDIA_GALLERY_ITEMS_MAX, f)?;
f.write_str(" respectively")
}
ComponentValidationErrorType::MediaGalleryItemDescriptionTooLong { len } => {
f.write_str("a media gallery item description length is ")?;
Display::fmt(len, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&MEDIA_GALLERY_ITEM_DESCRIPTION_LENGTH_MAX, f)
}
ComponentValidationErrorType::SectionComponentCountOutOfRange { count } => {
f.write_str("a section has ")?;
Display::fmt(count, f)?;
f.write_str(" components, but the min and max are ")?;
Display::fmt(&SECTION_COMPONENTS_MIN, f)?;
f.write_str(" and ")?;
Display::fmt(&SECTION_COMPONENTS_MAX, f)?;
f.write_str(" respectively")
}
ComponentValidationErrorType::SelectDisallowedDisabled => {
f.write_str("a select menu was disabled when disallowed")
}
ComponentValidationErrorType::ThumbnailDescriptionTooLong { len } => {
f.write_str("a thumbnail description length is ")?;
Display::fmt(len, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&THUMBNAIL_DESCRIPTION_LENGTH_MAX, f)
}
ComponentValidationErrorType::LabelLabelTooLong { len } => {
f.write_str("a label text of a label component is ")?;
Display::fmt(len, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&LABEL_LABEL_LENGTH_MAX, f)
}
ComponentValidationErrorType::LabelDescriptionTooLong { len } => {
f.write_str("a label description length is ")?;
Display::fmt(len, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&LABEL_DESCRIPTION_LENGTH_MAX, f)
}
ComponentValidationErrorType::FileUploadMaximumValuesCount { count } => {
f.write_str("maximum number of files that can be uploaded is ")?;
Display::fmt(count, f)?;
f.write_str(", but must be less than or equal to ")?;
Display::fmt(&FILE_UPLOAD_MAXIMUM_VALUES_LIMIT, f)
}
ComponentValidationErrorType::FileUploadMinimumValuesCount { count } => {
f.write_str("minimum number of files that must be uploaded is ")?;
Display::fmt(count, f)?;
f.write_str(", but must be less than or equal to ")?;
Display::fmt(&FILE_UPLOAD_MINIMUM_VALUES_LIMIT, f)
}
}
}
}
impl Error for ComponentValidationError {}
#[derive(Debug)]
#[non_exhaustive]
pub enum ComponentValidationErrorType {
ActionRowComponentCount {
count: usize,
},
ButtonConflict,
ButtonStyle {
style: ButtonStyle,
},
ComponentCount {
count: usize,
},
ComponentCustomIdLength {
chars: usize,
},
ComponentLabelLength {
chars: usize,
},
InvalidChildComponent {
kind: ComponentType,
},
InvalidRootComponent {
kind: ComponentType,
},
SelectMaximumValuesCount {
count: usize,
},
SelectMinimumValuesCount {
count: usize,
},
SelectNotEnoughDefaultValues {
provided: usize,
min: usize,
},
SelectOptionsMissing,
SelectOptionCount {
count: usize,
},
SelectOptionDescriptionLength {
chars: usize,
},
SelectOptionLabelLength {
chars: usize,
},
SelectOptionValueLength {
chars: usize,
},
SelectPlaceholderLength {
chars: usize,
},
SelectTooManyDefaultValues {
provided: usize,
max: usize,
},
SelectUnsupportedDefaultValues {
kind: SelectMenuType,
},
TextInputLabelLength {
len: usize,
},
TextInputMaxLength {
len: usize,
},
TextInputMinLength {
len: usize,
},
TextInputPlaceholderLength {
chars: usize,
},
TextInputValueLength {
chars: usize,
},
TextInputDisallowedLabel,
DisallowedV2Components,
DisallowedChildren,
TextDisplayContentTooLong {
len: usize,
},
MediaGalleryItemCountOutOfRange {
count: usize,
},
MediaGalleryItemDescriptionTooLong {
len: usize,
},
SectionComponentCountOutOfRange {
count: usize,
},
SelectDisallowedDisabled,
ThumbnailDescriptionTooLong {
len: usize,
},
LabelLabelTooLong {
len: usize,
},
LabelDescriptionTooLong {
len: usize,
},
FileUploadMaximumValuesCount {
count: u8,
},
FileUploadMinimumValuesCount {
count: u8,
},
}
pub fn component_v1(component: &Component) -> Result<(), ComponentValidationError> {
match component {
Component::ActionRow(action_row) => self::action_row(action_row, false)?,
other => {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::InvalidRootComponent { kind: other.kind() },
});
}
}
Ok(())
}
#[deprecated(note = "Use component_v1 for old components and component_v2 for new ones")]
pub fn component(component: &Component) -> Result<(), ComponentValidationError> {
component_v1(component)
}
pub fn action_row(action_row: &ActionRow, is_v2: bool) -> Result<(), ComponentValidationError> {
self::component_action_row_components(&action_row.components)?;
for component in &action_row.components {
match component {
Component::ActionRow(_) | Component::Label(_) => {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::InvalidChildComponent {
kind: component.kind(),
},
});
}
Component::Button(button) => self::button(button)?,
Component::SelectMenu(select_menu) => self::select_menu(select_menu, true)?,
Component::TextInput(text_input) => self::text_input(text_input, true)?,
Component::Unknown(unknown) => {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::InvalidChildComponent {
kind: ComponentType::Unknown(*unknown),
},
});
}
Component::TextDisplay(_)
| Component::MediaGallery(_)
| Component::Separator(_)
| Component::File(_)
| Component::Section(_)
| Component::Container(_)
| Component::Thumbnail(_)
| Component::FileUpload(_) => {
return Err(ComponentValidationError {
kind: if is_v2 {
ComponentValidationErrorType::DisallowedChildren
} else {
ComponentValidationErrorType::DisallowedV2Components
},
});
}
}
}
Ok(())
}
pub fn button(button: &Button) -> Result<(), ComponentValidationError> {
let has_custom_id = button.custom_id.is_some();
let has_emoji = button.emoji.is_some();
let has_label = button.label.is_some();
let has_sku_id = button.sku_id.is_some();
let has_url = button.url.is_some();
if has_custom_id && has_url {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::ButtonConflict,
});
}
let is_premium = button.style == ButtonStyle::Premium;
if is_premium && (has_custom_id || has_url || has_label || has_emoji || !has_sku_id) {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::ButtonStyle {
style: button.style,
},
});
}
let is_link = button.style == ButtonStyle::Link;
if (is_link && !has_url) || (!is_link && !has_custom_id) {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::ButtonStyle {
style: button.style,
},
});
}
if let Some(custom_id) = button.custom_id.as_ref() {
self::component_custom_id(custom_id)?;
}
if let Some(label) = button.label.as_ref() {
self::component_button_label(label)?;
}
Ok(())
}
pub fn select_menu(
select_menu: &SelectMenu,
disabled_allowed: bool,
) -> Result<(), ComponentValidationError> {
self::component_custom_id(&select_menu.custom_id)?;
if !disabled_allowed && select_menu.disabled {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::SelectDisallowedDisabled,
});
}
if let SelectMenuType::Text = &select_menu.kind {
let options = select_menu
.options
.as_ref()
.ok_or(ComponentValidationError {
kind: ComponentValidationErrorType::SelectOptionsMissing,
})?;
for option in options {
component_select_option_label(&option.label)?;
component_select_option_value(&option.value)?;
if let Some(description) = option.description.as_ref() {
component_option_description(description)?;
}
}
component_select_options(options)?;
}
if let Some(placeholder) = select_menu.placeholder.as_ref() {
self::component_select_placeholder(placeholder)?;
}
if let Some(max_values) = select_menu.max_values {
self::component_select_max_values(usize::from(max_values))?;
}
if let Some(min_values) = select_menu.min_values {
self::component_select_min_values(usize::from(min_values))?;
}
if let Some(default_values) = select_menu.default_values.as_ref() {
component_select_default_values_supported(select_menu.kind)?;
component_select_default_values_count(
select_menu.min_values,
select_menu.max_values,
default_values.len(),
)?;
}
Ok(())
}
pub fn text_input(
text_input: &TextInput,
label_allowed: bool,
) -> Result<(), ComponentValidationError> {
self::component_custom_id(&text_input.custom_id)?;
#[allow(deprecated)]
if let Some(label) = &text_input.label {
if !label_allowed {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::TextInputDisallowedLabel,
});
}
self::component_text_input_label(label)?;
}
if let Some(max_length) = text_input.max_length {
self::component_text_input_max(max_length)?;
}
if let Some(min_length) = text_input.min_length {
self::component_text_input_min(min_length)?;
}
if let Some(placeholder) = text_input.placeholder.as_ref() {
self::component_text_input_placeholder(placeholder)?;
}
if let Some(value) = text_input.value.as_ref() {
self::component_text_input_value(value)?;
}
Ok(())
}
const fn component_action_row_components(
components: &[Component],
) -> Result<(), ComponentValidationError> {
let count = components.len();
if count > COMPONENT_COUNT {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::ActionRowComponentCount { count },
});
}
Ok(())
}
fn component_button_label(label: impl AsRef<str>) -> Result<(), ComponentValidationError> {
let chars = label.as_ref().chars().count();
if chars > COMPONENT_BUTTON_LABEL_LENGTH {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::ComponentLabelLength { chars },
});
}
Ok(())
}
fn component_custom_id(custom_id: impl AsRef<str>) -> Result<(), ComponentValidationError> {
let chars = custom_id.as_ref().chars().count();
if chars > COMPONENT_CUSTOM_ID_LENGTH {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::ComponentCustomIdLength { chars },
});
}
Ok(())
}
fn component_option_description(
description: impl AsRef<str>,
) -> Result<(), ComponentValidationError> {
let chars = description.as_ref().chars().count();
if chars > SELECT_OPTION_DESCRIPTION_LENGTH {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::SelectOptionDescriptionLength { chars },
});
}
Ok(())
}
const fn component_select_default_values_supported(
menu_type: SelectMenuType,
) -> Result<(), ComponentValidationError> {
if !matches!(
menu_type,
SelectMenuType::User
| SelectMenuType::Role
| SelectMenuType::Mentionable
| SelectMenuType::Channel
) {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::SelectUnsupportedDefaultValues { kind: menu_type },
});
}
Ok(())
}
const fn component_select_default_values_count(
min_values: Option<u8>,
max_values: Option<u8>,
default_values: usize,
) -> Result<(), ComponentValidationError> {
if let Some(min) = min_values {
let min = min as usize;
if default_values < min {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::SelectNotEnoughDefaultValues {
provided: default_values,
min,
},
});
}
}
if let Some(max) = max_values {
let max = max as usize;
if default_values > max {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::SelectTooManyDefaultValues {
provided: default_values,
max,
},
});
}
}
Ok(())
}
const fn component_select_max_values(count: usize) -> Result<(), ComponentValidationError> {
if count > SELECT_MAXIMUM_VALUES_LIMIT {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::SelectMaximumValuesCount { count },
});
}
if count < SELECT_MAXIMUM_VALUES_REQUIREMENT {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::SelectMaximumValuesCount { count },
});
}
Ok(())
}
const fn component_select_min_values(count: usize) -> Result<(), ComponentValidationError> {
if count > SELECT_MINIMUM_VALUES_LIMIT {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::SelectMinimumValuesCount { count },
});
}
Ok(())
}
fn component_select_option_label(label: impl AsRef<str>) -> Result<(), ComponentValidationError> {
let chars = label.as_ref().chars().count();
if chars > SELECT_OPTION_LABEL_LENGTH {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::SelectOptionLabelLength { chars },
});
}
Ok(())
}
fn component_select_option_value(value: impl AsRef<str>) -> Result<(), ComponentValidationError> {
let chars = value.as_ref().chars().count();
if chars > SELECT_OPTION_VALUE_LENGTH {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::SelectOptionValueLength { chars },
});
}
Ok(())
}
const fn component_select_options(
options: &[SelectMenuOption],
) -> Result<(), ComponentValidationError> {
let count = options.len();
if count > SELECT_OPTION_COUNT {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::SelectOptionCount { count },
});
}
Ok(())
}
fn component_select_placeholder(
placeholder: impl AsRef<str>,
) -> Result<(), ComponentValidationError> {
let chars = placeholder.as_ref().chars().count();
if chars > SELECT_PLACEHOLDER_LENGTH {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::SelectPlaceholderLength { chars },
});
}
Ok(())
}
fn component_text_input_label(label: impl AsRef<str>) -> Result<(), ComponentValidationError> {
let len = label.as_ref().len();
if (TEXT_INPUT_LABEL_MIN..=TEXT_INPUT_LABEL_MAX).contains(&len) {
Ok(())
} else {
Err(ComponentValidationError {
kind: ComponentValidationErrorType::TextInputLabelLength { len },
})
}
}
const fn component_text_input_max(len: u16) -> Result<(), ComponentValidationError> {
let len = len as usize;
if len >= TEXT_INPUT_LENGTH_MIN && len <= TEXT_INPUT_LENGTH_MAX {
Ok(())
} else {
Err(ComponentValidationError {
kind: ComponentValidationErrorType::TextInputMaxLength { len },
})
}
}
const fn component_text_input_min(len: u16) -> Result<(), ComponentValidationError> {
let len = len as usize;
if len <= TEXT_INPUT_LENGTH_MAX {
Ok(())
} else {
Err(ComponentValidationError {
kind: ComponentValidationErrorType::TextInputMinLength { len },
})
}
}
fn component_text_input_placeholder(
placeholder: impl AsRef<str>,
) -> Result<(), ComponentValidationError> {
let chars = placeholder.as_ref().chars().count();
if chars <= TEXT_INPUT_PLACEHOLDER_MAX {
Ok(())
} else {
Err(ComponentValidationError {
kind: ComponentValidationErrorType::TextInputPlaceholderLength { chars },
})
}
}
fn component_text_input_value(value: impl AsRef<str>) -> Result<(), ComponentValidationError> {
let chars = value.as_ref().chars().count();
if chars <= TEXT_INPUT_LENGTH_MAX {
Ok(())
} else {
Err(ComponentValidationError {
kind: ComponentValidationErrorType::TextInputValueLength { chars },
})
}
}
#[allow(clippy::non_ascii_literal)]
#[cfg(test)]
mod tests {
use super::*;
use static_assertions::{assert_fields, assert_impl_all};
use twilight_model::channel::message::EmojiReactionType;
assert_fields!(ComponentValidationErrorType::ActionRowComponentCount: count);
assert_fields!(ComponentValidationErrorType::ComponentCount: count);
assert_fields!(ComponentValidationErrorType::ComponentCustomIdLength: chars);
assert_fields!(ComponentValidationErrorType::ComponentLabelLength: chars);
assert_fields!(ComponentValidationErrorType::InvalidChildComponent: kind);
assert_fields!(ComponentValidationErrorType::InvalidRootComponent: kind);
assert_fields!(ComponentValidationErrorType::SelectMaximumValuesCount: count);
assert_fields!(ComponentValidationErrorType::SelectMinimumValuesCount: count);
assert_fields!(ComponentValidationErrorType::SelectOptionDescriptionLength: chars);
assert_fields!(ComponentValidationErrorType::SelectOptionLabelLength: chars);
assert_fields!(ComponentValidationErrorType::SelectOptionValueLength: chars);
assert_fields!(ComponentValidationErrorType::SelectPlaceholderLength: chars);
assert_impl_all!(ComponentValidationErrorType: Debug, Send, Sync);
assert_impl_all!(ComponentValidationError: Debug, Send, Sync);
const ALL_BUTTON_STYLES: &[ButtonStyle] = &[
ButtonStyle::Primary,
ButtonStyle::Secondary,
ButtonStyle::Success,
ButtonStyle::Danger,
ButtonStyle::Link,
ButtonStyle::Premium,
];
#[test]
fn component_action_row() {
let button = Button {
custom_id: None,
disabled: false,
emoji: Some(EmojiReactionType::Unicode {
name: "📚".into()
}),
label: Some("Read".into()),
style: ButtonStyle::Link,
url: Some("https://abebooks.com".into()),
sku_id: None,
id: None,
};
let select_menu = SelectMenu {
channel_types: None,
custom_id: "custom id 2".into(),
disabled: false,
default_values: None,
kind: SelectMenuType::Text,
max_values: Some(2),
min_values: Some(1),
options: Some(Vec::from([SelectMenuOption {
default: true,
description: Some("Book 1 of the Expanse".into()),
emoji: None,
label: "Leviathan Wakes".into(),
value: "9780316129084".into(),
}])),
placeholder: Some("Choose a book".into()),
id: None,
required: None,
};
let action_row = ActionRow {
components: Vec::from([
Component::SelectMenu(select_menu.clone()),
Component::Button(button),
]),
id: None,
};
assert!(component_v1(&Component::ActionRow(action_row.clone())).is_ok());
assert!(component_v1(&Component::SelectMenu(select_menu.clone())).is_err());
assert!(super::action_row(&action_row, false).is_ok());
let invalid_action_row = Component::ActionRow(ActionRow {
components: Vec::from([
Component::SelectMenu(select_menu.clone()),
Component::SelectMenu(select_menu.clone()),
Component::SelectMenu(select_menu.clone()),
Component::SelectMenu(select_menu.clone()),
Component::SelectMenu(select_menu.clone()),
Component::SelectMenu(select_menu),
]),
id: None,
});
assert!(component_v1(&invalid_action_row).is_err());
}
#[test]
fn button_conflict() {
let button = Button {
custom_id: Some("a".to_owned()),
disabled: false,
emoji: None,
label: None,
style: ButtonStyle::Primary,
url: Some("https://twilight.rs".to_owned()),
sku_id: None,
id: None,
};
assert!(matches!(
super::button(&button),
Err(ComponentValidationError {
kind: ComponentValidationErrorType::ButtonConflict,
}),
));
}
#[test]
fn button_style() {
for style in ALL_BUTTON_STYLES {
let button = Button {
custom_id: None,
disabled: false,
emoji: None,
label: Some("some label".to_owned()),
style: *style,
url: None,
sku_id: None,
id: None,
};
assert!(matches!(
super::button(&button),
Err(ComponentValidationError {
kind: ComponentValidationErrorType::ButtonStyle {
style: error_style,
}
})
if error_style == *style
));
}
}
#[test]
fn component_label() {
assert!(component_button_label("").is_ok());
assert!(component_button_label("a").is_ok());
assert!(component_button_label("a".repeat(80)).is_ok());
assert!(component_button_label("a".repeat(81)).is_err());
}
#[test]
fn component_custom_id_length() {
assert!(component_custom_id("").is_ok());
assert!(component_custom_id("a").is_ok());
assert!(component_custom_id("a".repeat(100)).is_ok());
assert!(component_custom_id("a".repeat(101)).is_err());
}
#[test]
fn component_option_description_length() {
assert!(component_option_description("").is_ok());
assert!(component_option_description("a").is_ok());
assert!(component_option_description("a".repeat(100)).is_ok());
assert!(component_option_description("a".repeat(101)).is_err());
}
#[test]
fn component_select_default_values_support() {
assert!(component_select_default_values_supported(SelectMenuType::User).is_ok());
assert!(component_select_default_values_supported(SelectMenuType::Role).is_ok());
assert!(component_select_default_values_supported(SelectMenuType::Mentionable).is_ok());
assert!(component_select_default_values_supported(SelectMenuType::Channel).is_ok());
assert!(component_select_default_values_supported(SelectMenuType::Text).is_err());
}
#[test]
fn component_select_num_default_values() {
assert!(component_select_default_values_count(None, None, 0).is_ok());
assert!(component_select_default_values_count(None, None, 1).is_ok());
assert!(component_select_default_values_count(Some(1), None, 5).is_ok());
assert!(component_select_default_values_count(Some(5), None, 5).is_ok());
assert!(component_select_default_values_count(None, Some(5), 5).is_ok());
assert!(component_select_default_values_count(None, Some(10), 5).is_ok());
assert!(component_select_default_values_count(Some(5), Some(5), 5).is_ok());
assert!(component_select_default_values_count(Some(1), Some(10), 5).is_ok());
assert!(component_select_default_values_count(Some(2), None, 1).is_err());
assert!(component_select_default_values_count(None, Some(1), 2).is_err());
assert!(component_select_default_values_count(Some(1), Some(1), 2).is_err());
assert!(component_select_default_values_count(Some(2), Some(2), 1).is_err());
}
#[test]
fn component_select_max_values_count() {
assert!(component_select_max_values(1).is_ok());
assert!(component_select_max_values(25).is_ok());
assert!(component_select_max_values(0).is_err());
assert!(component_select_max_values(26).is_err());
}
#[test]
fn component_select_min_values_count() {
assert!(component_select_min_values(1).is_ok());
assert!(component_select_min_values(25).is_ok());
assert!(component_select_min_values(26).is_err());
}
#[test]
fn component_select_option_value_length() {
assert!(component_select_option_value("a").is_ok());
assert!(component_select_option_value("a".repeat(100)).is_ok());
assert!(component_select_option_value("a".repeat(101)).is_err());
}
#[test]
fn component_select_options_count() {
let select_menu_options = Vec::from([SelectMenuOption {
default: false,
description: None,
emoji: None,
label: "label".into(),
value: "value".into(),
}]);
assert!(component_select_options(&select_menu_options).is_ok());
let select_menu_options_25 = select_menu_options
.iter()
.cloned()
.cycle()
.take(25)
.collect::<Vec<SelectMenuOption>>();
assert!(component_select_options(&select_menu_options_25).is_ok());
let select_menu_options_26 = select_menu_options
.iter()
.cloned()
.cycle()
.take(26)
.collect::<Vec<SelectMenuOption>>();
assert!(component_select_options(&select_menu_options_26).is_err());
}
#[test]
fn component_select_placeholder_length() {
assert!(component_select_placeholder("").is_ok());
assert!(component_select_placeholder("a").is_ok());
assert!(component_select_placeholder("a".repeat(150)).is_ok());
assert!(component_select_placeholder("a".repeat(151)).is_err());
}
#[test]
fn component_text_input_label_length() {
assert!(component_text_input_label("a").is_ok());
assert!(component_text_input_label("a".repeat(45)).is_ok());
assert!(component_text_input_label("").is_err());
assert!(component_text_input_label("a".repeat(46)).is_err());
}
#[test]
fn component_text_input_max_count() {
assert!(component_text_input_max(1).is_ok());
assert!(component_text_input_max(4000).is_ok());
assert!(component_text_input_max(0).is_err());
assert!(component_text_input_max(4001).is_err());
}
#[test]
fn component_text_input_min_count() {
assert!(component_text_input_min(0).is_ok());
assert!(component_text_input_min(1).is_ok());
assert!(component_text_input_min(4000).is_ok());
assert!(component_text_input_min(4001).is_err());
}
#[test]
fn component_text_input_placeholder_length() {
assert!(component_text_input_placeholder("").is_ok());
assert!(component_text_input_placeholder("a").is_ok());
assert!(component_text_input_placeholder("a".repeat(100)).is_ok());
assert!(component_text_input_placeholder("a".repeat(101)).is_err());
}
#[test]
fn component_text_input_value() {
assert!(component_text_input_min(0).is_ok());
assert!(component_text_input_min(1).is_ok());
assert!(component_text_input_min(4000).is_ok());
assert!(component_text_input_min(4001).is_err());
}
}