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