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