use std::{
error::Error,
fmt::{Debug, Display, Formatter, Result as FmtResult},
};
use twilight_model::channel::message::component::{
ActionRow, Button, ButtonStyle, Component, ComponentType, SelectMenu, SelectMenuOption,
TextInput,
};
pub const ACTION_ROW_COMPONENT_COUNT: usize = 5;
pub const COMPONENT_COUNT: usize = 5;
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("maximum 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_MAXIMUM_VALUES_LIMIT, f)
}
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::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)
}
}
}
}
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,
},
SelectOptionCount {
count: usize,
},
SelectOptionDescriptionLength {
chars: usize,
},
SelectOptionLabelLength {
chars: usize,
},
SelectOptionValueLength {
chars: usize,
},
SelectPlaceholderLength {
chars: usize,
},
TextInputLabelLength {
len: usize,
},
TextInputMaxLength {
len: usize,
},
TextInputMinLength {
len: usize,
},
TextInputPlaceholderLength {
chars: usize,
},
TextInputValueLength {
chars: usize,
},
}
pub fn component(component: &Component) -> Result<(), ComponentValidationError> {
match component {
Component::ActionRow(action_row) => self::action_row(action_row)?,
other => {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::InvalidRootComponent { kind: other.kind() },
});
}
}
Ok(())
}
pub fn action_row(action_row: &ActionRow) -> Result<(), ComponentValidationError> {
self::component_action_row_components(&action_row.components)?;
for component in &action_row.components {
match component {
Component::ActionRow(_) => {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::InvalidChildComponent {
kind: ComponentType::ActionRow,
},
});
}
Component::Button(button) => self::button(button)?,
Component::SelectMenu(select_menu) => self::select_menu(select_menu)?,
Component::TextInput(text_input) => self::text_input(text_input)?,
Component::Unknown(unknown) => {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::InvalidChildComponent {
kind: ComponentType::Unknown(*unknown),
},
})
}
}
}
Ok(())
}
pub fn button(button: &Button) -> Result<(), ComponentValidationError> {
let has_custom_id = button.custom_id.is_some();
let has_url = button.url.is_some();
if has_custom_id && has_url {
return Err(ComponentValidationError {
kind: ComponentValidationErrorType::ButtonConflict,
});
}
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) -> Result<(), ComponentValidationError> {
self::component_custom_id(&select_menu.custom_id)?;
self::component_select_options(&select_menu.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))?;
}
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)?;
}
}
Ok(())
}
pub fn text_input(text_input: &TextInput) -> Result<(), ComponentValidationError> {
self::component_custom_id(&text_input.custom_id)?;
self::component_text_input_label(&text_input.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_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 std::fmt::Debug;
use twilight_model::channel::message::ReactionType;
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,
];
#[test]
fn component_action_row() {
let button = Button {
custom_id: None,
disabled: false,
emoji: Some(ReactionType::Unicode {
name: "📚".into()
}),
label: Some("Read".into()),
style: ButtonStyle::Link,
url: Some("https://abebooks.com".into()),
};
let select_menu = SelectMenu {
custom_id: "custom id 2".into(),
disabled: false,
max_values: Some(2),
min_values: Some(1),
options: 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()),
};
let action_row = ActionRow {
components: Vec::from([
Component::SelectMenu(select_menu.clone()),
Component::Button(button),
]),
};
assert!(component(&Component::ActionRow(action_row.clone())).is_ok());
assert!(component(&Component::SelectMenu(select_menu.clone())).is_err());
assert!(super::action_row(&action_row).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),
]),
});
assert!(component(&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()),
};
assert!(matches!(
super::button(&button),
Err(ComponentValidationError {
kind: ComponentValidationErrorType::ButtonConflict,
}),
));
}
#[test]
fn button_style() {
for style in ALL_BUTTON_STYLES.iter() {
let button = Button {
custom_id: None,
disabled: false,
emoji: None,
label: Some("some label".to_owned()),
style: *style,
url: 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_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());
}
}