Skip to main content

twilight_validate/component/
component_v2.rs

1//! Validates components V2.
2
3use super::{
4    ComponentValidationError, ComponentValidationErrorType, action_row, button,
5    component_custom_id, select_menu, text_input,
6};
7use twilight_model::channel::message::Component;
8use twilight_model::channel::message::component::{
9    Checkbox, CheckboxGroup, ComponentType, Container, FileUpload, Label, MediaGallery,
10    MediaGalleryItem, Section, TextDisplay, Thumbnail,
11};
12
13/// Maximum number of [`CheckboxGroupOption`]s in a [`CheckboxGroup`].
14///
15/// This is defined in Discord's documentation, per
16/// [Discord Docs/Checkbox Group][1].
17///
18/// [1]: https://docs.discord.com/developers/components/reference#checkbox-group
19pub const CHECKBOXGROUP_OPTION_COUNT: usize = 10;
20
21/// Maximum number of [`CheckboxGroupOption`]s that can be chosen in a
22/// [`CheckboxGroup`].
23///
24/// This is defined in Dicsord's documentation, per
25/// [Discord Docs/Checkbox Group][1].
26///
27/// [1]: https://docs.discord.com/developers/components/reference#checkbox-group
28pub const CHECKBOXGROUP_MAXIMUM_VALUES_LIMIT: usize = 10;
29
30/// Minimum number of [`CheckboxGroupOption`]s that can be chosen in a
31/// [`CheckboxGroup`].
32///
33/// This is defined in Dicsord's documentation, per
34/// [Discord Docs/Checkbox Group][1].
35///
36/// [1]: https://docs.discord.com/developers/components/reference#checkbox-group
37pub const CHECKBOXGROUP_MAXIMUM_VALUES_REQUIREMENT: usize = 1;
38
39/// Maximum number of [`CheckboxGroupOption`]s that must be chosen in a
40/// [`CheckboxGroup`].
41///
42/// This is defined in Dicsord's documentation, per
43/// [Discord Docs/Checkbox Group][1].
44///
45/// [1]: https://docs.discord.com/developers/components/reference#checkbox-group
46pub const CHECKBOXGROUP_MINIMUM_VALUES_LIMIT: usize = 10;
47
48/// Maximum length of text display content.
49pub const TEXT_DISPLAY_CONTENT_LENGTH_MAX: usize = 2000;
50
51/// Minimum amount of items in a media gallery.
52pub const MEDIA_GALLERY_ITEMS_MIN: usize = 1;
53
54/// Maximum amount of items in a media gallery.
55pub const MEDIA_GALLERY_ITEMS_MAX: usize = 10;
56
57/// Maximum length of a description of a media gallery item.
58pub const MEDIA_GALLERY_ITEM_DESCRIPTION_LENGTH_MAX: usize = 1024;
59
60/// Minimum amount of components in a section.
61pub const SECTION_COMPONENTS_MIN: usize = 1;
62
63/// Maximum amount of components in a section.
64pub const SECTION_COMPONENTS_MAX: usize = 3;
65
66/// Maximum length of a thumbnail description.
67pub const THUMBNAIL_DESCRIPTION_LENGTH_MAX: usize = 1024;
68
69/// Maximum length of the label text of a label component.
70pub const LABEL_LABEL_LENGTH_MAX: usize = 45;
71
72/// Maximum length of a label description.
73pub const LABEL_DESCRIPTION_LENGTH_MAX: usize = 100;
74
75/// Maximum value of [`FileUpload::max_values`].
76///
77/// [`FileUpload::max_values`]: FileUpload::max_values
78pub const FILE_UPLOAD_MAXIMUM_VALUES_LIMIT: u8 = 10;
79
80/// Maximum value of [`FileUpload::min_values`].
81///
82/// [`FileUpload::min_values`]: FileUpload::min_values
83pub const FILE_UPLOAD_MINIMUM_VALUES_LIMIT: u8 = 10;
84
85/// Ensure that a top-level request component is correct in V2.
86///
87/// Intended to ensure that a fully formed top-level component for requests
88/// is an action row.
89///
90/// Refer to other validators like [`button`] if you need to validate other
91/// components.
92///
93/// # Errors
94///
95/// Returns an error of type [`InvalidRootComponent`] if the component cannot be a root
96/// component in both modals and messages.
97///
98/// For other errors refer to the errors of the following functions:
99/// - [`action_row`]
100/// - [`label`]
101/// - [`button`]
102/// - [`container`]
103/// - [`media_gallery`]
104/// - [`section`]
105/// - [`select_menu`]
106/// - [`text_display`]
107/// - [`text_input`]
108/// - [`thumbnail`]
109///
110/// [`InvalidRootComponent`]: ComponentValidationErrorType::InvalidRootComponent
111pub fn component_v2(component: &Component) -> Result<(), ComponentValidationError> {
112    match component {
113        Component::ActionRow(ar) => action_row(ar, true)?,
114        Component::Label(l) => label(l)?,
115        Component::Button(b) => button(b)?,
116        Component::Container(c) => container(c)?,
117        Component::MediaGallery(mg) => media_gallery(mg)?,
118        Component::Section(s) => section(s)?,
119        Component::SelectMenu(sm) => select_menu(sm, true)?,
120        Component::TextDisplay(td) => text_display(td)?,
121        Component::TextInput(_)
122        | Component::FileUpload(_)
123        | Component::Checkbox(_)
124        | Component::CheckboxGroup(_) => {
125            return Err(ComponentValidationError {
126                kind: ComponentValidationErrorType::InvalidRootComponent {
127                    kind: ComponentType::TextInput,
128                },
129            });
130        }
131        Component::Thumbnail(t) => thumbnail(t)?,
132        Component::Separator(_) | Component::File(_) | Component::Unknown(_) => (),
133    }
134
135    Ok(())
136}
137
138/// Ensure that a label is correct.
139///
140/// # Errors
141///
142/// Returns an error of type [`InvalidChildComponent`] if the provided nested
143/// component is an [`ActionRow`] or a [`Label`]. Labels cannot contain other top-level
144/// components.
145///
146/// Returns an error of type [`DisallowedChildren`] if the label contains components
147/// that are disallowed in labels.
148///
149/// Refer to [`select_menu`] for potential errors when validating a select menu in the label.
150///
151/// Refer to [`text_input`] for potential errors when validating a text input in the label.
152///
153/// Refer to [`text_display`] for potential errors when validating a text display in the label.
154///
155/// [`InvalidChildComponent`]: ComponentValidationErrorType::InvalidChildComponent
156/// [`DisallowedChildren`]: ComponentValidationErrorType::DisallowedChildren
157/// [`ActionRow`]: twilight_model::channel::message::component::ActionRow
158pub fn label(label: &Label) -> Result<(), ComponentValidationError> {
159    self::label_label(&label.label)?;
160
161    if let Some(description) = &label.description {
162        self::label_description(description)?;
163    }
164
165    match &*label.component {
166        Component::ActionRow(_) | Component::Label(_) => Err(ComponentValidationError {
167            kind: ComponentValidationErrorType::InvalidChildComponent {
168                kind: label.component.kind(),
169            },
170        }),
171        Component::SelectMenu(select_menu) => self::select_menu(select_menu, false),
172        Component::TextInput(text_input) => self::text_input(text_input, false),
173        Component::FileUpload(file_upload) => self::file_upload(file_upload),
174        Component::CheckboxGroup(cg) => self::checkbox_group(cg),
175        Component::Checkbox(c) => self::checkbox(c),
176        Component::Unknown(unknown) => Err(ComponentValidationError {
177            kind: ComponentValidationErrorType::InvalidChildComponent {
178                kind: ComponentType::Unknown(*unknown),
179            },
180        }),
181
182        Component::Button(_)
183        | Component::TextDisplay(_)
184        | Component::MediaGallery(_)
185        | Component::Separator(_)
186        | Component::File(_)
187        | Component::Section(_)
188        | Component::Container(_)
189        | Component::Thumbnail(_) => Err(ComponentValidationError {
190            kind: ComponentValidationErrorType::DisallowedChildren,
191        }),
192    }
193}
194
195/// Validates a text display component.
196///
197/// # Errors
198///
199/// This will error with [`TextDisplayContentTooLong`] if the content is longer
200/// than [`TEXT_DISPLAY_CONTENT_LENGTH_MAX`].
201///
202/// [`TextDisplayContentTooLong`]: ComponentValidationErrorType::TextDisplayContentTooLong
203pub const fn text_display(text_display: &TextDisplay) -> Result<(), ComponentValidationError> {
204    let content_len = text_display.content.len();
205    if content_len > TEXT_DISPLAY_CONTENT_LENGTH_MAX {
206        return Err(ComponentValidationError {
207            kind: ComponentValidationErrorType::TextDisplayContentTooLong { len: content_len },
208        });
209    }
210
211    Ok(())
212}
213
214/// Validates a media gallery component.
215///
216/// # Errors
217///
218/// This will error with [`MediaGalleryItemCountOutOfRange`] if the amount of
219/// media items is less than [`MEDIA_GALLERY_ITEMS_MIN`] or greater than
220/// [`MEDIA_GALLERY_ITEMS_MAX`].
221///
222/// For errors for validation of induvidual items see the documentation for [`media_gallery_item`].
223///
224/// [`MediaGalleryItemCountOutOfRange`]: ComponentValidationErrorType::MediaGalleryItemCountOutOfRange
225pub fn media_gallery(media_gallery: &MediaGallery) -> Result<(), ComponentValidationError> {
226    let items = media_gallery.items.len();
227    if !(MEDIA_GALLERY_ITEMS_MIN..=MEDIA_GALLERY_ITEMS_MAX).contains(&items) {
228        return Err(ComponentValidationError {
229            kind: ComponentValidationErrorType::MediaGalleryItemCountOutOfRange { count: items },
230        });
231    }
232
233    for item in &media_gallery.items {
234        media_gallery_item(item)?;
235    }
236
237    Ok(())
238}
239
240/// Validates a section component.
241///
242/// # Errors
243///
244/// This will error with [`SectionComponentCountOutOfRange`] if the amount of
245/// section components is less than [`SECTION_COMPONENTS_MIN`] or greater than
246/// [`SECTION_COMPONENTS_MAX`].
247///
248/// For validation of specific components see:
249/// - [`button`]
250/// - [`text_display`]
251/// - [`thumbnail`]
252///
253/// [`SectionComponentCountOutOfRange`]: ComponentValidationErrorType::SectionComponentCountOutOfRange
254pub fn section(section: &Section) -> Result<(), ComponentValidationError> {
255    let components = section.components.len();
256    if !(SECTION_COMPONENTS_MIN..=SECTION_COMPONENTS_MAX).contains(&components) {
257        return Err(ComponentValidationError {
258            kind: ComponentValidationErrorType::SectionComponentCountOutOfRange {
259                count: components,
260            },
261        });
262    }
263
264    for component in &section.components {
265        match component {
266            Component::TextDisplay(td) => text_display(td)?,
267            _ => {
268                return Err(ComponentValidationError {
269                    kind: ComponentValidationErrorType::DisallowedChildren,
270                });
271            }
272        }
273    }
274
275    match section.accessory.as_ref() {
276        Component::Button(b) => button(b)?,
277        Component::Thumbnail(t) => thumbnail(t)?,
278        _ => {
279            return Err(ComponentValidationError {
280                kind: ComponentValidationErrorType::DisallowedChildren,
281            });
282        }
283    }
284
285    Ok(())
286}
287
288/// Validates a container component.
289///
290/// The only allowed components that are allowed are: `action_row`, `file`,
291/// `media_gallery`, `section`, `separator` and `text_display`.
292///
293/// # Errors
294///
295/// For errors for specific components refer to the errors of the following functions:
296/// - [`action_row`]
297/// - [`media_gallery`]
298/// - [`text_display`]
299/// - [`section`]
300///
301/// If any except the allowed components are used if will fail with [`DisallowedChildren`].
302///
303/// [`DisallowedChildren`]: ComponentValidationErrorType::DisallowedChildren
304pub fn container(container: &Container) -> Result<(), ComponentValidationError> {
305    for component in &container.components {
306        match component {
307            Component::ActionRow(ar) => action_row(ar, true)?,
308            Component::TextDisplay(td) => text_display(td)?,
309            Component::Section(s) => section(s)?,
310            Component::MediaGallery(mg) => media_gallery(mg)?,
311            Component::Separator(_) | Component::File(_) => (),
312            _ => {
313                return Err(ComponentValidationError {
314                    kind: ComponentValidationErrorType::DisallowedChildren,
315                });
316            }
317        }
318    }
319
320    Ok(())
321}
322
323/// Validates a thumbnail component.
324///
325/// # Errors
326///
327/// This will error with [`ThumbnailDescriptionTooLong`] if the description is longer
328/// than [`THUMBNAIL_DESCRIPTION_LENGTH_MAX`].
329///
330/// [`ThumbnailDescriptionTooLong`]: ComponentValidationErrorType::ThumbnailDescriptionTooLong
331pub const fn thumbnail(thumbnail: &Thumbnail) -> Result<(), ComponentValidationError> {
332    let Some(Some(desc)) = thumbnail.description.as_ref() else {
333        return Ok(());
334    };
335
336    let len = desc.len();
337    if len > THUMBNAIL_DESCRIPTION_LENGTH_MAX {
338        return Err(ComponentValidationError {
339            kind: ComponentValidationErrorType::ThumbnailDescriptionTooLong { len },
340        });
341    }
342
343    Ok(())
344}
345
346/// Validates a file upload component.
347///
348/// # Errors
349///
350/// Returns an error of type [`ComponentCustomIdLength`] if the provided custom
351/// ID is too long.
352///
353/// Returns an error of type [`FileUploadMaximumValuesCount`] if the provided number
354/// of files that can be uploaded is larger than the maximum.
355///
356/// Returns an error of type [`FileUploadMinimumValuesCount`] if the provided number
357/// of files that must be uploaded is larger than the maximum.
358///
359/// [`ComponentCustomIdLength`]: ComponentValidationErrorType::ComponentCustomIdLength
360/// [`FileUploadMaximumValuesCount`]: ComponentValidationErrorType::FileUploadMaximumValuesCount
361/// [`FileUploadMinimumValuesCount`]: ComponentValidationErrorType::FileUploadMaximumValuesCount
362pub fn file_upload(file_upload: &FileUpload) -> Result<(), ComponentValidationError> {
363    component_custom_id(&file_upload.custom_id)?;
364
365    if let Some(min_values) = file_upload.min_values {
366        component_file_upload_min_values(min_values)?;
367    }
368
369    if let Some(max_value) = file_upload.max_values {
370        component_file_upload_max_values(max_value)?;
371    }
372
373    Ok(())
374}
375
376/// Validates a checkbox group item
377///
378/// # Errors
379///
380/// Returns an error of type [`ComponentCustomIdLength`] if the provided custom
381/// ID is too long.
382///
383/// Returns an error of type [`CheckboxGroupOptionsMissing`] if the provided
384/// options vector is empty
385///
386/// Returns an error of type [`CheckboxGroupMaximumValuesCount`] if the set maximum value
387/// is greater than [`CHECKBOXGROUP_MAXIMUM_VALUES_LIMIT`] or less than [`CHECKBOXGROUP_MAXIMUM_VALUES_REQUIREMENT`]
388///
389/// Returns an error of type [`CheckboxGroupMinimumValuesCount`] if the set minimum value is greater than
390/// [`CHECKBOXGROUP_MINIMUM_VALUES_LIMIT`]
391///
392/// Returns an error of type [`CheckboxGroupRequiredWithNoMin`] if the set minimum is 0 but required is set to true
393///
394/// [`ComponentCustomIdLength`]: ComponentValidationErrorType::ComponentCustomIdLength
395/// [`CheckboxGroupOptionsMissing`]: ComponentValidationErrorType::CheckboxGroupOptionsMissing
396/// [`CheckboxGroupMaximumValuesCount`]: ComponentValidationErrorType::CheckboxGroupMaximumValuesCount
397/// [`CheckboxGroupMinimumValuesCount`]: ComponentValidationErrorType::CheckboxGroupMinimumValuesCount
398/// [`CheckboxGroupRequiredWithNoMin`]: ComponentValidationErrorType::CheckboxGroupRequiredWithNoMin
399pub fn checkbox_group(checkbox_group: &CheckboxGroup) -> Result<(), ComponentValidationError> {
400    // custom_id length must be valid
401    component_custom_id(&checkbox_group.custom_id)?;
402
403    // must have at least one option
404    if checkbox_group.options.is_empty() {
405        return Err(ComponentValidationError {
406            kind: ComponentValidationErrorType::CheckboxGroupOptionsMissing,
407        });
408    }
409
410    if let Some(max_values) = checkbox_group.max_values {
411        self::component_checkbox_group_max_values(usize::from(max_values))?;
412    }
413
414    if let Some(min_values) = checkbox_group.min_values {
415        self::component_checkbox_group_min_values(usize::from(min_values))?;
416        let required = checkbox_group.required.unwrap_or(true); //If required isn't provided discord sets required to true
417        component_checkbox_group_required(required, usize::from(min_values))?;
418    }
419
420    Ok(())
421}
422
423/// Validates a checkbox component.
424///
425/// # Errors
426///
427/// Returns an error of type [`ComponentCustomIdLength`] if the provided custom
428/// ID is too long.
429///
430/// [`ComponentCustomIdLength`]: ComponentValidationErrorType::ComponentCustomIdLength
431pub fn checkbox(checkbox: &Checkbox) -> Result<(), ComponentValidationError> {
432    // custom_id length must be valid
433    component_custom_id(&checkbox.custom_id)?;
434    Ok(())
435}
436
437/// Validates a media gallery item
438///
439/// # Errors
440///
441/// This will error with [`MediaGalleryItemDescriptionTooLong`] if the description is longer
442/// than [`MEDIA_GALLERY_ITEM_DESCRIPTION_LENGTH_MAX`].
443///
444/// [`MediaGalleryItemDescriptionTooLong`]: ComponentValidationErrorType::MediaGalleryItemDescriptionTooLong
445pub const fn media_gallery_item(item: &MediaGalleryItem) -> Result<(), ComponentValidationError> {
446    let Some(desc) = item.description.as_ref() else {
447        return Ok(());
448    };
449
450    let len = desc.len();
451    if len > MEDIA_GALLERY_ITEM_DESCRIPTION_LENGTH_MAX {
452        return Err(ComponentValidationError {
453            kind: ComponentValidationErrorType::MediaGalleryItemDescriptionTooLong { len },
454        });
455    }
456
457    Ok(())
458}
459
460/// Ensure a [`Label::label`]'s length is correct.
461///
462/// # Errors
463///
464/// Returns an error of type [`LabelLabelTooLong`] if the length is invalid.
465///
466/// [`Label::label`]: twilight_model::channel::message::component::Label::label
467/// [`LabelLabelTooLong`]: ComponentValidationErrorType::LabelLabelTooLong
468fn label_label(value: impl AsRef<str>) -> Result<(), ComponentValidationError> {
469    let chars = value.as_ref().chars().count();
470
471    if chars <= LABEL_LABEL_LENGTH_MAX {
472        Ok(())
473    } else {
474        Err(ComponentValidationError {
475            kind: ComponentValidationErrorType::LabelLabelTooLong { len: chars },
476        })
477    }
478}
479
480/// Ensure a [`Label::description`]'s length is correct.
481///
482/// # Errors
483///
484/// Returns an error of type [`LabelDescriptionTooLong`] if the length is invalid.
485///
486/// [`Label::label`]: twilight_model::channel::message::component::Label::description
487/// [`LabelDescriptionTooLong`]: ComponentValidationErrorType::LabelDescriptionTooLong
488fn label_description(value: impl AsRef<str>) -> Result<(), ComponentValidationError> {
489    let chars = value.as_ref().chars().count();
490
491    if chars <= LABEL_DESCRIPTION_LENGTH_MAX {
492        Ok(())
493    } else {
494        Err(ComponentValidationError {
495            kind: ComponentValidationErrorType::LabelDescriptionTooLong { len: chars },
496        })
497    }
498}
499
500/// Validate a [`FileUpload::max_values`] amount.
501///
502/// # Errors
503///
504/// Returns an error of type [`FileUploadMaximumValuesCount`] if the provided number
505/// of files that can be uploaded is larger than
506/// [the maximum][`FILE_UPLOAD_MAXIMUM_VALUES_LIMIT`].
507///
508/// [`FileUpload::max_values`]: twilight_model::channel::message::component::FileUpload::max_values
509/// [`FileUploadMaximumValuesCount`]: ComponentValidationErrorType::FileUploadMaximumValuesCount
510const fn component_file_upload_max_values(count: u8) -> Result<(), ComponentValidationError> {
511    if count > FILE_UPLOAD_MAXIMUM_VALUES_LIMIT {
512        return Err(ComponentValidationError {
513            kind: ComponentValidationErrorType::FileUploadMaximumValuesCount { count },
514        });
515    }
516
517    Ok(())
518}
519
520/// Validate a [`FileUpload::min_values`] amount.
521///
522/// # Errors
523///
524/// Returns an error of type [`FileUploadMinimumValuesCount`] if the provided number
525/// of files that must be uploaded is larger than
526/// [the maximum][`FILE_UPLOAD_MINIMUM_VALUES_LIMIT`].
527///
528/// [`FileUpload::min_values`]: twilight_model::channel::message::component::FileUpload::min_values
529/// [`FileUploadMinimumValuesCount`]: ComponentValidationErrorType::FileUploadMinimumValuesCount
530const fn component_file_upload_min_values(count: u8) -> Result<(), ComponentValidationError> {
531    if count > FILE_UPLOAD_MINIMUM_VALUES_LIMIT {
532        return Err(ComponentValidationError {
533            kind: ComponentValidationErrorType::FileUploadMinimumValuesCount { count },
534        });
535    }
536
537    Ok(())
538}
539
540/// Validate a [`CheckboxGroup::max_values`] amount.
541///
542/// # Errors
543///
544/// Returns an error of type [`CheckboxGroupMaximumValuesCount`] if the provided maximum value
545/// number is greater than [`CHECKBOXGROUP_MAXIMUM_VALUES_LIMIT`] or less than [`CHECKBOXGROUP_MAXIMUM_VALUES_REQUIREMENT`]
546///
547/// [the maximum][`CHECKBOXGROUP_MAXIMUM_VALUES_LIMIT`].
548/// [the minimum][`CHECKBOXGROUP_MAXIMUM_VALUES_REQUIREMENT`]
549///
550/// [`CheckboxGroupMaximumValuesCount`]: ComponentValidationErrorType::CheckboxGroupMaximumValuesCount
551const fn component_checkbox_group_max_values(count: usize) -> Result<(), ComponentValidationError> {
552    if count > CHECKBOXGROUP_MAXIMUM_VALUES_LIMIT {
553        return Err(ComponentValidationError {
554            kind: ComponentValidationErrorType::CheckboxGroupMaximumValuesCount { count },
555        });
556    }
557
558    if count < CHECKBOXGROUP_MAXIMUM_VALUES_REQUIREMENT {
559        return Err(ComponentValidationError {
560            kind: ComponentValidationErrorType::CheckboxGroupMaximumValuesCount { count },
561        });
562    }
563
564    Ok(())
565}
566
567/// Validate a [`CheckboxGroup::min_values`] amount.
568///
569/// # Errors
570///
571/// Returns an error of type [`CheckboxGroupMinimumValuesCount`] if the provided minimum
572/// selected value count is greater than
573/// [the maximum][`CHECKBOXGROUP_MINIMUM_VALUES_LIMIT`].
574///
575/// [`CheckboxGroupMinimumValuesCount`]: ComponentValidationErrorType::CheckboxGroupMinimumValuesCount
576const fn component_checkbox_group_min_values(count: usize) -> Result<(), ComponentValidationError> {
577    if count > CHECKBOXGROUP_MINIMUM_VALUES_LIMIT {
578        return Err(ComponentValidationError {
579            kind: ComponentValidationErrorType::CheckboxGroupMinimumValuesCount { count },
580        });
581    }
582
583    Ok(())
584}
585
586/// Validate a [`CheckboxGroup::required`] boolean.
587///
588/// # Errors
589///
590/// Returns an error of type [`CheckboxGroupRequiredWithNoMin`] if the provided minimum
591/// selected values is 0 while required is set to true
592///
593/// [`CheckboxGroupRequiredWithNoMin`]: ComponentValidationErrorType::CheckboxGroupRequiredWithNoMin
594const fn component_checkbox_group_required(
595    required: bool,
596    min_values: usize,
597) -> Result<(), ComponentValidationError> {
598    if required && min_values == 0 {
599        return Err(ComponentValidationError {
600            kind: ComponentValidationErrorType::CheckboxGroupRequiredWithNoMin,
601        });
602    }
603
604    Ok(())
605}
606
607#[cfg(test)]
608mod tests {
609    use super::*;
610    use std::iter;
611    use twilight_model::channel::message::Component;
612    use twilight_model::channel::message::component::{
613        Button, ButtonStyle, Label, SelectMenu, SelectMenuType, TextInput, TextInputStyle,
614    };
615
616    fn wrap_in_label(component: Component) -> Component {
617        Component::Label(Label {
618            id: None,
619            label: "label".to_owned(),
620            description: None,
621            component: Box::new(component),
622        })
623    }
624
625    #[test]
626    fn component_label() {
627        let button = Component::Button(Button {
628            custom_id: None,
629            disabled: false,
630            emoji: None,
631            label: Some("Press me".to_owned()),
632            style: ButtonStyle::Danger,
633            url: None,
634            sku_id: None,
635            id: None,
636        });
637
638        let text_display = Component::TextDisplay(TextDisplay {
639            id: None,
640            content: "Text display".to_owned(),
641        });
642
643        let valid_select_menu = SelectMenu {
644            channel_types: None,
645            custom_id: "my_select".to_owned(),
646            default_values: None,
647            disabled: false,
648            kind: SelectMenuType::User,
649            max_values: None,
650            min_values: None,
651            options: None,
652            placeholder: None,
653            id: None,
654            required: None,
655        };
656
657        let disabled_select_menu = SelectMenu {
658            disabled: true,
659            ..valid_select_menu.clone()
660        };
661
662        let valid_label = Label {
663            id: None,
664            label: "Label".to_owned(),
665            description: Some("This is a description".to_owned()),
666            component: Box::new(Component::SelectMenu(valid_select_menu)),
667        };
668
669        let label_invalid_child_button = Label {
670            component: Box::new(button),
671            ..valid_label.clone()
672        };
673
674        let label_invalid_child_text_display = Label {
675            id: None,
676            label: "Another label".to_owned(),
677            description: None,
678            component: Box::new(text_display),
679        };
680
681        let label_invalid_child_disabled_select = Label {
682            component: Box::new(Component::SelectMenu(disabled_select_menu)),
683            ..valid_label.clone()
684        };
685
686        let label_too_long_description = Label {
687            description: Some(iter::repeat_n('a', 101).collect()),
688            ..valid_label.clone()
689        };
690
691        let label_too_long_label = Label {
692            label: iter::repeat_n('a', 46).collect(),
693            ..valid_label.clone()
694        };
695
696        assert!(label(&valid_label).is_ok());
697        assert!(component_v2(&Component::Label(valid_label)).is_ok());
698        assert!(label(&label_invalid_child_button).is_err());
699        assert!(component_v2(&Component::Label(label_invalid_child_button)).is_err());
700        assert!(label(&label_invalid_child_text_display).is_err());
701        assert!(component_v2(&Component::Label(label_invalid_child_text_display)).is_err());
702        assert!(label(&label_invalid_child_disabled_select).is_err());
703        assert!(component_v2(&Component::Label(label_invalid_child_disabled_select)).is_err());
704        assert!(label(&label_too_long_description).is_err());
705        assert!(component_v2(&Component::Label(label_too_long_description)).is_err());
706        assert!(label(&label_too_long_label).is_err());
707        assert!(component_v2(&Component::Label(label_too_long_label)).is_err());
708    }
709
710    #[test]
711    fn no_text_input_label_in_label_component() {
712        #[allow(deprecated)]
713        let text_input_with_label = Component::TextInput(TextInput {
714            id: None,
715            custom_id: "The custom id".to_owned(),
716            label: Some("The text input label".to_owned()),
717            max_length: None,
718            min_length: None,
719            placeholder: None,
720            required: None,
721            style: TextInputStyle::Short,
722            value: None,
723        });
724
725        let invalid_label_component = Label {
726            id: None,
727            label: "Label".to_owned(),
728            description: None,
729            component: Box::new(text_input_with_label),
730        };
731
732        assert!(label(&invalid_label_component).is_err());
733        assert!(component_v2(&Component::Label(invalid_label_component)).is_err());
734    }
735
736    #[test]
737    fn component_file_upload() {
738        let valid = FileUpload {
739            id: Some(42),
740            custom_id: "custom_id".to_owned(),
741            max_values: Some(10),
742            min_values: Some(10),
743            required: None,
744        };
745
746        assert!(file_upload(&valid).is_ok());
747        assert!(component_v2(&wrap_in_label(Component::FileUpload(valid.clone()))).is_ok());
748
749        let invalid_custom_id = FileUpload {
750            custom_id: iter::repeat_n('a', 101).collect(),
751            ..valid.clone()
752        };
753
754        assert!(file_upload(&invalid_custom_id).is_err());
755        assert!(component_v2(&wrap_in_label(Component::FileUpload(invalid_custom_id))).is_err());
756
757        let invalid_min_values = FileUpload {
758            min_values: Some(11),
759            ..valid.clone()
760        };
761
762        assert!(file_upload(&invalid_min_values).is_err());
763        assert!(component_v2(&wrap_in_label(Component::FileUpload(invalid_min_values))).is_err());
764
765        let invalid_max_values_too_high = FileUpload {
766            max_values: Some(11),
767            ..valid
768        };
769
770        assert!(file_upload(&invalid_max_values_too_high).is_err());
771        assert!(
772            component_v2(&wrap_in_label(Component::FileUpload(
773                invalid_max_values_too_high
774            )))
775            .is_err()
776        );
777    }
778}