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,
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 }
490 }
491}
492
493impl Error for ComponentValidationError {}
494
495#[derive(Debug)]
497#[non_exhaustive]
498pub enum ComponentValidationErrorType {
499 ActionRowComponentCount {
502 count: usize,
504 },
505 ButtonConflict,
507 ButtonStyle {
512 style: ButtonStyle,
514 },
515 CheckboxGroupMaximumValuesCount {
519 count: usize,
521 },
522 CheckboxGroupOptionCount {
525 count: usize,
527 },
528 CheckboxGroupMinimumValuesCount {
530 count: usize,
532 },
533 CheckboxGroupOptionsMissing,
535 CheckboxGroupRequiredWithNoMin,
537 ComponentCount {
540 count: usize,
542 },
543 ComponentCustomIdLength {
546 chars: usize,
548 },
549 ComponentLabelLength {
551 chars: usize,
553 },
554 InvalidChildComponent {
556 kind: ComponentType,
558 },
559 InvalidRootComponent {
561 kind: ComponentType,
563 },
564 SelectMaximumValuesCount {
568 count: usize,
570 },
571 SelectMinimumValuesCount {
574 count: usize,
576 },
577 SelectNotEnoughDefaultValues {
579 provided: usize,
581 min: usize,
583 },
584 SelectOptionsMissing,
588 SelectOptionCount {
591 count: usize,
593 },
594 SelectOptionDescriptionLength {
597 chars: usize,
599 },
600 SelectOptionLabelLength {
603 chars: usize,
605 },
606 SelectOptionValueLength {
609 chars: usize,
611 },
612 SelectPlaceholderLength {
615 chars: usize,
617 },
618 SelectTooManyDefaultValues {
620 provided: usize,
622 max: usize,
624 },
625 SelectUnsupportedDefaultValues {
627 kind: SelectMenuType,
629 },
630 TextInputLabelLength {
632 len: usize,
634 },
635 TextInputMaxLength {
637 len: usize,
639 },
640 TextInputMinLength {
642 len: usize,
644 },
645 TextInputPlaceholderLength {
648 chars: usize,
650 },
651 TextInputValueLength {
654 chars: usize,
656 },
657 TextInputDisallowedLabel,
659 DisallowedV2Components,
661 DisallowedChildren,
663 TextDisplayContentTooLong {
665 len: usize,
667 },
668 MediaGalleryItemCountOutOfRange {
670 count: usize,
672 },
673 MediaGalleryItemDescriptionTooLong {
675 len: usize,
677 },
678 SectionComponentCountOutOfRange {
680 count: usize,
682 },
683 SelectDisallowedDisabled,
686 ThumbnailDescriptionTooLong {
688 len: usize,
690 },
691 LabelLabelTooLong {
693 len: usize,
695 },
696 LabelDescriptionTooLong {
698 len: usize,
700 },
701 FileUploadMaximumValuesCount {
704 count: u8,
708 },
709 FileUploadMinimumValuesCount {
712 count: u8,
716 },
717}
718
719pub fn component_v1(component: &Component) -> Result<(), ComponentValidationError> {
739 match component {
740 Component::ActionRow(action_row) => self::action_row(action_row, false)?,
741 other => {
742 return Err(ComponentValidationError {
743 kind: ComponentValidationErrorType::InvalidRootComponent { kind: other.kind() },
744 });
745 }
746 }
747
748 Ok(())
749}
750
751#[deprecated(note = "Use component_v1 for old components and component_v2 for new ones")]
769pub fn component(component: &Component) -> Result<(), ComponentValidationError> {
770 component_v1(component)
771}
772
773pub fn action_row(action_row: &ActionRow, is_v2: bool) -> Result<(), ComponentValidationError> {
805 self::component_action_row_components(&action_row.components)?;
806
807 for component in &action_row.components {
808 match component {
809 Component::ActionRow(_) | Component::Label(_) => {
810 return Err(ComponentValidationError {
811 kind: ComponentValidationErrorType::InvalidChildComponent {
812 kind: component.kind(),
813 },
814 });
815 }
816 Component::Button(button) => self::button(button)?,
817 Component::SelectMenu(select_menu) => self::select_menu(select_menu, true)?,
818 Component::TextInput(text_input) => self::text_input(text_input, true)?,
819 Component::Unknown(unknown) => {
820 return Err(ComponentValidationError {
821 kind: ComponentValidationErrorType::InvalidChildComponent {
822 kind: ComponentType::Unknown(*unknown),
823 },
824 });
825 }
826
827 Component::TextDisplay(_)
828 | Component::MediaGallery(_)
829 | Component::Separator(_)
830 | Component::File(_)
831 | Component::Section(_)
832 | Component::Container(_)
833 | Component::Thumbnail(_)
834 | Component::Checkbox(_)
835 | Component::CheckboxGroup(_)
836 | Component::FileUpload(_) => {
837 return Err(ComponentValidationError {
838 kind: if is_v2 {
839 ComponentValidationErrorType::DisallowedChildren
840 } else {
841 ComponentValidationErrorType::DisallowedV2Components
842 },
843 });
844 }
845 }
846 }
847
848 Ok(())
849}
850
851pub fn button(button: &Button) -> Result<(), ComponentValidationError> {
873 let has_custom_id = button.custom_id.is_some();
874 let has_emoji = button.emoji.is_some();
875 let has_label = button.label.is_some();
876 let has_sku_id = button.sku_id.is_some();
877 let has_url = button.url.is_some();
878
879 if has_custom_id && has_url {
882 return Err(ComponentValidationError {
883 kind: ComponentValidationErrorType::ButtonConflict,
884 });
885 }
886
887 let is_premium = button.style == ButtonStyle::Premium;
892 if is_premium && (has_custom_id || has_url || has_label || has_emoji || !has_sku_id) {
893 return Err(ComponentValidationError {
894 kind: ComponentValidationErrorType::ButtonStyle {
895 style: button.style,
896 },
897 });
898 }
899
900 let is_link = button.style == ButtonStyle::Link;
905
906 if (is_link && !has_url) || (!is_link && !has_custom_id) {
907 return Err(ComponentValidationError {
908 kind: ComponentValidationErrorType::ButtonStyle {
909 style: button.style,
910 },
911 });
912 }
913
914 if let Some(custom_id) = button.custom_id.as_ref() {
915 self::component_custom_id(custom_id)?;
916 }
917
918 if let Some(label) = button.label.as_ref() {
919 self::component_button_label(label)?;
920 }
921
922 Ok(())
923}
924
925pub fn select_menu(
983 select_menu: &SelectMenu,
984 disabled_allowed: bool,
985) -> Result<(), ComponentValidationError> {
986 self::component_custom_id(&select_menu.custom_id)?;
987
988 if !disabled_allowed && select_menu.disabled {
989 return Err(ComponentValidationError {
990 kind: ComponentValidationErrorType::SelectDisallowedDisabled,
991 });
992 }
993
994 if let SelectMenuType::Text = &select_menu.kind {
996 let options = select_menu
997 .options
998 .as_ref()
999 .ok_or(ComponentValidationError {
1000 kind: ComponentValidationErrorType::SelectOptionsMissing,
1001 })?;
1002 for option in options {
1003 component_select_option_label(&option.label)?;
1004 component_select_option_value(&option.value)?;
1005
1006 if let Some(description) = option.description.as_ref() {
1007 component_option_description(description)?;
1008 }
1009 }
1010 component_select_options(options)?;
1011 }
1012
1013 if let Some(placeholder) = select_menu.placeholder.as_ref() {
1014 self::component_select_placeholder(placeholder)?;
1015 }
1016
1017 if let Some(max_values) = select_menu.max_values {
1018 self::component_select_max_values(usize::from(max_values))?;
1019 }
1020
1021 if let Some(min_values) = select_menu.min_values {
1022 self::component_select_min_values(usize::from(min_values))?;
1023 }
1024
1025 if let Some(default_values) = select_menu.default_values.as_ref() {
1026 component_select_default_values_supported(select_menu.kind)?;
1027 component_select_default_values_count(
1028 select_menu.min_values,
1029 select_menu.max_values,
1030 default_values.len(),
1031 )?;
1032 }
1033
1034 Ok(())
1035}
1036
1037pub fn text_input(
1071 text_input: &TextInput,
1072 label_allowed: bool,
1073) -> Result<(), ComponentValidationError> {
1074 self::component_custom_id(&text_input.custom_id)?;
1075
1076 #[allow(deprecated)]
1077 if let Some(label) = &text_input.label {
1078 if !label_allowed {
1079 return Err(ComponentValidationError {
1080 kind: ComponentValidationErrorType::TextInputDisallowedLabel,
1081 });
1082 }
1083
1084 self::component_text_input_label(label)?;
1085 }
1086
1087 if let Some(max_length) = text_input.max_length {
1088 self::component_text_input_max(max_length)?;
1089 }
1090
1091 if let Some(min_length) = text_input.min_length {
1092 self::component_text_input_min(min_length)?;
1093 }
1094
1095 if let Some(placeholder) = text_input.placeholder.as_ref() {
1096 self::component_text_input_placeholder(placeholder)?;
1097 }
1098
1099 if let Some(value) = text_input.value.as_ref() {
1100 self::component_text_input_value(value)?;
1101 }
1102
1103 Ok(())
1104}
1105
1106const fn component_action_row_components(
1119 components: &[Component],
1120) -> Result<(), ComponentValidationError> {
1121 let count = components.len();
1122
1123 if count > COMPONENT_COUNT {
1124 return Err(ComponentValidationError {
1125 kind: ComponentValidationErrorType::ActionRowComponentCount { count },
1126 });
1127 }
1128
1129 Ok(())
1130}
1131
1132fn component_button_label(label: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1141 let chars = label.as_ref().chars().count();
1142
1143 if chars > COMPONENT_BUTTON_LABEL_LENGTH {
1144 return Err(ComponentValidationError {
1145 kind: ComponentValidationErrorType::ComponentLabelLength { chars },
1146 });
1147 }
1148
1149 Ok(())
1150}
1151
1152fn component_custom_id(custom_id: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1161 let chars = custom_id.as_ref().chars().count();
1162
1163 if chars > COMPONENT_CUSTOM_ID_LENGTH {
1164 return Err(ComponentValidationError {
1165 kind: ComponentValidationErrorType::ComponentCustomIdLength { chars },
1166 });
1167 }
1168
1169 Ok(())
1170}
1171
1172fn component_option_description(
1182 description: impl AsRef<str>,
1183) -> Result<(), ComponentValidationError> {
1184 let chars = description.as_ref().chars().count();
1185
1186 if chars > SELECT_OPTION_DESCRIPTION_LENGTH {
1187 return Err(ComponentValidationError {
1188 kind: ComponentValidationErrorType::SelectOptionDescriptionLength { chars },
1189 });
1190 }
1191
1192 Ok(())
1193}
1194
1195const fn component_select_default_values_supported(
1202 menu_type: SelectMenuType,
1203) -> Result<(), ComponentValidationError> {
1204 if !matches!(
1205 menu_type,
1206 SelectMenuType::User
1207 | SelectMenuType::Role
1208 | SelectMenuType::Mentionable
1209 | SelectMenuType::Channel
1210 ) {
1211 return Err(ComponentValidationError {
1212 kind: ComponentValidationErrorType::SelectUnsupportedDefaultValues { kind: menu_type },
1213 });
1214 }
1215
1216 Ok(())
1217}
1218
1219const fn component_select_default_values_count(
1229 min_values: Option<u8>,
1230 max_values: Option<u8>,
1231 default_values: usize,
1232) -> Result<(), ComponentValidationError> {
1233 if let Some(min) = min_values {
1234 let min = min as usize;
1235 if default_values < min {
1236 return Err(ComponentValidationError {
1237 kind: ComponentValidationErrorType::SelectNotEnoughDefaultValues {
1238 provided: default_values,
1239 min,
1240 },
1241 });
1242 }
1243 }
1244 if let Some(max) = max_values {
1245 let max = max as usize;
1246 if default_values > max {
1247 return Err(ComponentValidationError {
1248 kind: ComponentValidationErrorType::SelectTooManyDefaultValues {
1249 provided: default_values,
1250 max,
1251 },
1252 });
1253 }
1254 }
1255
1256 Ok(())
1257}
1258
1259const fn component_select_max_values(count: usize) -> Result<(), ComponentValidationError> {
1271 if count > SELECT_MAXIMUM_VALUES_LIMIT {
1272 return Err(ComponentValidationError {
1273 kind: ComponentValidationErrorType::SelectMaximumValuesCount { count },
1274 });
1275 }
1276
1277 if count < SELECT_MAXIMUM_VALUES_REQUIREMENT {
1278 return Err(ComponentValidationError {
1279 kind: ComponentValidationErrorType::SelectMaximumValuesCount { count },
1280 });
1281 }
1282
1283 Ok(())
1284}
1285
1286const fn component_select_min_values(count: usize) -> Result<(), ComponentValidationError> {
1297 if count > SELECT_MINIMUM_VALUES_LIMIT {
1298 return Err(ComponentValidationError {
1299 kind: ComponentValidationErrorType::SelectMinimumValuesCount { count },
1300 });
1301 }
1302
1303 Ok(())
1304}
1305
1306fn component_select_option_label(label: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1316 let chars = label.as_ref().chars().count();
1317
1318 if chars > SELECT_OPTION_LABEL_LENGTH {
1319 return Err(ComponentValidationError {
1320 kind: ComponentValidationErrorType::SelectOptionLabelLength { chars },
1321 });
1322 }
1323
1324 Ok(())
1325}
1326
1327fn component_select_option_value(value: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1337 let chars = value.as_ref().chars().count();
1338
1339 if chars > SELECT_OPTION_VALUE_LENGTH {
1340 return Err(ComponentValidationError {
1341 kind: ComponentValidationErrorType::SelectOptionValueLength { chars },
1342 });
1343 }
1344
1345 Ok(())
1346}
1347
1348const fn component_select_options(
1363 options: &[SelectMenuOption],
1364) -> Result<(), ComponentValidationError> {
1365 let count = options.len();
1366
1367 if count > SELECT_OPTION_COUNT {
1368 return Err(ComponentValidationError {
1369 kind: ComponentValidationErrorType::SelectOptionCount { count },
1370 });
1371 }
1372
1373 Ok(())
1374}
1375
1376fn component_select_placeholder(
1386 placeholder: impl AsRef<str>,
1387) -> Result<(), ComponentValidationError> {
1388 let chars = placeholder.as_ref().chars().count();
1389
1390 if chars > SELECT_PLACEHOLDER_LENGTH {
1391 return Err(ComponentValidationError {
1392 kind: ComponentValidationErrorType::SelectPlaceholderLength { chars },
1393 });
1394 }
1395
1396 Ok(())
1397}
1398
1399fn component_text_input_label(label: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1411 let len = label.as_ref().len();
1412
1413 if (TEXT_INPUT_LABEL_MIN..=TEXT_INPUT_LABEL_MAX).contains(&len) {
1414 Ok(())
1415 } else {
1416 Err(ComponentValidationError {
1417 kind: ComponentValidationErrorType::TextInputLabelLength { len },
1418 })
1419 }
1420}
1421
1422const fn component_text_input_max(len: u16) -> Result<(), ComponentValidationError> {
1431 let len = len as usize;
1432
1433 if len >= TEXT_INPUT_LENGTH_MIN && len <= TEXT_INPUT_LENGTH_MAX {
1434 Ok(())
1435 } else {
1436 Err(ComponentValidationError {
1437 kind: ComponentValidationErrorType::TextInputMaxLength { len },
1438 })
1439 }
1440}
1441
1442const fn component_text_input_min(len: u16) -> Result<(), ComponentValidationError> {
1451 let len = len as usize;
1452
1453 if len <= TEXT_INPUT_LENGTH_MAX {
1454 Ok(())
1455 } else {
1456 Err(ComponentValidationError {
1457 kind: ComponentValidationErrorType::TextInputMinLength { len },
1458 })
1459 }
1460}
1461
1462fn component_text_input_placeholder(
1474 placeholder: impl AsRef<str>,
1475) -> Result<(), ComponentValidationError> {
1476 let chars = placeholder.as_ref().chars().count();
1477
1478 if chars <= TEXT_INPUT_PLACEHOLDER_MAX {
1479 Ok(())
1480 } else {
1481 Err(ComponentValidationError {
1482 kind: ComponentValidationErrorType::TextInputPlaceholderLength { chars },
1483 })
1484 }
1485}
1486
1487fn component_text_input_value(value: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1496 let chars = value.as_ref().chars().count();
1497
1498 if chars <= TEXT_INPUT_LENGTH_MAX {
1499 Ok(())
1500 } else {
1501 Err(ComponentValidationError {
1502 kind: ComponentValidationErrorType::TextInputValueLength { chars },
1503 })
1504 }
1505}
1506
1507#[allow(clippy::non_ascii_literal)]
1508#[cfg(test)]
1509mod tests {
1510 use super::*;
1511 use static_assertions::{assert_fields, assert_impl_all};
1512 use twilight_model::channel::message::EmojiReactionType;
1513
1514 assert_fields!(ComponentValidationErrorType::ActionRowComponentCount: count);
1515 assert_fields!(ComponentValidationErrorType::ComponentCount: count);
1516 assert_fields!(ComponentValidationErrorType::ComponentCustomIdLength: chars);
1517 assert_fields!(ComponentValidationErrorType::ComponentLabelLength: chars);
1518 assert_fields!(ComponentValidationErrorType::InvalidChildComponent: kind);
1519 assert_fields!(ComponentValidationErrorType::InvalidRootComponent: kind);
1520 assert_fields!(ComponentValidationErrorType::SelectMaximumValuesCount: count);
1521 assert_fields!(ComponentValidationErrorType::SelectMinimumValuesCount: count);
1522 assert_fields!(ComponentValidationErrorType::SelectOptionDescriptionLength: chars);
1523 assert_fields!(ComponentValidationErrorType::SelectOptionLabelLength: chars);
1524 assert_fields!(ComponentValidationErrorType::SelectOptionValueLength: chars);
1525 assert_fields!(ComponentValidationErrorType::SelectPlaceholderLength: chars);
1526 assert_impl_all!(ComponentValidationErrorType: Debug, Send, Sync);
1527 assert_impl_all!(ComponentValidationError: Debug, Send, Sync);
1528
1529 const ALL_BUTTON_STYLES: &[ButtonStyle] = &[
1531 ButtonStyle::Primary,
1532 ButtonStyle::Secondary,
1533 ButtonStyle::Success,
1534 ButtonStyle::Danger,
1535 ButtonStyle::Link,
1536 ButtonStyle::Premium,
1537 ];
1538
1539 #[test]
1540 fn component_action_row() {
1541 let button = Button {
1542 custom_id: None,
1543 disabled: false,
1544 emoji: Some(EmojiReactionType::Unicode {
1545 name: "📚".into()
1546 }),
1547 label: Some("Read".into()),
1548 style: ButtonStyle::Link,
1549 url: Some("https://abebooks.com".into()),
1550 sku_id: None,
1551 id: None,
1552 };
1553
1554 let select_menu = SelectMenu {
1555 channel_types: None,
1556 custom_id: "custom id 2".into(),
1557 disabled: false,
1558 default_values: None,
1559 kind: SelectMenuType::Text,
1560 max_values: Some(2),
1561 min_values: Some(1),
1562 options: Some(Vec::from([SelectMenuOption {
1563 default: true,
1564 description: Some("Book 1 of the Expanse".into()),
1565 emoji: None,
1566 label: "Leviathan Wakes".into(),
1567 value: "9780316129084".into(),
1568 }])),
1569 placeholder: Some("Choose a book".into()),
1570 id: None,
1571 required: None,
1572 };
1573
1574 let action_row = ActionRow {
1575 components: Vec::from([
1576 Component::SelectMenu(select_menu.clone()),
1577 Component::Button(button),
1578 ]),
1579 id: None,
1580 };
1581
1582 assert!(component_v1(&Component::ActionRow(action_row.clone())).is_ok());
1583
1584 assert!(component_v1(&Component::SelectMenu(select_menu.clone())).is_err());
1585
1586 assert!(super::action_row(&action_row, false).is_ok());
1587
1588 let invalid_action_row = Component::ActionRow(ActionRow {
1589 components: Vec::from([
1590 Component::SelectMenu(select_menu.clone()),
1591 Component::SelectMenu(select_menu.clone()),
1592 Component::SelectMenu(select_menu.clone()),
1593 Component::SelectMenu(select_menu.clone()),
1594 Component::SelectMenu(select_menu.clone()),
1595 Component::SelectMenu(select_menu),
1596 ]),
1597 id: None,
1598 });
1599
1600 assert!(component_v1(&invalid_action_row).is_err());
1601 }
1602
1603 #[test]
1606 fn button_conflict() {
1607 let button = Button {
1608 custom_id: Some("a".to_owned()),
1609 disabled: false,
1610 emoji: None,
1611 label: None,
1612 style: ButtonStyle::Primary,
1613 url: Some("https://twilight.rs".to_owned()),
1614 sku_id: None,
1615 id: None,
1616 };
1617
1618 assert!(matches!(
1619 super::button(&button),
1620 Err(ComponentValidationError {
1621 kind: ComponentValidationErrorType::ButtonConflict,
1622 }),
1623 ));
1624 }
1625
1626 #[test]
1629 fn button_style() {
1630 for style in ALL_BUTTON_STYLES {
1631 let button = Button {
1632 custom_id: None,
1633 disabled: false,
1634 emoji: None,
1635 label: Some("some label".to_owned()),
1636 style: *style,
1637 url: None,
1638 sku_id: None,
1639 id: None,
1640 };
1641
1642 assert!(matches!(
1643 super::button(&button),
1644 Err(ComponentValidationError {
1645 kind: ComponentValidationErrorType::ButtonStyle {
1646 style: error_style,
1647 }
1648 })
1649 if error_style == *style
1650 ));
1651 }
1652 }
1653
1654 #[test]
1655 fn component_label() {
1656 assert!(component_button_label("").is_ok());
1657 assert!(component_button_label("a").is_ok());
1658 assert!(component_button_label("a".repeat(80)).is_ok());
1659
1660 assert!(component_button_label("a".repeat(81)).is_err());
1661 }
1662
1663 #[test]
1664 fn component_custom_id_length() {
1665 assert!(component_custom_id("").is_ok());
1666 assert!(component_custom_id("a").is_ok());
1667 assert!(component_custom_id("a".repeat(100)).is_ok());
1668
1669 assert!(component_custom_id("a".repeat(101)).is_err());
1670 }
1671
1672 #[test]
1673 fn component_option_description_length() {
1674 assert!(component_option_description("").is_ok());
1675 assert!(component_option_description("a").is_ok());
1676 assert!(component_option_description("a".repeat(100)).is_ok());
1677
1678 assert!(component_option_description("a".repeat(101)).is_err());
1679 }
1680
1681 #[test]
1682 fn component_select_default_values_support() {
1683 assert!(component_select_default_values_supported(SelectMenuType::User).is_ok());
1684 assert!(component_select_default_values_supported(SelectMenuType::Role).is_ok());
1685 assert!(component_select_default_values_supported(SelectMenuType::Mentionable).is_ok());
1686 assert!(component_select_default_values_supported(SelectMenuType::Channel).is_ok());
1687
1688 assert!(component_select_default_values_supported(SelectMenuType::Text).is_err());
1689 }
1690
1691 #[test]
1692 fn component_select_num_default_values() {
1693 assert!(component_select_default_values_count(None, None, 0).is_ok());
1694 assert!(component_select_default_values_count(None, None, 1).is_ok());
1695 assert!(component_select_default_values_count(Some(1), None, 5).is_ok());
1696 assert!(component_select_default_values_count(Some(5), None, 5).is_ok());
1697 assert!(component_select_default_values_count(None, Some(5), 5).is_ok());
1698 assert!(component_select_default_values_count(None, Some(10), 5).is_ok());
1699 assert!(component_select_default_values_count(Some(5), Some(5), 5).is_ok());
1700 assert!(component_select_default_values_count(Some(1), Some(10), 5).is_ok());
1701
1702 assert!(component_select_default_values_count(Some(2), None, 1).is_err());
1703 assert!(component_select_default_values_count(None, Some(1), 2).is_err());
1704 assert!(component_select_default_values_count(Some(1), Some(1), 2).is_err());
1705 assert!(component_select_default_values_count(Some(2), Some(2), 1).is_err());
1706 }
1707
1708 #[test]
1709 fn component_select_max_values_count() {
1710 assert!(component_select_max_values(1).is_ok());
1711 assert!(component_select_max_values(25).is_ok());
1712
1713 assert!(component_select_max_values(0).is_err());
1714 assert!(component_select_max_values(26).is_err());
1715 }
1716
1717 #[test]
1718 fn component_select_min_values_count() {
1719 assert!(component_select_min_values(1).is_ok());
1720 assert!(component_select_min_values(25).is_ok());
1721
1722 assert!(component_select_min_values(26).is_err());
1723 }
1724
1725 #[test]
1726 fn component_select_option_value_length() {
1727 assert!(component_select_option_value("a").is_ok());
1728 assert!(component_select_option_value("a".repeat(100)).is_ok());
1729
1730 assert!(component_select_option_value("a".repeat(101)).is_err());
1731 }
1732
1733 #[test]
1734 fn component_select_options_count() {
1735 let select_menu_options = Vec::from([SelectMenuOption {
1736 default: false,
1737 description: None,
1738 emoji: None,
1739 label: "label".into(),
1740 value: "value".into(),
1741 }]);
1742
1743 assert!(component_select_options(&select_menu_options).is_ok());
1744
1745 let select_menu_options_25 = select_menu_options
1746 .iter()
1747 .cloned()
1748 .cycle()
1749 .take(25)
1750 .collect::<Vec<SelectMenuOption>>();
1751
1752 assert!(component_select_options(&select_menu_options_25).is_ok());
1753
1754 let select_menu_options_26 = select_menu_options
1755 .iter()
1756 .cloned()
1757 .cycle()
1758 .take(26)
1759 .collect::<Vec<SelectMenuOption>>();
1760
1761 assert!(component_select_options(&select_menu_options_26).is_err());
1762 }
1763
1764 #[test]
1765 fn component_select_placeholder_length() {
1766 assert!(component_select_placeholder("").is_ok());
1767 assert!(component_select_placeholder("a").is_ok());
1768 assert!(component_select_placeholder("a".repeat(150)).is_ok());
1769
1770 assert!(component_select_placeholder("a".repeat(151)).is_err());
1771 }
1772
1773 #[test]
1774 fn component_text_input_label_length() {
1775 assert!(component_text_input_label("a").is_ok());
1776 assert!(component_text_input_label("a".repeat(45)).is_ok());
1777
1778 assert!(component_text_input_label("").is_err());
1779 assert!(component_text_input_label("a".repeat(46)).is_err());
1780 }
1781
1782 #[test]
1783 fn component_text_input_max_count() {
1784 assert!(component_text_input_max(1).is_ok());
1785 assert!(component_text_input_max(4000).is_ok());
1786
1787 assert!(component_text_input_max(0).is_err());
1788 assert!(component_text_input_max(4001).is_err());
1789 }
1790
1791 #[test]
1792 fn component_text_input_min_count() {
1793 assert!(component_text_input_min(0).is_ok());
1794 assert!(component_text_input_min(1).is_ok());
1795 assert!(component_text_input_min(4000).is_ok());
1796
1797 assert!(component_text_input_min(4001).is_err());
1798 }
1799
1800 #[test]
1801 fn component_text_input_placeholder_length() {
1802 assert!(component_text_input_placeholder("").is_ok());
1803 assert!(component_text_input_placeholder("a").is_ok());
1804 assert!(component_text_input_placeholder("a".repeat(100)).is_ok());
1805
1806 assert!(component_text_input_placeholder("a".repeat(101)).is_err());
1807 }
1808
1809 #[test]
1810 fn component_text_input_value() {
1811 assert!(component_text_input_min(0).is_ok());
1812 assert!(component_text_input_min(1).is_ok());
1813 assert!(component_text_input_min(4000).is_ok());
1814
1815 assert!(component_text_input_min(4001).is_err());
1816 }
1817}