twilight_validate/
component.rs

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