1mod component_v2;
4
5use std::{
6 error::Error,
7 fmt::{Debug, Display, Formatter, Result as FmtResult},
8};
9use twilight_model::channel::message::component::{
10 ActionRow, Button, ButtonStyle, Component, ComponentType, SelectMenu, SelectMenuOption,
11 SelectMenuType, TextInput,
12};
13
14pub use component_v2::{
15 CHECKBOXGROUP_MAXIMUM_VALUES_LIMIT, CHECKBOXGROUP_MAXIMUM_VALUES_REQUIREMENT,
16 CHECKBOXGROUP_MINIMUM_VALUES_LIMIT, CHECKBOXGROUP_OPTION_COUNT, FILE_UPLOAD_FILE_TYPES_LIMIT,
17 FILE_UPLOAD_MAXIMUM_VALUES_LIMIT, FILE_UPLOAD_MINIMUM_VALUES_LIMIT,
18 LABEL_DESCRIPTION_LENGTH_MAX, LABEL_LABEL_LENGTH_MAX,
19 MEDIA_GALLERY_ITEM_DESCRIPTION_LENGTH_MAX, MEDIA_GALLERY_ITEMS_MAX, MEDIA_GALLERY_ITEMS_MIN,
20 SECTION_COMPONENTS_MAX, SECTION_COMPONENTS_MIN, TEXT_DISPLAY_CONTENT_LENGTH_MAX,
21 THUMBNAIL_DESCRIPTION_LENGTH_MAX, component_v2, container, file_upload, label, media_gallery,
22 media_gallery_item, section, text_display, thumbnail,
23};
24
25pub const ACTION_ROW_COMPONENT_COUNT: usize = 5;
32
33pub const COMPONENT_COUNT: usize = 5;
40
41pub const COMPONENT_V2_COUNT: usize = 40;
48
49pub const COMPONENT_CUSTOM_ID_LENGTH: usize = 100;
59
60pub const COMPONENT_BUTTON_LABEL_LENGTH: usize = 80;
69
70pub const SELECT_MAXIMUM_VALUES_LIMIT: usize = 25;
78
79pub const SELECT_MAXIMUM_VALUES_REQUIREMENT: usize = 1;
87
88pub const SELECT_MINIMUM_VALUES_LIMIT: usize = 25;
96
97pub const SELECT_OPTION_COUNT: usize = 25;
104
105pub const SELECT_OPTION_DESCRIPTION_LENGTH: usize = 100;
112
113pub const SELECT_OPTION_LABEL_LENGTH: usize = 100;
120
121pub const SELECT_OPTION_VALUE_LENGTH: usize = 100;
128
129pub const SELECT_PLACEHOLDER_LENGTH: usize = 150;
136
137pub const TEXT_INPUT_LABEL_MAX: usize = 45;
143
144pub const TEXT_INPUT_LABEL_MIN: usize = 1;
150
151pub const TEXT_INPUT_LENGTH_MAX: usize = 4000;
157
158pub const TEXT_INPUT_LENGTH_MIN: usize = 1;
164
165pub const TEXT_INPUT_PLACEHOLDER_MAX: usize = 100;
171
172#[derive(Debug)]
177pub struct ComponentValidationError {
178 kind: ComponentValidationErrorType,
180}
181
182impl ComponentValidationError {
183 #[must_use = "retrieving the type has no effect if left unused"]
185 pub const fn kind(&self) -> &ComponentValidationErrorType {
186 &self.kind
187 }
188
189 #[allow(clippy::unused_self)]
191 #[must_use = "consuming the error and retrieving the source has no effect if left unused"]
192 pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
193 None
194 }
195
196 #[must_use = "consuming the error into its parts has no effect if left unused"]
198 pub fn into_parts(
199 self,
200 ) -> (
201 ComponentValidationErrorType,
202 Option<Box<dyn Error + Send + Sync>>,
203 ) {
204 (self.kind, None)
205 }
206}
207
208impl Display for ComponentValidationError {
209 #[allow(clippy::too_many_lines)]
210 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
211 match &self.kind {
212 ComponentValidationErrorType::ActionRowComponentCount { count } => {
213 f.write_str("an action row has ")?;
214 Display::fmt(&count, f)?;
215 f.write_str(" children, but the max is ")?;
216
217 Display::fmt(&ACTION_ROW_COMPONENT_COUNT, f)
218 }
219 ComponentValidationErrorType::ButtonConflict => {
220 f.write_str("button has both a custom id and url, which is never valid")
221 }
222 ComponentValidationErrorType::ButtonStyle { style } => {
223 f.write_str("button has a type of ")?;
224 Debug::fmt(style, f)?;
225 f.write_str(", which must have a ")?;
226
227 f.write_str(if *style == ButtonStyle::Link {
228 "url"
229 } else {
230 "custom id"
231 })?;
232
233 f.write_str(" configured")
234 }
235 ComponentValidationErrorType::CheckboxGroupMaximumValuesCount { count } => {
236 f.write_str("maximum number of values that can be chosen is ")?;
237 Display::fmt(count, f)?;
238 f.write_str(", but must be greater than or equal to ")?;
239 Display::fmt(&CHECKBOXGROUP_MAXIMUM_VALUES_REQUIREMENT, f)?;
240 f.write_str("and less than or equal to ")?;
241
242 Display::fmt(&CHECKBOXGROUP_MAXIMUM_VALUES_LIMIT, f)
243 }
244 ComponentValidationErrorType::CheckboxGroupMinimumValuesCount { count } => {
245 f.write_str("minimum number of values that must be chosen is ")?;
246 Display::fmt(count, f)?;
247 f.write_str(", but must be less than or equal to ")?;
248
249 Display::fmt(&CHECKBOXGROUP_MINIMUM_VALUES_LIMIT, f)
250 }
251 ComponentValidationErrorType::CheckboxGroupOptionCount { count } => {
252 f.write_str("a checkbox group has ")?;
253 Display::fmt(&count, f)?;
254 f.write_str(" options, but the max is ")?;
255
256 Display::fmt(&CHECKBOXGROUP_OPTION_COUNT, f)
257 }
258 ComponentValidationErrorType::CheckboxGroupOptionsMissing => {
259 f.write_str("a checkbox group is missing it's options field")
260 }
261 ComponentValidationErrorType::CheckboxGroupRequiredWithNoMin => f.write_str(
262 "a checkbox group is marked required but has a min of 0 selected options",
263 ),
264 ComponentValidationErrorType::ComponentCount { count } => {
265 Display::fmt(count, f)?;
266 f.write_str(" components were provided, but the max is ")?;
267
268 Display::fmt(&COMPONENT_COUNT, f)
269 }
270 ComponentValidationErrorType::ComponentCustomIdLength { chars } => {
271 f.write_str("a component's custom id is ")?;
272 Display::fmt(&chars, f)?;
273 f.write_str(" characters long, but the max is ")?;
274
275 Display::fmt(&COMPONENT_CUSTOM_ID_LENGTH, f)
276 }
277 ComponentValidationErrorType::ComponentLabelLength { chars } => {
278 f.write_str("a component's label is ")?;
279 Display::fmt(&chars, f)?;
280 f.write_str(" characters long, but the max is ")?;
281
282 Display::fmt(&COMPONENT_BUTTON_LABEL_LENGTH, f)
283 }
284 ComponentValidationErrorType::InvalidChildComponent { kind } => {
285 f.write_str("a '")?;
286 Display::fmt(&kind, f)?;
287
288 f.write_str(" component was provided, but can not be a child component")
289 }
290 ComponentValidationErrorType::InvalidRootComponent { kind } => {
291 f.write_str("a '")?;
292 Display::fmt(kind, f)?;
293
294 f.write_str("' component was provided, but can not be a root component")
295 }
296 ComponentValidationErrorType::SelectMaximumValuesCount { count } => {
297 f.write_str("maximum number of values that can be chosen is ")?;
298 Display::fmt(count, f)?;
299 f.write_str(", but must be greater than or equal to ")?;
300 Display::fmt(&SELECT_MAXIMUM_VALUES_REQUIREMENT, f)?;
301 f.write_str("and less than or equal to ")?;
302
303 Display::fmt(&SELECT_MAXIMUM_VALUES_LIMIT, f)
304 }
305 ComponentValidationErrorType::SelectMinimumValuesCount { count } => {
306 f.write_str("minimum number of values that must be chosen is ")?;
307 Display::fmt(count, f)?;
308 f.write_str(", but must be less than or equal to ")?;
309
310 Display::fmt(&SELECT_MINIMUM_VALUES_LIMIT, f)
311 }
312 ComponentValidationErrorType::SelectNotEnoughDefaultValues { provided, min } => {
313 f.write_str("a select menu provided ")?;
314 Display::fmt(provided, f)?;
315 f.write_str(" values, but it requires at least ")?;
316 Display::fmt(min, f)?;
317 f.write_str(" values")
318 }
319 ComponentValidationErrorType::SelectOptionsMissing => {
320 f.write_str("a text select menu doesn't specify the required options field")
321 }
322 ComponentValidationErrorType::SelectOptionDescriptionLength { chars } => {
323 f.write_str("a select menu option's description is ")?;
324 Display::fmt(&chars, f)?;
325 f.write_str(" characters long, but the max is ")?;
326
327 Display::fmt(&SELECT_OPTION_DESCRIPTION_LENGTH, f)
328 }
329 ComponentValidationErrorType::SelectOptionLabelLength { chars } => {
330 f.write_str("a select menu option's label is ")?;
331 Display::fmt(&chars, f)?;
332 f.write_str(" characters long, but the max is ")?;
333
334 Display::fmt(&SELECT_OPTION_LABEL_LENGTH, f)
335 }
336 ComponentValidationErrorType::SelectOptionValueLength { chars } => {
337 f.write_str("a select menu option's value is ")?;
338 Display::fmt(&chars, f)?;
339 f.write_str(" characters long, but the max is ")?;
340
341 Display::fmt(&SELECT_OPTION_VALUE_LENGTH, f)
342 }
343 ComponentValidationErrorType::SelectPlaceholderLength { chars } => {
344 f.write_str("a select menu's placeholder is ")?;
345 Display::fmt(&chars, f)?;
346 f.write_str(" characters long, but the max is ")?;
347
348 Display::fmt(&SELECT_PLACEHOLDER_LENGTH, f)
349 }
350 ComponentValidationErrorType::SelectOptionCount { count } => {
351 f.write_str("a select menu has ")?;
352 Display::fmt(&count, f)?;
353 f.write_str(" options, but the max is ")?;
354
355 Display::fmt(&SELECT_OPTION_COUNT, f)
356 }
357 ComponentValidationErrorType::SelectTooManyDefaultValues { provided, max } => {
358 f.write_str("a select menu provided ")?;
359 Display::fmt(provided, f)?;
360 f.write_str(" values, but it allows at most ")?;
361 Display::fmt(max, f)?;
362 f.write_str(" values")
363 }
364 ComponentValidationErrorType::SelectUnsupportedDefaultValues { kind } => {
365 f.write_str("a select menu has defined default_values, but its type, ")?;
366 Debug::fmt(kind, f)?;
367 f.write_str(", does not support them")
368 }
369 ComponentValidationErrorType::TextInputLabelLength { len: count } => {
370 f.write_str("a text input label length is ")?;
371 Display::fmt(count, f)?;
372 f.write_str(", but it must be at least ")?;
373 Display::fmt(&TEXT_INPUT_LABEL_MIN, f)?;
374 f.write_str(" and at most ")?;
375
376 Display::fmt(&TEXT_INPUT_LABEL_MAX, f)
377 }
378 ComponentValidationErrorType::TextInputMaxLength { len: count } => {
379 f.write_str("a text input max length is ")?;
380 Display::fmt(count, f)?;
381 f.write_str(", but it must be at least ")?;
382 Display::fmt(&TEXT_INPUT_LENGTH_MIN, f)?;
383 f.write_str(" and at most ")?;
384
385 Display::fmt(&TEXT_INPUT_LENGTH_MAX, f)
386 }
387 ComponentValidationErrorType::TextInputMinLength { len: count } => {
388 f.write_str("a text input min length is ")?;
389 Display::fmt(count, f)?;
390 f.write_str(", but it must be at most ")?;
391
392 Display::fmt(&TEXT_INPUT_LENGTH_MAX, f)
393 }
394 ComponentValidationErrorType::TextInputPlaceholderLength { chars } => {
395 f.write_str("a text input's placeholder is ")?;
396 Display::fmt(&chars, f)?;
397 f.write_str(" characters long, but the max is ")?;
398
399 Display::fmt(&TEXT_INPUT_PLACEHOLDER_MAX, f)
400 }
401 ComponentValidationErrorType::TextInputValueLength { chars } => {
402 f.write_str("a text input's value is ")?;
403 Display::fmt(&chars, f)?;
404 f.write_str(" characters long, but the max is ")?;
405
406 Display::fmt(&TEXT_INPUT_PLACEHOLDER_MAX, f)
407 }
408 ComponentValidationErrorType::TextInputDisallowedLabel => {
409 f.write_str("a text input contained a label when disallowed")
410 }
411 ComponentValidationErrorType::DisallowedV2Components => {
412 f.write_str("a V2 component was used in a component V1 message")
413 }
414 ComponentValidationErrorType::DisallowedChildren => {
415 f.write_str("a component contains a disallowed child component")
416 }
417 ComponentValidationErrorType::TextDisplayContentTooLong { len: count } => {
418 f.write_str("a text display content length is ")?;
419 Display::fmt(count, f)?;
420 f.write_str(" characters long, but the max is ")?;
421
422 Display::fmt(&TEXT_DISPLAY_CONTENT_LENGTH_MAX, f)
423 }
424 ComponentValidationErrorType::MediaGalleryItemCountOutOfRange { count } => {
425 f.write_str("a media gallery has ")?;
426 Display::fmt(count, f)?;
427 f.write_str(" items, but the min and max are ")?;
428 Display::fmt(&MEDIA_GALLERY_ITEMS_MIN, f)?;
429 f.write_str(" and ")?;
430 Display::fmt(&MEDIA_GALLERY_ITEMS_MAX, f)?;
431
432 f.write_str(" respectively")
433 }
434 ComponentValidationErrorType::MediaGalleryItemDescriptionTooLong { len } => {
435 f.write_str("a media gallery item description length is ")?;
436 Display::fmt(len, f)?;
437 f.write_str(" characters long, but the max is ")?;
438
439 Display::fmt(&MEDIA_GALLERY_ITEM_DESCRIPTION_LENGTH_MAX, f)
440 }
441 ComponentValidationErrorType::SectionComponentCountOutOfRange { count } => {
442 f.write_str("a section has ")?;
443 Display::fmt(count, f)?;
444 f.write_str(" components, but the min and max are ")?;
445 Display::fmt(&SECTION_COMPONENTS_MIN, f)?;
446 f.write_str(" and ")?;
447 Display::fmt(&SECTION_COMPONENTS_MAX, f)?;
448
449 f.write_str(" respectively")
450 }
451 ComponentValidationErrorType::SelectDisallowedDisabled => {
452 f.write_str("a select menu was disabled when disallowed")
453 }
454 ComponentValidationErrorType::ThumbnailDescriptionTooLong { len } => {
455 f.write_str("a thumbnail description length is ")?;
456 Display::fmt(len, f)?;
457 f.write_str(" characters long, but the max is ")?;
458
459 Display::fmt(&THUMBNAIL_DESCRIPTION_LENGTH_MAX, f)
460 }
461 ComponentValidationErrorType::LabelLabelTooLong { len } => {
462 f.write_str("a label text of a label component is ")?;
463 Display::fmt(len, f)?;
464 f.write_str(" characters long, but the max is ")?;
465
466 Display::fmt(&LABEL_LABEL_LENGTH_MAX, f)
467 }
468 ComponentValidationErrorType::LabelDescriptionTooLong { len } => {
469 f.write_str("a label description length is ")?;
470 Display::fmt(len, f)?;
471 f.write_str(" characters long, but the max is ")?;
472
473 Display::fmt(&LABEL_DESCRIPTION_LENGTH_MAX, f)
474 }
475 ComponentValidationErrorType::FileUploadMaximumValuesCount { count } => {
476 f.write_str("maximum number of files that can be uploaded is ")?;
477 Display::fmt(count, f)?;
478 f.write_str(", but must be less than or equal to ")?;
479
480 Display::fmt(&FILE_UPLOAD_MAXIMUM_VALUES_LIMIT, f)
481 }
482 ComponentValidationErrorType::FileUploadMinimumValuesCount { count } => {
483 f.write_str("minimum number of files that must be uploaded is ")?;
484 Display::fmt(count, f)?;
485 f.write_str(", but must be less than or equal to ")?;
486
487 Display::fmt(&FILE_UPLOAD_MINIMUM_VALUES_LIMIT, f)
488 }
489 ComponentValidationErrorType::FileUploadFileTypesCount { count } => {
490 f.write_str("number of file type filters is ")?;
491 Display::fmt(count, f)?;
492 f.write_str(", but must be less than or equal to ")?;
493
494 Display::fmt(&FILE_UPLOAD_FILE_TYPES_LIMIT, f)
495 }
496 }
497 }
498}
499
500impl Error for ComponentValidationError {}
501
502#[derive(Debug)]
504#[non_exhaustive]
505pub enum ComponentValidationErrorType {
506 ActionRowComponentCount {
509 count: usize,
511 },
512 ButtonConflict,
514 ButtonStyle {
519 style: ButtonStyle,
521 },
522 CheckboxGroupMaximumValuesCount {
526 count: usize,
528 },
529 CheckboxGroupOptionCount {
532 count: usize,
534 },
535 CheckboxGroupMinimumValuesCount {
537 count: usize,
539 },
540 CheckboxGroupOptionsMissing,
542 CheckboxGroupRequiredWithNoMin,
544 ComponentCount {
547 count: usize,
549 },
550 ComponentCustomIdLength {
553 chars: usize,
555 },
556 ComponentLabelLength {
558 chars: usize,
560 },
561 InvalidChildComponent {
563 kind: ComponentType,
565 },
566 InvalidRootComponent {
568 kind: ComponentType,
570 },
571 SelectMaximumValuesCount {
575 count: usize,
577 },
578 SelectMinimumValuesCount {
581 count: usize,
583 },
584 SelectNotEnoughDefaultValues {
586 provided: usize,
588 min: usize,
590 },
591 SelectOptionsMissing,
595 SelectOptionCount {
598 count: usize,
600 },
601 SelectOptionDescriptionLength {
604 chars: usize,
606 },
607 SelectOptionLabelLength {
610 chars: usize,
612 },
613 SelectOptionValueLength {
616 chars: usize,
618 },
619 SelectPlaceholderLength {
622 chars: usize,
624 },
625 SelectTooManyDefaultValues {
627 provided: usize,
629 max: usize,
631 },
632 SelectUnsupportedDefaultValues {
634 kind: SelectMenuType,
636 },
637 TextInputLabelLength {
639 len: usize,
641 },
642 TextInputMaxLength {
644 len: usize,
646 },
647 TextInputMinLength {
649 len: usize,
651 },
652 TextInputPlaceholderLength {
655 chars: usize,
657 },
658 TextInputValueLength {
661 chars: usize,
663 },
664 TextInputDisallowedLabel,
666 DisallowedV2Components,
668 DisallowedChildren,
670 TextDisplayContentTooLong {
672 len: usize,
674 },
675 MediaGalleryItemCountOutOfRange {
677 count: usize,
679 },
680 MediaGalleryItemDescriptionTooLong {
682 len: usize,
684 },
685 SectionComponentCountOutOfRange {
687 count: usize,
689 },
690 SelectDisallowedDisabled,
693 ThumbnailDescriptionTooLong {
695 len: usize,
697 },
698 LabelLabelTooLong {
700 len: usize,
702 },
703 LabelDescriptionTooLong {
705 len: usize,
707 },
708 FileUploadMaximumValuesCount {
711 count: u8,
715 },
716 FileUploadMinimumValuesCount {
719 count: u8,
723 },
724 FileUploadFileTypesCount {
727 count: usize,
729 },
730}
731
732pub fn component_v1(component: &Component) -> Result<(), ComponentValidationError> {
752 match component {
753 Component::ActionRow(action_row) => self::action_row(action_row, false)?,
754 other => {
755 return Err(ComponentValidationError {
756 kind: ComponentValidationErrorType::InvalidRootComponent { kind: other.kind() },
757 });
758 }
759 }
760
761 Ok(())
762}
763
764#[deprecated(note = "Use component_v1 for old components and component_v2 for new ones")]
782pub fn component(component: &Component) -> Result<(), ComponentValidationError> {
783 component_v1(component)
784}
785
786pub fn action_row(action_row: &ActionRow, is_v2: bool) -> Result<(), ComponentValidationError> {
818 self::component_action_row_components(&action_row.components)?;
819
820 for component in &action_row.components {
821 match component {
822 Component::ActionRow(_) | Component::Label(_) => {
823 return Err(ComponentValidationError {
824 kind: ComponentValidationErrorType::InvalidChildComponent {
825 kind: component.kind(),
826 },
827 });
828 }
829 Component::Button(button) => self::button(button)?,
830 Component::SelectMenu(select_menu) => self::select_menu(select_menu, true)?,
831 Component::TextInput(text_input) => self::text_input(text_input, true)?,
832 Component::Unknown(unknown) => {
833 return Err(ComponentValidationError {
834 kind: ComponentValidationErrorType::InvalidChildComponent {
835 kind: ComponentType::Unknown(*unknown),
836 },
837 });
838 }
839
840 Component::TextDisplay(_)
841 | Component::MediaGallery(_)
842 | Component::Separator(_)
843 | Component::File(_)
844 | Component::Section(_)
845 | Component::Container(_)
846 | Component::Thumbnail(_)
847 | Component::Checkbox(_)
848 | Component::CheckboxGroup(_)
849 | Component::FileUpload(_) => {
850 return Err(ComponentValidationError {
851 kind: if is_v2 {
852 ComponentValidationErrorType::DisallowedChildren
853 } else {
854 ComponentValidationErrorType::DisallowedV2Components
855 },
856 });
857 }
858 }
859 }
860
861 Ok(())
862}
863
864pub fn button(button: &Button) -> Result<(), ComponentValidationError> {
886 let has_custom_id = button.custom_id.is_some();
887 let has_emoji = button.emoji.is_some();
888 let has_label = button.label.is_some();
889 let has_sku_id = button.sku_id.is_some();
890 let has_url = button.url.is_some();
891
892 if has_custom_id && has_url {
895 return Err(ComponentValidationError {
896 kind: ComponentValidationErrorType::ButtonConflict,
897 });
898 }
899
900 let is_premium = button.style == ButtonStyle::Premium;
905 if is_premium && (has_custom_id || has_url || has_label || has_emoji || !has_sku_id) {
906 return Err(ComponentValidationError {
907 kind: ComponentValidationErrorType::ButtonStyle {
908 style: button.style,
909 },
910 });
911 }
912
913 let is_link = button.style == ButtonStyle::Link;
918
919 if (is_link && !has_url) || (!is_link && !has_custom_id) {
920 return Err(ComponentValidationError {
921 kind: ComponentValidationErrorType::ButtonStyle {
922 style: button.style,
923 },
924 });
925 }
926
927 if let Some(custom_id) = button.custom_id.as_ref() {
928 self::component_custom_id(custom_id)?;
929 }
930
931 if let Some(label) = button.label.as_ref() {
932 self::component_button_label(label)?;
933 }
934
935 Ok(())
936}
937
938pub fn select_menu(
996 select_menu: &SelectMenu,
997 disabled_allowed: bool,
998) -> Result<(), ComponentValidationError> {
999 self::component_custom_id(&select_menu.custom_id)?;
1000
1001 if !disabled_allowed && select_menu.disabled {
1002 return Err(ComponentValidationError {
1003 kind: ComponentValidationErrorType::SelectDisallowedDisabled,
1004 });
1005 }
1006
1007 if let SelectMenuType::Text = &select_menu.kind {
1009 let options = select_menu
1010 .options
1011 .as_ref()
1012 .ok_or(ComponentValidationError {
1013 kind: ComponentValidationErrorType::SelectOptionsMissing,
1014 })?;
1015 for option in options {
1016 component_select_option_label(&option.label)?;
1017 component_select_option_value(&option.value)?;
1018
1019 if let Some(description) = option.description.as_ref() {
1020 component_option_description(description)?;
1021 }
1022 }
1023 component_select_options(options)?;
1024 }
1025
1026 if let Some(placeholder) = select_menu.placeholder.as_ref() {
1027 self::component_select_placeholder(placeholder)?;
1028 }
1029
1030 if let Some(max_values) = select_menu.max_values {
1031 self::component_select_max_values(usize::from(max_values))?;
1032 }
1033
1034 if let Some(min_values) = select_menu.min_values {
1035 self::component_select_min_values(usize::from(min_values))?;
1036 }
1037
1038 if let Some(default_values) = select_menu.default_values.as_ref() {
1039 component_select_default_values_supported(select_menu.kind)?;
1040 component_select_default_values_count(
1041 select_menu.min_values,
1042 select_menu.max_values,
1043 default_values.len(),
1044 )?;
1045 }
1046
1047 Ok(())
1048}
1049
1050pub fn text_input(
1084 text_input: &TextInput,
1085 label_allowed: bool,
1086) -> Result<(), ComponentValidationError> {
1087 self::component_custom_id(&text_input.custom_id)?;
1088
1089 #[allow(deprecated)]
1090 if let Some(label) = &text_input.label {
1091 if !label_allowed {
1092 return Err(ComponentValidationError {
1093 kind: ComponentValidationErrorType::TextInputDisallowedLabel,
1094 });
1095 }
1096
1097 self::component_text_input_label(label)?;
1098 }
1099
1100 if let Some(max_length) = text_input.max_length {
1101 self::component_text_input_max(max_length)?;
1102 }
1103
1104 if let Some(min_length) = text_input.min_length {
1105 self::component_text_input_min(min_length)?;
1106 }
1107
1108 if let Some(placeholder) = text_input.placeholder.as_ref() {
1109 self::component_text_input_placeholder(placeholder)?;
1110 }
1111
1112 if let Some(value) = text_input.value.as_ref() {
1113 self::component_text_input_value(value)?;
1114 }
1115
1116 Ok(())
1117}
1118
1119const fn component_action_row_components(
1132 components: &[Component],
1133) -> Result<(), ComponentValidationError> {
1134 let count = components.len();
1135
1136 if count > ACTION_ROW_COMPONENT_COUNT {
1137 return Err(ComponentValidationError {
1138 kind: ComponentValidationErrorType::ActionRowComponentCount { count },
1139 });
1140 }
1141
1142 Ok(())
1143}
1144
1145fn component_button_label(label: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1154 let chars = label.as_ref().chars().count();
1155
1156 if chars > COMPONENT_BUTTON_LABEL_LENGTH {
1157 return Err(ComponentValidationError {
1158 kind: ComponentValidationErrorType::ComponentLabelLength { chars },
1159 });
1160 }
1161
1162 Ok(())
1163}
1164
1165fn component_custom_id(custom_id: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1174 let chars = custom_id.as_ref().chars().count();
1175
1176 if chars > COMPONENT_CUSTOM_ID_LENGTH {
1177 return Err(ComponentValidationError {
1178 kind: ComponentValidationErrorType::ComponentCustomIdLength { chars },
1179 });
1180 }
1181
1182 Ok(())
1183}
1184
1185fn component_option_description(
1195 description: impl AsRef<str>,
1196) -> Result<(), ComponentValidationError> {
1197 let chars = description.as_ref().chars().count();
1198
1199 if chars > SELECT_OPTION_DESCRIPTION_LENGTH {
1200 return Err(ComponentValidationError {
1201 kind: ComponentValidationErrorType::SelectOptionDescriptionLength { chars },
1202 });
1203 }
1204
1205 Ok(())
1206}
1207
1208const fn component_select_default_values_supported(
1215 menu_type: SelectMenuType,
1216) -> Result<(), ComponentValidationError> {
1217 if !matches!(
1218 menu_type,
1219 SelectMenuType::User
1220 | SelectMenuType::Role
1221 | SelectMenuType::Mentionable
1222 | SelectMenuType::Channel
1223 ) {
1224 return Err(ComponentValidationError {
1225 kind: ComponentValidationErrorType::SelectUnsupportedDefaultValues { kind: menu_type },
1226 });
1227 }
1228
1229 Ok(())
1230}
1231
1232const fn component_select_default_values_count(
1242 min_values: Option<u8>,
1243 max_values: Option<u8>,
1244 default_values: usize,
1245) -> Result<(), ComponentValidationError> {
1246 if let Some(min) = min_values {
1247 let min = min as usize;
1248 if default_values < min {
1249 return Err(ComponentValidationError {
1250 kind: ComponentValidationErrorType::SelectNotEnoughDefaultValues {
1251 provided: default_values,
1252 min,
1253 },
1254 });
1255 }
1256 }
1257 if let Some(max) = max_values {
1258 let max = max as usize;
1259 if default_values > max {
1260 return Err(ComponentValidationError {
1261 kind: ComponentValidationErrorType::SelectTooManyDefaultValues {
1262 provided: default_values,
1263 max,
1264 },
1265 });
1266 }
1267 }
1268
1269 Ok(())
1270}
1271
1272const fn component_select_max_values(count: usize) -> Result<(), ComponentValidationError> {
1284 if count > SELECT_MAXIMUM_VALUES_LIMIT {
1285 return Err(ComponentValidationError {
1286 kind: ComponentValidationErrorType::SelectMaximumValuesCount { count },
1287 });
1288 }
1289
1290 if count < SELECT_MAXIMUM_VALUES_REQUIREMENT {
1291 return Err(ComponentValidationError {
1292 kind: ComponentValidationErrorType::SelectMaximumValuesCount { count },
1293 });
1294 }
1295
1296 Ok(())
1297}
1298
1299const fn component_select_min_values(count: usize) -> Result<(), ComponentValidationError> {
1310 if count > SELECT_MINIMUM_VALUES_LIMIT {
1311 return Err(ComponentValidationError {
1312 kind: ComponentValidationErrorType::SelectMinimumValuesCount { count },
1313 });
1314 }
1315
1316 Ok(())
1317}
1318
1319fn component_select_option_label(label: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1329 let chars = label.as_ref().chars().count();
1330
1331 if chars > SELECT_OPTION_LABEL_LENGTH {
1332 return Err(ComponentValidationError {
1333 kind: ComponentValidationErrorType::SelectOptionLabelLength { chars },
1334 });
1335 }
1336
1337 Ok(())
1338}
1339
1340fn component_select_option_value(value: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1350 let chars = value.as_ref().chars().count();
1351
1352 if chars > SELECT_OPTION_VALUE_LENGTH {
1353 return Err(ComponentValidationError {
1354 kind: ComponentValidationErrorType::SelectOptionValueLength { chars },
1355 });
1356 }
1357
1358 Ok(())
1359}
1360
1361const fn component_select_options(
1376 options: &[SelectMenuOption],
1377) -> Result<(), ComponentValidationError> {
1378 let count = options.len();
1379
1380 if count > SELECT_OPTION_COUNT {
1381 return Err(ComponentValidationError {
1382 kind: ComponentValidationErrorType::SelectOptionCount { count },
1383 });
1384 }
1385
1386 Ok(())
1387}
1388
1389fn component_select_placeholder(
1399 placeholder: impl AsRef<str>,
1400) -> Result<(), ComponentValidationError> {
1401 let chars = placeholder.as_ref().chars().count();
1402
1403 if chars > SELECT_PLACEHOLDER_LENGTH {
1404 return Err(ComponentValidationError {
1405 kind: ComponentValidationErrorType::SelectPlaceholderLength { chars },
1406 });
1407 }
1408
1409 Ok(())
1410}
1411
1412fn component_text_input_label(label: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1424 let len = label.as_ref().chars().count();
1425
1426 if (TEXT_INPUT_LABEL_MIN..=TEXT_INPUT_LABEL_MAX).contains(&len) {
1427 Ok(())
1428 } else {
1429 Err(ComponentValidationError {
1430 kind: ComponentValidationErrorType::TextInputLabelLength { len },
1431 })
1432 }
1433}
1434
1435const fn component_text_input_max(len: u16) -> Result<(), ComponentValidationError> {
1444 let len = len as usize;
1445
1446 if len >= TEXT_INPUT_LENGTH_MIN && len <= TEXT_INPUT_LENGTH_MAX {
1447 Ok(())
1448 } else {
1449 Err(ComponentValidationError {
1450 kind: ComponentValidationErrorType::TextInputMaxLength { len },
1451 })
1452 }
1453}
1454
1455const fn component_text_input_min(len: u16) -> Result<(), ComponentValidationError> {
1464 let len = len as usize;
1465
1466 if len <= TEXT_INPUT_LENGTH_MAX {
1467 Ok(())
1468 } else {
1469 Err(ComponentValidationError {
1470 kind: ComponentValidationErrorType::TextInputMinLength { len },
1471 })
1472 }
1473}
1474
1475fn component_text_input_placeholder(
1487 placeholder: impl AsRef<str>,
1488) -> Result<(), ComponentValidationError> {
1489 let chars = placeholder.as_ref().chars().count();
1490
1491 if chars <= TEXT_INPUT_PLACEHOLDER_MAX {
1492 Ok(())
1493 } else {
1494 Err(ComponentValidationError {
1495 kind: ComponentValidationErrorType::TextInputPlaceholderLength { chars },
1496 })
1497 }
1498}
1499
1500fn component_text_input_value(value: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1509 let chars = value.as_ref().chars().count();
1510
1511 if chars <= TEXT_INPUT_LENGTH_MAX {
1512 Ok(())
1513 } else {
1514 Err(ComponentValidationError {
1515 kind: ComponentValidationErrorType::TextInputValueLength { chars },
1516 })
1517 }
1518}
1519
1520#[allow(clippy::non_ascii_literal)]
1521#[cfg(test)]
1522mod tests {
1523 use super::*;
1524 use static_assertions::{assert_fields, assert_impl_all};
1525 use twilight_model::channel::message::EmojiReactionType;
1526
1527 assert_fields!(ComponentValidationErrorType::ActionRowComponentCount: count);
1528 assert_fields!(ComponentValidationErrorType::ComponentCount: count);
1529 assert_fields!(ComponentValidationErrorType::ComponentCustomIdLength: chars);
1530 assert_fields!(ComponentValidationErrorType::ComponentLabelLength: chars);
1531 assert_fields!(ComponentValidationErrorType::InvalidChildComponent: kind);
1532 assert_fields!(ComponentValidationErrorType::InvalidRootComponent: kind);
1533 assert_fields!(ComponentValidationErrorType::SelectMaximumValuesCount: count);
1534 assert_fields!(ComponentValidationErrorType::SelectMinimumValuesCount: count);
1535 assert_fields!(ComponentValidationErrorType::SelectOptionDescriptionLength: chars);
1536 assert_fields!(ComponentValidationErrorType::SelectOptionLabelLength: chars);
1537 assert_fields!(ComponentValidationErrorType::SelectOptionValueLength: chars);
1538 assert_fields!(ComponentValidationErrorType::SelectPlaceholderLength: chars);
1539 assert_impl_all!(ComponentValidationErrorType: Debug, Send, Sync);
1540 assert_impl_all!(ComponentValidationError: Debug, Send, Sync);
1541
1542 const ALL_BUTTON_STYLES: &[ButtonStyle] = &[
1544 ButtonStyle::Primary,
1545 ButtonStyle::Secondary,
1546 ButtonStyle::Success,
1547 ButtonStyle::Danger,
1548 ButtonStyle::Link,
1549 ButtonStyle::Premium,
1550 ];
1551
1552 #[test]
1553 fn component_action_row() {
1554 let button = Button {
1555 custom_id: None,
1556 disabled: false,
1557 emoji: Some(EmojiReactionType::Unicode {
1558 name: "📚".into()
1559 }),
1560 label: Some("Read".into()),
1561 style: ButtonStyle::Link,
1562 url: Some("https://abebooks.com".into()),
1563 sku_id: None,
1564 id: None,
1565 };
1566
1567 let select_menu = SelectMenu {
1568 channel_types: None,
1569 custom_id: "custom id 2".into(),
1570 disabled: false,
1571 default_values: None,
1572 kind: SelectMenuType::Text,
1573 max_values: Some(2),
1574 min_values: Some(1),
1575 options: Some(Vec::from([SelectMenuOption {
1576 default: true,
1577 description: Some("Book 1 of the Expanse".into()),
1578 emoji: None,
1579 label: "Leviathan Wakes".into(),
1580 value: "9780316129084".into(),
1581 }])),
1582 placeholder: Some("Choose a book".into()),
1583 id: None,
1584 required: None,
1585 };
1586
1587 let action_row = ActionRow {
1588 components: Vec::from([
1589 Component::SelectMenu(select_menu.clone()),
1590 Component::Button(button),
1591 ]),
1592 id: None,
1593 };
1594
1595 assert!(component_v1(&Component::ActionRow(action_row.clone())).is_ok());
1596
1597 assert!(component_v1(&Component::SelectMenu(select_menu.clone())).is_err());
1598
1599 assert!(super::action_row(&action_row, false).is_ok());
1600
1601 let invalid_action_row = Component::ActionRow(ActionRow {
1602 components: Vec::from([
1603 Component::SelectMenu(select_menu.clone()),
1604 Component::SelectMenu(select_menu.clone()),
1605 Component::SelectMenu(select_menu.clone()),
1606 Component::SelectMenu(select_menu.clone()),
1607 Component::SelectMenu(select_menu.clone()),
1608 Component::SelectMenu(select_menu),
1609 ]),
1610 id: None,
1611 });
1612
1613 assert!(component_v1(&invalid_action_row).is_err());
1614 }
1615
1616 #[test]
1619 fn button_conflict() {
1620 let button = Button {
1621 custom_id: Some("a".to_owned()),
1622 disabled: false,
1623 emoji: None,
1624 label: None,
1625 style: ButtonStyle::Primary,
1626 url: Some("https://twilight.rs".to_owned()),
1627 sku_id: None,
1628 id: None,
1629 };
1630
1631 assert!(matches!(
1632 super::button(&button),
1633 Err(ComponentValidationError {
1634 kind: ComponentValidationErrorType::ButtonConflict,
1635 }),
1636 ));
1637 }
1638
1639 #[test]
1642 fn button_style() {
1643 for style in ALL_BUTTON_STYLES {
1644 let button = Button {
1645 custom_id: None,
1646 disabled: false,
1647 emoji: None,
1648 label: Some("some label".to_owned()),
1649 style: *style,
1650 url: None,
1651 sku_id: None,
1652 id: None,
1653 };
1654
1655 assert!(matches!(
1656 super::button(&button),
1657 Err(ComponentValidationError {
1658 kind: ComponentValidationErrorType::ButtonStyle {
1659 style: error_style,
1660 }
1661 })
1662 if error_style == *style
1663 ));
1664 }
1665 }
1666
1667 #[test]
1668 fn component_label() {
1669 assert!(component_button_label("").is_ok());
1670 assert!(component_button_label("a").is_ok());
1671 assert!(component_button_label("a".repeat(80)).is_ok());
1672
1673 assert!(component_button_label("a".repeat(81)).is_err());
1674 }
1675
1676 #[test]
1677 fn component_custom_id_length() {
1678 assert!(component_custom_id("").is_ok());
1679 assert!(component_custom_id("a").is_ok());
1680 assert!(component_custom_id("a".repeat(100)).is_ok());
1681
1682 assert!(component_custom_id("a".repeat(101)).is_err());
1683 }
1684
1685 #[test]
1686 fn component_option_description_length() {
1687 assert!(component_option_description("").is_ok());
1688 assert!(component_option_description("a").is_ok());
1689 assert!(component_option_description("a".repeat(100)).is_ok());
1690
1691 assert!(component_option_description("a".repeat(101)).is_err());
1692 }
1693
1694 #[test]
1695 fn component_select_default_values_support() {
1696 assert!(component_select_default_values_supported(SelectMenuType::User).is_ok());
1697 assert!(component_select_default_values_supported(SelectMenuType::Role).is_ok());
1698 assert!(component_select_default_values_supported(SelectMenuType::Mentionable).is_ok());
1699 assert!(component_select_default_values_supported(SelectMenuType::Channel).is_ok());
1700
1701 assert!(component_select_default_values_supported(SelectMenuType::Text).is_err());
1702 }
1703
1704 #[test]
1705 fn component_select_num_default_values() {
1706 assert!(component_select_default_values_count(None, None, 0).is_ok());
1707 assert!(component_select_default_values_count(None, None, 1).is_ok());
1708 assert!(component_select_default_values_count(Some(1), None, 5).is_ok());
1709 assert!(component_select_default_values_count(Some(5), None, 5).is_ok());
1710 assert!(component_select_default_values_count(None, Some(5), 5).is_ok());
1711 assert!(component_select_default_values_count(None, Some(10), 5).is_ok());
1712 assert!(component_select_default_values_count(Some(5), Some(5), 5).is_ok());
1713 assert!(component_select_default_values_count(Some(1), Some(10), 5).is_ok());
1714
1715 assert!(component_select_default_values_count(Some(2), None, 1).is_err());
1716 assert!(component_select_default_values_count(None, Some(1), 2).is_err());
1717 assert!(component_select_default_values_count(Some(1), Some(1), 2).is_err());
1718 assert!(component_select_default_values_count(Some(2), Some(2), 1).is_err());
1719 }
1720
1721 #[test]
1722 fn component_select_max_values_count() {
1723 assert!(component_select_max_values(1).is_ok());
1724 assert!(component_select_max_values(25).is_ok());
1725
1726 assert!(component_select_max_values(0).is_err());
1727 assert!(component_select_max_values(26).is_err());
1728 }
1729
1730 #[test]
1731 fn component_select_min_values_count() {
1732 assert!(component_select_min_values(1).is_ok());
1733 assert!(component_select_min_values(25).is_ok());
1734
1735 assert!(component_select_min_values(26).is_err());
1736 }
1737
1738 #[test]
1739 fn component_select_option_value_length() {
1740 assert!(component_select_option_value("a").is_ok());
1741 assert!(component_select_option_value("a".repeat(100)).is_ok());
1742
1743 assert!(component_select_option_value("a".repeat(101)).is_err());
1744 }
1745
1746 #[test]
1747 fn component_select_options_count() {
1748 let select_menu_options = Vec::from([SelectMenuOption {
1749 default: false,
1750 description: None,
1751 emoji: None,
1752 label: "label".into(),
1753 value: "value".into(),
1754 }]);
1755
1756 assert!(component_select_options(&select_menu_options).is_ok());
1757
1758 let select_menu_options_25 = select_menu_options
1759 .iter()
1760 .cloned()
1761 .cycle()
1762 .take(25)
1763 .collect::<Vec<SelectMenuOption>>();
1764
1765 assert!(component_select_options(&select_menu_options_25).is_ok());
1766
1767 let select_menu_options_26 = select_menu_options
1768 .iter()
1769 .cloned()
1770 .cycle()
1771 .take(26)
1772 .collect::<Vec<SelectMenuOption>>();
1773
1774 assert!(component_select_options(&select_menu_options_26).is_err());
1775 }
1776
1777 #[test]
1778 fn component_select_placeholder_length() {
1779 assert!(component_select_placeholder("").is_ok());
1780 assert!(component_select_placeholder("a").is_ok());
1781 assert!(component_select_placeholder("a".repeat(150)).is_ok());
1782
1783 assert!(component_select_placeholder("a".repeat(151)).is_err());
1784 }
1785
1786 #[test]
1789 fn component_text_input_label_length() {
1790 let label = "🪄".repeat(45);
1792 assert_eq!(label.chars().count(), 45);
1793 #[allow(clippy::disallowed_methods)] {
1796 assert_eq!(label.len(), 180);
1797 }
1798 assert!(component_text_input_label(label).is_ok());
1799
1800 let label = "🪄".repeat(46);
1802 let err = component_text_input_label(label).unwrap_err();
1803 assert!(matches!(
1804 err.kind(),
1805 ComponentValidationErrorType::TextInputLabelLength { len } if *len == 46
1806 ));
1807 }
1808
1809 #[test]
1810 fn component_text_input_max_count() {
1811 assert!(component_text_input_max(1).is_ok());
1812 assert!(component_text_input_max(4000).is_ok());
1813
1814 assert!(component_text_input_max(0).is_err());
1815 assert!(component_text_input_max(4001).is_err());
1816 }
1817
1818 #[test]
1819 fn component_text_input_min_count() {
1820 assert!(component_text_input_min(0).is_ok());
1821 assert!(component_text_input_min(1).is_ok());
1822 assert!(component_text_input_min(4000).is_ok());
1823
1824 assert!(component_text_input_min(4001).is_err());
1825 }
1826
1827 #[test]
1828 fn component_text_input_placeholder_length() {
1829 assert!(component_text_input_placeholder("").is_ok());
1830 assert!(component_text_input_placeholder("a").is_ok());
1831 assert!(component_text_input_placeholder("a".repeat(100)).is_ok());
1832
1833 assert!(component_text_input_placeholder("a".repeat(101)).is_err());
1834 }
1835
1836 #[test]
1837 fn component_text_input_value() {
1838 assert!(component_text_input_min(0).is_ok());
1839 assert!(component_text_input_min(1).is_ok());
1840 assert!(component_text_input_min(4000).is_ok());
1841
1842 assert!(component_text_input_min(4001).is_err());
1843 }
1844}