twilight_validate/
component.rs

1//! Constants, error types, and functions for validating [`Component`]s.
2
3use std::{
4    error::Error,
5    fmt::{Debug, Display, Formatter, Result as FmtResult},
6};
7use twilight_model::channel::message::component::{
8    ActionRow, Button, ButtonStyle, Component, ComponentType, SelectMenu, SelectMenuOption,
9    SelectMenuType, TextInput,
10};
11
12/// Maximum number of [`Component`]s allowed inside an [`ActionRow`].
13///
14/// This is defined in Discord's documentation, per
15/// [Discord Docs/Action Rows][1].
16///
17/// [1]: https://discord.com/developers/docs/interactions/message-components#action-rows
18pub const ACTION_ROW_COMPONENT_COUNT: usize = 5;
19
20/// Maximum number of root [`Component`]s in a message.
21///
22/// This is defined in Discord's documentation, per
23/// [Discord Docs/Action Row][1].
24///
25/// [1]: https://discord.com/developers/docs/interactions/message-components#action-rows
26pub const COMPONENT_COUNT: usize = 5;
27
28/// Maximum length of a [`Component`] custom ID in codepoints.
29///
30/// An example of a component with a custom ID is the
31/// [`Button`][`Button::custom_id`].
32///
33/// This is defined in Discord's documentation, per
34/// [Discord Docs/Components][1].
35///
36/// [1]: https://discord.com/developers/docs/interactions/message-components#component-object-component-structure
37pub const COMPONENT_CUSTOM_ID_LENGTH: usize = 100;
38
39/// Maximum [`Component`] label length in codepoints.
40///
41/// An example of a component with a label is the [`Button`][`Button::label`].
42///
43/// This is defined in Discord's documentation, per
44/// [Discord Docs/Components][1].
45///
46/// [1]: https://discord.com/developers/docs/interactions/message-components#component-object-component-structure
47pub const COMPONENT_BUTTON_LABEL_LENGTH: usize = 80;
48
49/// Maximum number of [`SelectMenuOption`]s that can be chosen in a
50/// [`SelectMenu`].
51///
52/// This is defined in Dicsord's documentation, per
53/// [Discord Docs/Select Menu][1].
54///
55/// [1]: https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure
56pub const SELECT_MAXIMUM_VALUES_LIMIT: usize = 25;
57
58/// Minimum number of [`SelectMenuOption`]s that can be chosen in a
59/// [`SelectMenu`].
60///
61/// This is defined in Dicsord's documentation, per
62/// [Discord Docs/Select Menu][1].
63///
64/// [1]: https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure
65pub const SELECT_MAXIMUM_VALUES_REQUIREMENT: usize = 1;
66
67/// Maximum number of [`SelectMenuOption`]s that must be chosen in a
68/// [`SelectMenu`].
69///
70/// This is defined in Dicsord's documentation, per
71/// [Discord Docs/Select Menu][1].
72///
73/// [1]: https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure
74pub const SELECT_MINIMUM_VALUES_LIMIT: usize = 25;
75
76/// Maximum number of [`SelectMenuOption`]s in a [`SelectMenu`].
77///
78/// This is defined in Discord's documentation, per
79/// [Discord Docs/Select Menu][1].
80///
81/// [1]: https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure
82pub const SELECT_OPTION_COUNT: usize = 25;
83
84/// Maximum length of a [`SelectMenuOption::description`] in codepoints.
85///
86/// This is defined in Discord's documentation, per
87/// [Discord Docs/Select Menu Option][1].
88///
89/// [1]: https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure
90pub const SELECT_OPTION_DESCRIPTION_LENGTH: usize = 100;
91
92/// Maximum length of a [`SelectMenuOption::label`] in codepoints.
93///
94/// This is defined in Discord's documentation, per
95/// [Discord Docs/Select Menu Option][1].
96///
97/// [1]: https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure
98pub const SELECT_OPTION_LABEL_LENGTH: usize = 100;
99
100/// Maximum length of a [`SelectMenuOption::value`] in codepoints.
101///
102/// This is defined in Discord's documentation, per
103/// [Discord Docs/Select Menu Option][1].
104///
105/// [1]: https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure
106pub const SELECT_OPTION_VALUE_LENGTH: usize = 100;
107
108/// Maximum length of a [`SelectMenu::placeholder`] in codepoints.
109///
110/// This is defined in Discord's documentation, per
111/// [Discord Docs/Select Menu][1].
112///
113/// [1]: https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure
114pub const SELECT_PLACEHOLDER_LENGTH: usize = 150;
115
116/// Maximum length of [`TextInput::label`].
117///
118/// This is based on [Discord Docs/Text Inputs].
119///
120/// [Discord Docs/Text Inputs]: https://discord.com/developers/docs/interactions/message-components#text-inputs
121pub const TEXT_INPUT_LABEL_MAX: usize = 45;
122
123/// Minimum length of [`TextInput::label`].
124///
125/// This is based on [Discord Docs/Text Inputs].
126///
127/// [Discord Docs/Text Inputs]: https://discord.com/developers/docs/interactions/message-components#text-inputs
128pub const TEXT_INPUT_LABEL_MIN: usize = 1;
129
130/// Maximum length of [`TextInput::value`].
131///
132/// This is based on [Discord Docs/Text Inputs].
133///
134/// [Discord Docs/Text Inputs]: https://discord.com/developers/docs/interactions/message-components#text-inputs
135pub const TEXT_INPUT_LENGTH_MAX: usize = 4000;
136
137/// Minimum length of [`TextInput::value`].
138///
139/// This is based on [Discord Docs/Text Inputs].
140///
141/// [Discord Docs/Text Inputs]: https://discord.com/developers/docs/interactions/message-components#text-inputs
142pub const TEXT_INPUT_LENGTH_MIN: usize = 1;
143
144/// Maximum length of a [`TextInput::placeholder`] in codepoints.
145///
146/// This is based on [Discord Docs/Text Inputs].
147///
148/// [Discord Docs/Text Inputs]: https://discord.com/developers/docs/interactions/message-components#text-inputs
149pub const TEXT_INPUT_PLACEHOLDER_MAX: usize = 100;
150
151/// A provided [`Component`] is invalid.
152///
153/// While multiple components may be invalid, validation will short-circuit on
154/// the first invalid component.
155#[derive(Debug)]
156pub struct ComponentValidationError {
157    /// Type of error that occurred.
158    kind: ComponentValidationErrorType,
159}
160
161impl ComponentValidationError {
162    /// Immutable reference to the type of error that occurred.
163    #[must_use = "retrieving the type has no effect if left unused"]
164    pub const fn kind(&self) -> &ComponentValidationErrorType {
165        &self.kind
166    }
167
168    /// Consume the error, returning the source error if there is any.
169    #[allow(clippy::unused_self)]
170    #[must_use = "consuming the error and retrieving the source has no effect if left unused"]
171    pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
172        None
173    }
174
175    /// Consume the error, returning the owned error type and the source error.
176    #[must_use = "consuming the error into its parts has no effect if left unused"]
177    pub fn into_parts(
178        self,
179    ) -> (
180        ComponentValidationErrorType,
181        Option<Box<dyn Error + Send + Sync>>,
182    ) {
183        (self.kind, None)
184    }
185}
186
187impl Display for ComponentValidationError {
188    #[allow(clippy::too_many_lines)]
189    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
190        match &self.kind {
191            ComponentValidationErrorType::ActionRowComponentCount { count } => {
192                f.write_str("an action row has ")?;
193                Display::fmt(&count, f)?;
194                f.write_str(" children, but the max is ")?;
195
196                Display::fmt(&ACTION_ROW_COMPONENT_COUNT, f)
197            }
198            ComponentValidationErrorType::ButtonConflict => {
199                f.write_str("button has both a custom id and url, which is never valid")
200            }
201            ComponentValidationErrorType::ButtonStyle { style } => {
202                f.write_str("button has a type of ")?;
203                Debug::fmt(style, f)?;
204                f.write_str(", which must have a ")?;
205
206                f.write_str(if *style == ButtonStyle::Link {
207                    "url"
208                } else {
209                    "custom id"
210                })?;
211
212                f.write_str(" configured")
213            }
214            ComponentValidationErrorType::ComponentCount { count } => {
215                Display::fmt(count, f)?;
216                f.write_str(" components were provided, but the max is ")?;
217
218                Display::fmt(&COMPONENT_COUNT, f)
219            }
220            ComponentValidationErrorType::ComponentCustomIdLength { chars } => {
221                f.write_str("a component's custom id is ")?;
222                Display::fmt(&chars, f)?;
223                f.write_str(" characters long, but the max is ")?;
224
225                Display::fmt(&COMPONENT_CUSTOM_ID_LENGTH, f)
226            }
227            ComponentValidationErrorType::ComponentLabelLength { chars } => {
228                f.write_str("a component's label is ")?;
229                Display::fmt(&chars, f)?;
230                f.write_str(" characters long, but the max is ")?;
231
232                Display::fmt(&COMPONENT_BUTTON_LABEL_LENGTH, f)
233            }
234            ComponentValidationErrorType::InvalidChildComponent { kind } => {
235                f.write_str("a '")?;
236                Display::fmt(&kind, f)?;
237
238                f.write_str(" component was provided, but can not be a child component")
239            }
240            ComponentValidationErrorType::InvalidRootComponent { kind } => {
241                f.write_str("a '")?;
242                Display::fmt(kind, f)?;
243
244                f.write_str("' component was provided, but can not be a root component")
245            }
246            ComponentValidationErrorType::SelectMaximumValuesCount { count } => {
247                f.write_str("maximum number of values that can be chosen is ")?;
248                Display::fmt(count, f)?;
249                f.write_str(", but must be greater than or equal to ")?;
250                Display::fmt(&SELECT_MAXIMUM_VALUES_REQUIREMENT, f)?;
251                f.write_str("and less than or equal to ")?;
252
253                Display::fmt(&SELECT_MAXIMUM_VALUES_LIMIT, f)
254            }
255            ComponentValidationErrorType::SelectMinimumValuesCount { count } => {
256                f.write_str("maximum number of values that must be chosen is ")?;
257                Display::fmt(count, f)?;
258                f.write_str(", but must be less than or equal to ")?;
259
260                Display::fmt(&SELECT_MAXIMUM_VALUES_LIMIT, f)
261            }
262            ComponentValidationErrorType::SelectNotEnoughDefaultValues { provided, min } => {
263                f.write_str("a select menu provided ")?;
264                Display::fmt(provided, f)?;
265                f.write_str(" values, but it requires at least ")?;
266                Display::fmt(min, f)?;
267                f.write_str(" values")
268            }
269            ComponentValidationErrorType::SelectOptionsMissing => {
270                f.write_str("a text select menu doesn't specify the required options field")
271            }
272            ComponentValidationErrorType::SelectOptionDescriptionLength { chars } => {
273                f.write_str("a select menu option's description is ")?;
274                Display::fmt(&chars, f)?;
275                f.write_str(" characters long, but the max is ")?;
276
277                Display::fmt(&SELECT_OPTION_DESCRIPTION_LENGTH, f)
278            }
279            ComponentValidationErrorType::SelectOptionLabelLength { chars } => {
280                f.write_str("a select menu option's label is ")?;
281                Display::fmt(&chars, f)?;
282                f.write_str(" characters long, but the max is ")?;
283
284                Display::fmt(&SELECT_OPTION_LABEL_LENGTH, f)
285            }
286            ComponentValidationErrorType::SelectOptionValueLength { chars } => {
287                f.write_str("a select menu option's value is ")?;
288                Display::fmt(&chars, f)?;
289                f.write_str(" characters long, but the max is ")?;
290
291                Display::fmt(&SELECT_OPTION_VALUE_LENGTH, f)
292            }
293            ComponentValidationErrorType::SelectPlaceholderLength { chars } => {
294                f.write_str("a select menu's placeholder is ")?;
295                Display::fmt(&chars, f)?;
296                f.write_str(" characters long, but the max is ")?;
297
298                Display::fmt(&SELECT_PLACEHOLDER_LENGTH, f)
299            }
300            ComponentValidationErrorType::SelectOptionCount { count } => {
301                f.write_str("a select menu has ")?;
302                Display::fmt(&count, f)?;
303                f.write_str(" options, but the max is ")?;
304
305                Display::fmt(&SELECT_OPTION_COUNT, f)
306            }
307            ComponentValidationErrorType::SelectTooManyDefaultValues { provided, max } => {
308                f.write_str("a select menu provided ")?;
309                Display::fmt(provided, f)?;
310                f.write_str(" values, but it allows at most ")?;
311                Display::fmt(max, f)?;
312                f.write_str(" values")
313            }
314            ComponentValidationErrorType::SelectUnsupportedDefaultValues { kind } => {
315                f.write_str("a select menu has defined default_values, but its type, ")?;
316                Debug::fmt(kind, f)?;
317                f.write_str(", does not support them")
318            }
319            ComponentValidationErrorType::TextInputLabelLength { len: count } => {
320                f.write_str("a text input label length is ")?;
321                Display::fmt(count, f)?;
322                f.write_str(", but it must be at least ")?;
323                Display::fmt(&TEXT_INPUT_LABEL_MIN, f)?;
324                f.write_str(" and at most ")?;
325
326                Display::fmt(&TEXT_INPUT_LABEL_MAX, f)
327            }
328            ComponentValidationErrorType::TextInputMaxLength { len: count } => {
329                f.write_str("a text input max length is ")?;
330                Display::fmt(count, f)?;
331                f.write_str(", but it must be at least ")?;
332                Display::fmt(&TEXT_INPUT_LENGTH_MIN, f)?;
333                f.write_str(" and at most ")?;
334
335                Display::fmt(&TEXT_INPUT_LENGTH_MAX, f)
336            }
337            ComponentValidationErrorType::TextInputMinLength { len: count } => {
338                f.write_str("a text input min length is ")?;
339                Display::fmt(count, f)?;
340                f.write_str(", but it must be at most ")?;
341
342                Display::fmt(&TEXT_INPUT_LENGTH_MAX, f)
343            }
344            ComponentValidationErrorType::TextInputPlaceholderLength { chars } => {
345                f.write_str("a text input's placeholder is ")?;
346                Display::fmt(&chars, f)?;
347                f.write_str(" characters long, but the max is ")?;
348
349                Display::fmt(&TEXT_INPUT_PLACEHOLDER_MAX, f)
350            }
351            ComponentValidationErrorType::TextInputValueLength { chars } => {
352                f.write_str("a text input's value is ")?;
353                Display::fmt(&chars, f)?;
354                f.write_str(" characters long, but the max is ")?;
355
356                Display::fmt(&TEXT_INPUT_PLACEHOLDER_MAX, f)
357            }
358        }
359    }
360}
361
362impl Error for ComponentValidationError {}
363
364/// Type of [`ComponentValidationError`] that occurred.
365#[derive(Debug)]
366#[non_exhaustive]
367pub enum ComponentValidationErrorType {
368    /// Number of components a provided [`ActionRow`] is larger than
369    /// [the maximum][`ACTION_ROW_COMPONENT_COUNT`].
370    ActionRowComponentCount {
371        /// Number of components within the action row.
372        count: usize,
373    },
374    /// Button has both a custom ID and URL set.
375    ButtonConflict,
376    /// Button does not have the required field based on its style.
377    ///
378    /// A button with a style of [`ButtonStyle::Link`] must have a URL set,
379    /// while buttons of other styles must have a custom ID set.
380    ButtonStyle {
381        /// Style of the button.
382        style: ButtonStyle,
383    },
384    /// Number of components provided is larger than
385    /// [the maximum][`COMPONENT_COUNT`].
386    ComponentCount {
387        /// Number of components that were provided.
388        count: usize,
389    },
390    /// Component custom ID is larger than the
391    /// [the maximum][`COMPONENT_CUSTOM_ID_LENGTH`].
392    ComponentCustomIdLength {
393        /// Number of codepoints that were provided.
394        chars: usize,
395    },
396    /// Component label is larger than [the maximum][`COMPONENT_BUTTON_LABEL_LENGTH`].
397    ComponentLabelLength {
398        /// Number of codepoints that were provided.
399        chars: usize,
400    },
401    /// Provided component cannot be a child component.
402    InvalidChildComponent {
403        /// Type of provided component.
404        kind: ComponentType,
405    },
406    /// Provided component cannot be a root component.
407    InvalidRootComponent {
408        /// Type of provided component.
409        kind: ComponentType,
410    },
411    /// Maximum number of items that can be chosen is smaller than
412    /// [the minimum][`SELECT_MAXIMUM_VALUES_REQUIREMENT`] or larger than
413    /// [the maximum][`SELECT_MAXIMUM_VALUES_LIMIT`].
414    SelectMaximumValuesCount {
415        /// Number of options that were provided.
416        count: usize,
417    },
418    /// Minimum number of items that must be chosen is larger than
419    /// [the maximum][`SELECT_MINIMUM_VALUES_LIMIT`].
420    SelectMinimumValuesCount {
421        /// Number of options that were provided.
422        count: usize,
423    },
424    /// The select menu specifies less default values than its own minimum values requirement.
425    SelectNotEnoughDefaultValues {
426        /// Number of default values provided.
427        provided: usize,
428        /// Select menu's minimum number of default values.
429        min: usize,
430    },
431    /// The `options` field is `None` for a [text select menu][text-select].
432    ///
433    /// [text-select]: SelectMenuType::Text
434    SelectOptionsMissing,
435    /// Number of select menu options provided is larger than
436    /// [the maximum][`SELECT_OPTION_COUNT`].
437    SelectOptionCount {
438        /// Number of options that were provided.
439        count: usize,
440    },
441    /// Description of a select menu option is larger than
442    /// [the maximum][`SELECT_OPTION_DESCRIPTION_LENGTH`].
443    SelectOptionDescriptionLength {
444        /// Number of codepoints that were provided.
445        chars: usize,
446    },
447    /// Label of a select menu option is larger than
448    /// [the maximum][`SELECT_OPTION_LABEL_LENGTH`].
449    SelectOptionLabelLength {
450        /// Number of codepoints that were provided.
451        chars: usize,
452    },
453    /// Value of a select menu option is larger than
454    /// [the maximum][`SELECT_OPTION_VALUE_LENGTH`].
455    SelectOptionValueLength {
456        /// Number of codepoints that were provided.
457        chars: usize,
458    },
459    /// Placeholder of a component is larger than the
460    /// [maximum][`SELECT_PLACEHOLDER_LENGTH`].
461    SelectPlaceholderLength {
462        /// Number of codepoints that were provided.
463        chars: usize,
464    },
465    /// The select menu specifies less default values than its own minimum values requirement.
466    SelectTooManyDefaultValues {
467        /// Number of default values provided.
468        provided: usize,
469        /// Select menu's maximum number of values.
470        max: usize,
471    },
472    /// The select menu type doesn't support the `default_values` field.
473    SelectUnsupportedDefaultValues {
474        /// The select menu's type.
475        kind: SelectMenuType,
476    },
477    /// [`TextInput::label`] is invalid.
478    TextInputLabelLength {
479        /// Provided length.
480        len: usize,
481    },
482    /// [`TextInput::max_length`] is invalid.
483    TextInputMaxLength {
484        /// Provided length.
485        len: usize,
486    },
487    /// [`TextInput::min_length`] is too long.
488    TextInputMinLength {
489        /// Provided length.
490        len: usize,
491    },
492    /// Placeholder of a [`TextInput`] component is larger than
493    /// [`TEXT_INPUT_PLACEHOLDER_MAX`].
494    TextInputPlaceholderLength {
495        /// Provided number of codepoints.
496        chars: usize,
497    },
498    /// Value of a [`TextInput`] component is larger than
499    /// [`TEXT_INPUT_LENGTH_MAX`].
500    TextInputValueLength {
501        /// Provided number of codepoints.
502        chars: usize,
503    },
504}
505
506/// Ensure that a top-level request component is correct.
507///
508/// Intended to ensure that a fully formed top-level component for requests
509/// is an action row.
510///
511/// Refer to other validators like [`button`] if you need to validate other
512/// components.
513///
514/// # Errors
515///
516/// Returns an error of type [`InvalidRootComponent`] if the component is not an
517/// [`ActionRow`].
518///
519/// Refer to [`action_row`] for potential errors when validating an action row
520/// component.
521///
522/// [`InvalidRootComponent`]: ComponentValidationErrorType::InvalidRootComponent
523pub fn component(component: &Component) -> Result<(), ComponentValidationError> {
524    match component {
525        Component::ActionRow(action_row) => self::action_row(action_row)?,
526        other => {
527            return Err(ComponentValidationError {
528                kind: ComponentValidationErrorType::InvalidRootComponent { kind: other.kind() },
529            });
530        }
531    }
532
533    Ok(())
534}
535
536/// Ensure that an action row is correct.
537///
538/// # Errors
539///
540/// Returns an error of type [`ActionRowComponentCount`] if the action row has
541/// too many components in it.
542///
543/// Returns an error of type [`InvalidChildComponent`] if the provided nested
544/// component is an [`ActionRow`]. Action rows can not contain another action
545/// row.
546///
547/// Refer to [`button`] for potential errors when validating a button in the
548/// action row.
549///
550/// Refer to [`select_menu`] for potential errors when validating a select menu
551/// in the action row.
552///
553/// Refer to [`text_input`] for potential errors when validating a text input in
554/// the action row.
555///
556/// [`ActionRowComponentCount`]: ComponentValidationErrorType::ActionRowComponentCount
557/// [`InvalidChildComponent`]: ComponentValidationErrorType::InvalidChildComponent
558pub fn action_row(action_row: &ActionRow) -> Result<(), ComponentValidationError> {
559    self::component_action_row_components(&action_row.components)?;
560
561    for component in &action_row.components {
562        match component {
563            Component::ActionRow(_) => {
564                return Err(ComponentValidationError {
565                    kind: ComponentValidationErrorType::InvalidChildComponent {
566                        kind: ComponentType::ActionRow,
567                    },
568                });
569            }
570            Component::Button(button) => self::button(button)?,
571            Component::SelectMenu(select_menu) => self::select_menu(select_menu)?,
572            Component::TextInput(text_input) => self::text_input(text_input)?,
573            Component::Unknown(unknown) => {
574                return Err(ComponentValidationError {
575                    kind: ComponentValidationErrorType::InvalidChildComponent {
576                        kind: ComponentType::Unknown(*unknown),
577                    },
578                })
579            }
580        }
581    }
582
583    Ok(())
584}
585
586/// Ensure that a button is correct.
587///
588/// # Errors
589///
590/// Returns an error of type [`ButtonConflict`] if both a custom ID and URL are
591/// specified.
592///
593/// Returns an error of type
594/// [`ButtonStyle`][`ComponentValidationErrorType::ButtonStyle`] if
595/// [`ButtonStyle::Link`] is provided and a URL is provided, or if the style is
596/// not [`ButtonStyle::Link`] and a custom ID is not provided.
597///
598/// Returns an error of type [`ComponentCustomIdLength`] if the provided custom
599/// ID is too long.
600///
601/// Returns an error of type [`ComponentLabelLength`] if the provided button
602/// label is too long.
603///
604/// [`ButtonConflict`]: ComponentValidationErrorType::ButtonConflict
605/// [`ComponentCustomIdLength`]: ComponentValidationErrorType::ComponentCustomIdLength
606/// [`ComponentLabelLength`]: ComponentValidationErrorType::ComponentLabelLength
607pub fn button(button: &Button) -> Result<(), ComponentValidationError> {
608    let has_custom_id = button.custom_id.is_some();
609    let has_url = button.url.is_some();
610
611    // First check if a custom ID and URL are both set. If so this
612    // results in a conflict, as no valid button may have both set.
613    if has_custom_id && has_url {
614        return Err(ComponentValidationError {
615            kind: ComponentValidationErrorType::ButtonConflict,
616        });
617    }
618
619    // Next, we check if the button is a link and a URL is not set.
620    //
621    // Lastly, we check if the button is not a link and a custom ID is
622    // not set.
623    let is_link = button.style == ButtonStyle::Link;
624
625    if (is_link && !has_url) || (!is_link && !has_custom_id) {
626        return Err(ComponentValidationError {
627            kind: ComponentValidationErrorType::ButtonStyle {
628                style: button.style,
629            },
630        });
631    }
632
633    if let Some(custom_id) = button.custom_id.as_ref() {
634        self::component_custom_id(custom_id)?;
635    }
636
637    if let Some(label) = button.label.as_ref() {
638        self::component_button_label(label)?;
639    }
640
641    Ok(())
642}
643
644/// Ensure that a select menu is correct.
645///
646/// # Errors
647///
648/// Returns an error of type [`ComponentCustomIdLength`] if the provided custom
649/// ID is too long.
650///
651/// Returns an error of type [`ComponentLabelLength`] if the provided button
652/// label is too long.
653///
654/// Returns an error of type [`SelectMaximumValuesCount`] if the provided number
655/// of select menu values that can be chosen is smaller than the minimum or
656/// larger than the maximum.
657///
658/// Returns an error of type [`SelectMinimumValuesCount`] if the provided number
659/// of select menu values that must be chosen is larger than the maximum.
660///
661/// Returns an error of type [`SelectOptionDescriptionLength`] if a provided
662/// select option description is too long.
663///
664/// Returns an error of type [`SelectOptionLabelLength`] if a provided select
665/// option label is too long.
666///
667/// Returns an error of type [`SelectOptionValueLength`] error type if
668/// a provided select option value is too long.
669///
670/// Returns an error of type [`SelectPlaceholderLength`] if a provided select
671/// placeholder is too long.
672///
673/// Returns an error of type [`SelectUnsupportedDefaultValues`] if the select menu's type doesn't
674/// support the `default_values` field.
675///
676/// Returns an error of type [`SelectNotEnoughDefaultValues`] if the select menu specifies fewer
677/// default values than its minimum values property.
678///
679/// Returns an error of type [`SelectTooManyDefaultValues`] if the select menu specifies more
680/// default values than its maximum values property.
681///
682/// [`ComponentCustomIdLength`]: ComponentValidationErrorType::ComponentCustomIdLength
683/// [`ComponentLabelLength`]: ComponentValidationErrorType::ComponentLabelLength
684/// [`SelectMaximumValuesCount`]: ComponentValidationErrorType::SelectMaximumValuesCount
685/// [`SelectMinimumValuesCount`]: ComponentValidationErrorType::SelectMinimumValuesCount
686/// [`SelectOptionDescriptionLength`]: ComponentValidationErrorType::SelectOptionDescriptionLength
687/// [`SelectOptionLabelLength`]: ComponentValidationErrorType::SelectOptionLabelLength
688/// [`SelectOptionValueLength`]: ComponentValidationErrorType::SelectOptionValueLength
689/// [`SelectPlaceholderLength`]: ComponentValidationErrorType::SelectPlaceholderLength
690/// [`SelectUnsupportedDefaultValues`]: ComponentValidationErrorType::SelectUnsupportedDefaultValues
691/// [`SelectNotEnoughDefaultValues`]: ComponentValidationErrorType::SelectNotEnoughDefaultValues
692/// [`SelectTooManyDefaultValues`]: ComponentValidationErrorType::SelectTooManyDefaultValues
693pub fn select_menu(select_menu: &SelectMenu) -> Result<(), ComponentValidationError> {
694    self::component_custom_id(&select_menu.custom_id)?;
695
696    // There aren't any requirements for channel_types that we could validate here
697    if let SelectMenuType::Text = &select_menu.kind {
698        let options = select_menu
699            .options
700            .as_ref()
701            .ok_or(ComponentValidationError {
702                kind: ComponentValidationErrorType::SelectOptionsMissing,
703            })?;
704        for option in options {
705            component_select_option_label(&option.label)?;
706            component_select_option_value(&option.value)?;
707
708            if let Some(description) = option.description.as_ref() {
709                component_option_description(description)?;
710            }
711        }
712        component_select_options(options)?;
713    }
714
715    if let Some(placeholder) = select_menu.placeholder.as_ref() {
716        self::component_select_placeholder(placeholder)?;
717    }
718
719    if let Some(max_values) = select_menu.max_values {
720        self::component_select_max_values(usize::from(max_values))?;
721    }
722
723    if let Some(min_values) = select_menu.min_values {
724        self::component_select_min_values(usize::from(min_values))?;
725    }
726
727    if let Some(default_values) = select_menu.default_values.as_ref() {
728        component_select_default_values_supported(select_menu.kind)?;
729        component_select_default_values_count(
730            select_menu.min_values,
731            select_menu.max_values,
732            default_values.len(),
733        )?;
734    }
735
736    Ok(())
737}
738
739/// Ensure that a text input is correct.
740///
741/// # Errors
742///
743/// Returns an error of type [`ComponentCustomIdLength`] if the provided custom
744/// ID is too long.
745///
746/// Returns an error of type [`ComponentLabelLength`] if the provided button
747/// label is too long.
748///
749/// Returns an error of type [`TextInputMaxLength`] if the length is invalid.
750///
751/// Returns an error of type [`TextInputMinLength`] if the length is invalid.
752///
753/// Returns an error of type [`TextInputPlaceholderLength`] if the provided
754/// placeholder is too long.
755///
756/// Returns an error of type [`TextInputValueLength`] if the length is invalid.
757///
758/// [`ComponentCustomIdLength`]: ComponentValidationErrorType::ComponentCustomIdLength
759/// [`ComponentLabelLength`]: ComponentValidationErrorType::ComponentLabelLength
760/// [`TextInputMaxLength`]: ComponentValidationErrorType::TextInputMaxLength
761/// [`TextInputMinLength`]: ComponentValidationErrorType::TextInputMinLength
762/// [`TextInputPlaceholderLength`]: ComponentValidationErrorType::TextInputPlaceholderLength
763/// [`TextInputValueLength`]: ComponentValidationErrorType::TextInputValueLength
764pub fn text_input(text_input: &TextInput) -> Result<(), ComponentValidationError> {
765    self::component_custom_id(&text_input.custom_id)?;
766    self::component_text_input_label(&text_input.label)?;
767
768    if let Some(max_length) = text_input.max_length {
769        self::component_text_input_max(max_length)?;
770    }
771
772    if let Some(min_length) = text_input.min_length {
773        self::component_text_input_min(min_length)?;
774    }
775
776    if let Some(placeholder) = text_input.placeholder.as_ref() {
777        self::component_text_input_placeholder(placeholder)?;
778    }
779
780    if let Some(value) = text_input.value.as_ref() {
781        self::component_text_input_value(value)?;
782    }
783
784    Ok(())
785}
786
787/// Validate that an [`ActionRow`] does not contain too many components.
788///
789/// [`ActionRow`]s may only have so many components within it, defined by
790/// [`ACTION_ROW_COMPONENT_COUNT`].
791///
792/// # Errors
793///
794/// Returns an error of type [`ActionRowComponentCount`] if the provided list of
795/// components is too many for an [`ActionRow`].
796///
797/// [`ActionRowComponentCount`]: ComponentValidationErrorType::ActionRowComponentCount
798/// [`ActionRow`]: twilight_model::application::component::ActionRow
799const fn component_action_row_components(
800    components: &[Component],
801) -> Result<(), ComponentValidationError> {
802    let count = components.len();
803
804    if count > COMPONENT_COUNT {
805        return Err(ComponentValidationError {
806            kind: ComponentValidationErrorType::ActionRowComponentCount { count },
807        });
808    }
809
810    Ok(())
811}
812
813/// Validate that a [`Component`]'s label is not too long.
814///
815/// # Errors
816///
817/// Returns an error of type [`ComponentLabelLength`] if the provided component
818/// label is too long.
819///
820/// [`ComponentLabelLength`]: ComponentValidationErrorType::ComponentLabelLength
821fn component_button_label(label: impl AsRef<str>) -> Result<(), ComponentValidationError> {
822    let chars = label.as_ref().chars().count();
823
824    if chars > COMPONENT_BUTTON_LABEL_LENGTH {
825        return Err(ComponentValidationError {
826            kind: ComponentValidationErrorType::ComponentLabelLength { chars },
827        });
828    }
829
830    Ok(())
831}
832
833/// Validate that a custom ID is not too long.
834///
835/// # Errors
836///
837/// Returns an error of type [`ComponentCustomIdLength`] if the provided custom
838/// ID is too long.
839///
840/// [`ComponentCustomIdLength`]: ComponentValidationErrorType::ComponentCustomIdLength
841fn component_custom_id(custom_id: impl AsRef<str>) -> Result<(), ComponentValidationError> {
842    let chars = custom_id.as_ref().chars().count();
843
844    if chars > COMPONENT_CUSTOM_ID_LENGTH {
845        return Err(ComponentValidationError {
846            kind: ComponentValidationErrorType::ComponentCustomIdLength { chars },
847        });
848    }
849
850    Ok(())
851}
852
853/// Validate a [`SelectMenuOption::description`]'s length.
854///
855/// # Errors
856///
857/// Returns an error of type [`SelectOptionDescriptionLength`] if the provided
858/// select option description is too long.
859///
860/// [`SelectMenuOption::description`]: twilight_model::application::component::select_menu::SelectMenuOption::description
861/// [`SelectOptionDescriptionLength`]: ComponentValidationErrorType::SelectOptionDescriptionLength
862fn component_option_description(
863    description: impl AsRef<str>,
864) -> Result<(), ComponentValidationError> {
865    let chars = description.as_ref().chars().count();
866
867    if chars > SELECT_OPTION_DESCRIPTION_LENGTH {
868        return Err(ComponentValidationError {
869            kind: ComponentValidationErrorType::SelectOptionDescriptionLength { chars },
870        });
871    }
872
873    Ok(())
874}
875
876/// Validate a [`SelectMenuType`] supports the `default_values` field.
877///
878/// # Errors
879///
880/// Returns an error of type [`SelectUnsupportedDefaultValues`] if the provided component type
881/// doesn't support the `default_values` field.
882const fn component_select_default_values_supported(
883    menu_type: SelectMenuType,
884) -> Result<(), ComponentValidationError> {
885    if !matches!(
886        menu_type,
887        SelectMenuType::User
888            | SelectMenuType::Role
889            | SelectMenuType::Mentionable
890            | SelectMenuType::Channel
891    ) {
892        return Err(ComponentValidationError {
893            kind: ComponentValidationErrorType::SelectUnsupportedDefaultValues { kind: menu_type },
894        });
895    }
896
897    Ok(())
898}
899
900/// Validate a [`SelectMenu`]'s `default_values` field has the correct number of values.
901///
902/// # Errors
903///
904/// Returns an error of the type [`SelectTooManyDefaultValues`] if the provided list of default
905/// values exceeds the provided `max_values` (if present).
906///
907/// Alternatively, this returns an error of the type [`SelectNotEnoughDefaultValues`] if the
908/// provided list of default values doesn't meet the provided `min_values` requirement (if present).
909const fn component_select_default_values_count(
910    min_values: Option<u8>,
911    max_values: Option<u8>,
912    default_values: usize,
913) -> Result<(), ComponentValidationError> {
914    if let Some(min) = min_values {
915        let min = min as usize;
916        if default_values < min {
917            return Err(ComponentValidationError {
918                kind: ComponentValidationErrorType::SelectNotEnoughDefaultValues {
919                    provided: default_values,
920                    min,
921                },
922            });
923        }
924    }
925    if let Some(max) = max_values {
926        let max = max as usize;
927        if default_values > max {
928            return Err(ComponentValidationError {
929                kind: ComponentValidationErrorType::SelectTooManyDefaultValues {
930                    provided: default_values,
931                    max,
932                },
933            });
934        }
935    }
936
937    Ok(())
938}
939
940/// Validate a [`SelectMenu::max_values`] amount.
941///
942/// # Errors
943///
944/// Returns an error of type [`SelectMaximumValuesCount`] if the provided number
945/// of values that can be chosen is smaller than
946/// [the minimum][`SELECT_MAXIMUM_VALUES_REQUIREMENT`] or larger than
947/// [the maximum][`SELECT_MAXIMUM_VALUES_LIMIT`].
948///
949/// [`SelectMenu::max_values`]: twilight_model::application::component::select_menu::SelectMenu::max_values
950/// [`SelectMaximumValuesCount`]: ComponentValidationErrorType::SelectMaximumValuesCount
951const fn component_select_max_values(count: usize) -> Result<(), ComponentValidationError> {
952    if count > SELECT_MAXIMUM_VALUES_LIMIT {
953        return Err(ComponentValidationError {
954            kind: ComponentValidationErrorType::SelectMaximumValuesCount { count },
955        });
956    }
957
958    if count < SELECT_MAXIMUM_VALUES_REQUIREMENT {
959        return Err(ComponentValidationError {
960            kind: ComponentValidationErrorType::SelectMaximumValuesCount { count },
961        });
962    }
963
964    Ok(())
965}
966
967/// Validate a [`SelectMenu::min_values`] amount.
968///
969/// # Errors
970///
971/// Returns an error of type [`SelectMinimumValuesCount`] if the provided number
972/// of values that must be chosen is larger than
973/// [the maximum][`SELECT_MINIMUM_VALUES_LIMIT`].
974///
975/// [`SelectMenu::min_values`]: twilight_model::application::component::select_menu::SelectMenu::min_values
976/// [`SelectMinimumValuesCount`]: ComponentValidationErrorType::SelectMinimumValuesCount
977const fn component_select_min_values(count: usize) -> Result<(), ComponentValidationError> {
978    if count > SELECT_MINIMUM_VALUES_LIMIT {
979        return Err(ComponentValidationError {
980            kind: ComponentValidationErrorType::SelectMinimumValuesCount { count },
981        });
982    }
983
984    Ok(())
985}
986
987/// Validate a [`SelectMenuOption::label`]'s length.
988///
989/// # Errors
990///
991/// Returns an error of type [`SelectOptionLabelLength`] if the provided select
992/// option label is too long.
993///
994/// [`SelectMenuOption::label`]: twilight_model::application::component::select_menu::SelectMenuOption::label
995/// [`SelectOptionLabelLength`]: ComponentValidationErrorType::SelectOptionLabelLength
996fn component_select_option_label(label: impl AsRef<str>) -> Result<(), ComponentValidationError> {
997    let chars = label.as_ref().chars().count();
998
999    if chars > SELECT_OPTION_LABEL_LENGTH {
1000        return Err(ComponentValidationError {
1001            kind: ComponentValidationErrorType::SelectOptionLabelLength { chars },
1002        });
1003    }
1004
1005    Ok(())
1006}
1007
1008/// Validate a [`SelectMenuOption::value`]'s length.
1009///
1010/// # Errors
1011///
1012/// Returns an error of type [`SelectOptionValueLength`] if the provided select
1013/// option value is too long.
1014///
1015/// [`SelectMenuOption::value`]: twilight_model::application::component::select_menu::SelectMenuOption::value
1016/// [`SelectOptionValueLength`]: ComponentValidationErrorType::SelectOptionValueLength
1017fn component_select_option_value(value: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1018    let chars = value.as_ref().chars().count();
1019
1020    if chars > SELECT_OPTION_VALUE_LENGTH {
1021        return Err(ComponentValidationError {
1022            kind: ComponentValidationErrorType::SelectOptionValueLength { chars },
1023        });
1024    }
1025
1026    Ok(())
1027}
1028
1029/// Validate a [`SelectMenu`]s number of [`options`].
1030///
1031/// [`Component::SelectMenu`]s may only have so many options within it, defined
1032/// by [`SELECT_OPTION_COUNT`].
1033///
1034/// # Errors
1035///
1036/// Returns an error of type [`SelectOptionCount`] if the provided list of
1037/// [`SelectMenuOption`]s is too many for a [`SelectMenu`].
1038///
1039/// [`SelectMenu::options`]: twilight_model::application::component::select_menu::SelectMenu::options
1040/// [`SelectMenuOption`]: twilight_model::application::component::select_menu::SelectMenuOption
1041/// [`SelectMenu`]: twilight_model::application::component::select_menu::SelectMenu
1042/// [`SelectOptionCount`]: ComponentValidationErrorType::SelectOptionCount
1043const fn component_select_options(
1044    options: &[SelectMenuOption],
1045) -> Result<(), ComponentValidationError> {
1046    let count = options.len();
1047
1048    if count > SELECT_OPTION_COUNT {
1049        return Err(ComponentValidationError {
1050            kind: ComponentValidationErrorType::SelectOptionCount { count },
1051        });
1052    }
1053
1054    Ok(())
1055}
1056
1057/// Validate a [`SelectMenu::placeholder`]'s length.
1058///
1059/// # Errors
1060///
1061/// Returns an error of type [`SelectPlaceholderLength`] if the provided select
1062/// placeholder is too long.
1063///
1064/// [`SelectMenu::placeholder`]: twilight_model::application::component::select_menu::SelectMenu::placeholder
1065/// [`SelectPlaceholderLength`]: ComponentValidationErrorType::SelectPlaceHolderLength
1066fn component_select_placeholder(
1067    placeholder: impl AsRef<str>,
1068) -> Result<(), ComponentValidationError> {
1069    let chars = placeholder.as_ref().chars().count();
1070
1071    if chars > SELECT_PLACEHOLDER_LENGTH {
1072        return Err(ComponentValidationError {
1073            kind: ComponentValidationErrorType::SelectPlaceholderLength { chars },
1074        });
1075    }
1076
1077    Ok(())
1078}
1079
1080/// Ensure a [`TextInput::label`]'s length is correct.
1081///
1082/// The length must be at most [`TEXT_INPUT_LABEL_MAX`].
1083///
1084/// # Errors
1085///
1086/// Returns an error of type [`TextInputLabelLength`] if the provided
1087/// label is too long.
1088///
1089/// [`TextInput::label`]: twilight_model::application::component::text_input::TextInput::label
1090/// [`TextInputLabelLength`]: ComponentValidationErrorType::TextInputLabelLength
1091fn component_text_input_label(label: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1092    let len = label.as_ref().len();
1093
1094    if (TEXT_INPUT_LABEL_MIN..=TEXT_INPUT_LABEL_MAX).contains(&len) {
1095        Ok(())
1096    } else {
1097        Err(ComponentValidationError {
1098            kind: ComponentValidationErrorType::TextInputLabelLength { len },
1099        })
1100    }
1101}
1102
1103/// Ensure a [`TextInput::max_length`]'s value is correct.
1104///
1105/// # Errors
1106///
1107/// Returns an error of type [`TextInputMaxLength`] if the length is invalid.
1108///
1109/// [`TextInput::max_length`]: twilight_model::application::component::text_input::TextInput::max_length
1110/// [`TextInputMaxLength`]: ComponentValidationErrorType::TextInputMaxLength
1111const fn component_text_input_max(len: u16) -> Result<(), ComponentValidationError> {
1112    let len = len as usize;
1113
1114    if len >= TEXT_INPUT_LENGTH_MIN && len <= TEXT_INPUT_LENGTH_MAX {
1115        Ok(())
1116    } else {
1117        Err(ComponentValidationError {
1118            kind: ComponentValidationErrorType::TextInputMaxLength { len },
1119        })
1120    }
1121}
1122
1123/// Ensure a [`TextInput::min_length`]'s value is correct.
1124///
1125/// # Errors
1126///
1127/// Returns an error of type [`TextInputMinLength`] if the length is invalid.
1128///
1129/// [`TextInput::min_length`]: twilight_model::application::component::text_input::TextInput::min_length
1130/// [`TextInputMinLength`]: ComponentValidationErrorType::TextInputMinLength
1131const fn component_text_input_min(len: u16) -> Result<(), ComponentValidationError> {
1132    let len = len as usize;
1133
1134    if len <= TEXT_INPUT_LENGTH_MAX {
1135        Ok(())
1136    } else {
1137        Err(ComponentValidationError {
1138            kind: ComponentValidationErrorType::TextInputMinLength { len },
1139        })
1140    }
1141}
1142
1143/// Ensure a [`TextInput::placeholder`]'s length is correct.
1144///
1145/// The length must be at most [`TEXT_INPUT_PLACEHOLDER_MAX`].
1146///
1147/// # Errors
1148///
1149/// Returns an error of type [`TextInputPlaceholderLength`] if the provided
1150/// placeholder is too long.
1151///
1152/// [`TextInput::placeholder`]: twilight_model::application::component::text_input::TextInput::placeholder
1153/// [`TextInputPlaceholderLength`]: ComponentValidationErrorType::TextInputPlaceholderLength
1154fn component_text_input_placeholder(
1155    placeholder: impl AsRef<str>,
1156) -> Result<(), ComponentValidationError> {
1157    let chars = placeholder.as_ref().chars().count();
1158
1159    if chars <= TEXT_INPUT_PLACEHOLDER_MAX {
1160        Ok(())
1161    } else {
1162        Err(ComponentValidationError {
1163            kind: ComponentValidationErrorType::TextInputPlaceholderLength { chars },
1164        })
1165    }
1166}
1167
1168/// Ensure a [`TextInput::value`]'s length is correct.
1169///
1170/// # Errors
1171///
1172/// Returns an error of type [`TextInputValueLength`] if the length is invalid.
1173///
1174/// [`TextInput::value_length`]: twilight_model::application::component::text_input::TextInput::value
1175/// [`TextInputValueLength`]: ComponentValidationErrorType::TextInputValueLength
1176fn component_text_input_value(value: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1177    let chars = value.as_ref().chars().count();
1178
1179    if chars <= TEXT_INPUT_LENGTH_MAX {
1180        Ok(())
1181    } else {
1182        Err(ComponentValidationError {
1183            kind: ComponentValidationErrorType::TextInputValueLength { chars },
1184        })
1185    }
1186}
1187
1188#[allow(clippy::non_ascii_literal)]
1189#[cfg(test)]
1190mod tests {
1191    use super::*;
1192    use static_assertions::{assert_fields, assert_impl_all};
1193    use twilight_model::channel::message::EmojiReactionType;
1194
1195    assert_fields!(ComponentValidationErrorType::ActionRowComponentCount: count);
1196    assert_fields!(ComponentValidationErrorType::ComponentCount: count);
1197    assert_fields!(ComponentValidationErrorType::ComponentCustomIdLength: chars);
1198    assert_fields!(ComponentValidationErrorType::ComponentLabelLength: chars);
1199    assert_fields!(ComponentValidationErrorType::InvalidChildComponent: kind);
1200    assert_fields!(ComponentValidationErrorType::InvalidRootComponent: kind);
1201    assert_fields!(ComponentValidationErrorType::SelectMaximumValuesCount: count);
1202    assert_fields!(ComponentValidationErrorType::SelectMinimumValuesCount: count);
1203    assert_fields!(ComponentValidationErrorType::SelectOptionDescriptionLength: chars);
1204    assert_fields!(ComponentValidationErrorType::SelectOptionLabelLength: chars);
1205    assert_fields!(ComponentValidationErrorType::SelectOptionValueLength: chars);
1206    assert_fields!(ComponentValidationErrorType::SelectPlaceholderLength: chars);
1207    assert_impl_all!(ComponentValidationErrorType: Debug, Send, Sync);
1208    assert_impl_all!(ComponentValidationError: Debug, Send, Sync);
1209
1210    // All styles of buttons.
1211    const ALL_BUTTON_STYLES: &[ButtonStyle] = &[
1212        ButtonStyle::Primary,
1213        ButtonStyle::Secondary,
1214        ButtonStyle::Success,
1215        ButtonStyle::Danger,
1216        ButtonStyle::Link,
1217        ButtonStyle::Premium,
1218    ];
1219
1220    #[test]
1221    fn component_action_row() {
1222        let button = Button {
1223            custom_id: None,
1224            disabled: false,
1225            emoji: Some(EmojiReactionType::Unicode {
1226                name: "📚".into()
1227            }),
1228            label: Some("Read".into()),
1229            style: ButtonStyle::Link,
1230            url: Some("https://abebooks.com".into()),
1231            sku_id: None,
1232        };
1233
1234        let select_menu = SelectMenu {
1235            channel_types: None,
1236            custom_id: "custom id 2".into(),
1237            disabled: false,
1238            default_values: None,
1239            kind: SelectMenuType::Text,
1240            max_values: Some(2),
1241            min_values: Some(1),
1242            options: Some(Vec::from([SelectMenuOption {
1243                default: true,
1244                description: Some("Book 1 of the Expanse".into()),
1245                emoji: None,
1246                label: "Leviathan Wakes".into(),
1247                value: "9780316129084".into(),
1248            }])),
1249            placeholder: Some("Choose a book".into()),
1250        };
1251
1252        let action_row = ActionRow {
1253            components: Vec::from([
1254                Component::SelectMenu(select_menu.clone()),
1255                Component::Button(button),
1256            ]),
1257        };
1258
1259        assert!(component(&Component::ActionRow(action_row.clone())).is_ok());
1260
1261        assert!(component(&Component::SelectMenu(select_menu.clone())).is_err());
1262
1263        assert!(super::action_row(&action_row).is_ok());
1264
1265        let invalid_action_row = Component::ActionRow(ActionRow {
1266            components: Vec::from([
1267                Component::SelectMenu(select_menu.clone()),
1268                Component::SelectMenu(select_menu.clone()),
1269                Component::SelectMenu(select_menu.clone()),
1270                Component::SelectMenu(select_menu.clone()),
1271                Component::SelectMenu(select_menu.clone()),
1272                Component::SelectMenu(select_menu),
1273            ]),
1274        });
1275
1276        assert!(component(&invalid_action_row).is_err());
1277    }
1278
1279    // Test that a button with both a custom ID and URL results in a
1280    // [`ComponentValidationErrorType::ButtonConflict`] error type.
1281    #[test]
1282    fn button_conflict() {
1283        let button = Button {
1284            custom_id: Some("a".to_owned()),
1285            disabled: false,
1286            emoji: None,
1287            label: None,
1288            style: ButtonStyle::Primary,
1289            url: Some("https://twilight.rs".to_owned()),
1290            sku_id: None,
1291        };
1292
1293        assert!(matches!(
1294            super::button(&button),
1295            Err(ComponentValidationError {
1296                kind: ComponentValidationErrorType::ButtonConflict,
1297            }),
1298        ));
1299    }
1300
1301    // Test that all button styles with no custom ID or URL results in a
1302    // [`ComponentValidationErrorType::ButtonStyle`] error type.
1303    #[test]
1304    fn button_style() {
1305        for style in ALL_BUTTON_STYLES {
1306            let button = Button {
1307                custom_id: None,
1308                disabled: false,
1309                emoji: None,
1310                label: Some("some label".to_owned()),
1311                style: *style,
1312                url: None,
1313                sku_id: None,
1314            };
1315
1316            assert!(matches!(
1317                super::button(&button),
1318                Err(ComponentValidationError {
1319                    kind: ComponentValidationErrorType::ButtonStyle {
1320                        style: error_style,
1321                    }
1322                })
1323                if error_style == *style
1324            ));
1325        }
1326    }
1327
1328    #[test]
1329    fn component_label() {
1330        assert!(component_button_label("").is_ok());
1331        assert!(component_button_label("a").is_ok());
1332        assert!(component_button_label("a".repeat(80)).is_ok());
1333
1334        assert!(component_button_label("a".repeat(81)).is_err());
1335    }
1336
1337    #[test]
1338    fn component_custom_id_length() {
1339        assert!(component_custom_id("").is_ok());
1340        assert!(component_custom_id("a").is_ok());
1341        assert!(component_custom_id("a".repeat(100)).is_ok());
1342
1343        assert!(component_custom_id("a".repeat(101)).is_err());
1344    }
1345
1346    #[test]
1347    fn component_option_description_length() {
1348        assert!(component_option_description("").is_ok());
1349        assert!(component_option_description("a").is_ok());
1350        assert!(component_option_description("a".repeat(100)).is_ok());
1351
1352        assert!(component_option_description("a".repeat(101)).is_err());
1353    }
1354
1355    #[test]
1356    fn component_select_default_values_support() {
1357        assert!(component_select_default_values_supported(SelectMenuType::User).is_ok());
1358        assert!(component_select_default_values_supported(SelectMenuType::Role).is_ok());
1359        assert!(component_select_default_values_supported(SelectMenuType::Mentionable).is_ok());
1360        assert!(component_select_default_values_supported(SelectMenuType::Channel).is_ok());
1361
1362        assert!(component_select_default_values_supported(SelectMenuType::Text).is_err());
1363    }
1364
1365    #[test]
1366    fn component_select_num_default_values() {
1367        assert!(component_select_default_values_count(None, None, 0).is_ok());
1368        assert!(component_select_default_values_count(None, None, 1).is_ok());
1369        assert!(component_select_default_values_count(Some(1), None, 5).is_ok());
1370        assert!(component_select_default_values_count(Some(5), None, 5).is_ok());
1371        assert!(component_select_default_values_count(None, Some(5), 5).is_ok());
1372        assert!(component_select_default_values_count(None, Some(10), 5).is_ok());
1373        assert!(component_select_default_values_count(Some(5), Some(5), 5).is_ok());
1374        assert!(component_select_default_values_count(Some(1), Some(10), 5).is_ok());
1375
1376        assert!(component_select_default_values_count(Some(2), None, 1).is_err());
1377        assert!(component_select_default_values_count(None, Some(1), 2).is_err());
1378        assert!(component_select_default_values_count(Some(1), Some(1), 2).is_err());
1379        assert!(component_select_default_values_count(Some(2), Some(2), 1).is_err());
1380    }
1381
1382    #[test]
1383    fn component_select_max_values_count() {
1384        assert!(component_select_max_values(1).is_ok());
1385        assert!(component_select_max_values(25).is_ok());
1386
1387        assert!(component_select_max_values(0).is_err());
1388        assert!(component_select_max_values(26).is_err());
1389    }
1390
1391    #[test]
1392    fn component_select_min_values_count() {
1393        assert!(component_select_min_values(1).is_ok());
1394        assert!(component_select_min_values(25).is_ok());
1395
1396        assert!(component_select_min_values(26).is_err());
1397    }
1398
1399    #[test]
1400    fn component_select_option_value_length() {
1401        assert!(component_select_option_value("a").is_ok());
1402        assert!(component_select_option_value("a".repeat(100)).is_ok());
1403
1404        assert!(component_select_option_value("a".repeat(101)).is_err());
1405    }
1406
1407    #[test]
1408    fn component_select_options_count() {
1409        let select_menu_options = Vec::from([SelectMenuOption {
1410            default: false,
1411            description: None,
1412            emoji: None,
1413            label: "label".into(),
1414            value: "value".into(),
1415        }]);
1416
1417        assert!(component_select_options(&select_menu_options).is_ok());
1418
1419        let select_menu_options_25 = select_menu_options
1420            .iter()
1421            .cloned()
1422            .cycle()
1423            .take(25)
1424            .collect::<Vec<SelectMenuOption>>();
1425
1426        assert!(component_select_options(&select_menu_options_25).is_ok());
1427
1428        let select_menu_options_26 = select_menu_options
1429            .iter()
1430            .cloned()
1431            .cycle()
1432            .take(26)
1433            .collect::<Vec<SelectMenuOption>>();
1434
1435        assert!(component_select_options(&select_menu_options_26).is_err());
1436    }
1437
1438    #[test]
1439    fn component_select_placeholder_length() {
1440        assert!(component_select_placeholder("").is_ok());
1441        assert!(component_select_placeholder("a").is_ok());
1442        assert!(component_select_placeholder("a".repeat(150)).is_ok());
1443
1444        assert!(component_select_placeholder("a".repeat(151)).is_err());
1445    }
1446
1447    #[test]
1448    fn component_text_input_label_length() {
1449        assert!(component_text_input_label("a").is_ok());
1450        assert!(component_text_input_label("a".repeat(45)).is_ok());
1451
1452        assert!(component_text_input_label("").is_err());
1453        assert!(component_text_input_label("a".repeat(46)).is_err());
1454    }
1455
1456    #[test]
1457    fn component_text_input_max_count() {
1458        assert!(component_text_input_max(1).is_ok());
1459        assert!(component_text_input_max(4000).is_ok());
1460
1461        assert!(component_text_input_max(0).is_err());
1462        assert!(component_text_input_max(4001).is_err());
1463    }
1464
1465    #[test]
1466    fn component_text_input_min_count() {
1467        assert!(component_text_input_min(0).is_ok());
1468        assert!(component_text_input_min(1).is_ok());
1469        assert!(component_text_input_min(4000).is_ok());
1470
1471        assert!(component_text_input_min(4001).is_err());
1472    }
1473
1474    #[test]
1475    fn component_text_input_placeholder_length() {
1476        assert!(component_text_input_placeholder("").is_ok());
1477        assert!(component_text_input_placeholder("a").is_ok());
1478        assert!(component_text_input_placeholder("a".repeat(100)).is_ok());
1479
1480        assert!(component_text_input_placeholder("a".repeat(101)).is_err());
1481    }
1482
1483    #[test]
1484    fn component_text_input_value() {
1485        assert!(component_text_input_min(0).is_ok());
1486        assert!(component_text_input_min(1).is_ok());
1487        assert!(component_text_input_min(4000).is_ok());
1488
1489        assert!(component_text_input_min(4001).is_err());
1490    }
1491}