Skip to main content

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