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 FILE_UPLOAD_MAXIMUM_VALUES_LIMIT, FILE_UPLOAD_MINIMUM_VALUES_LIMIT,
16 LABEL_DESCRIPTION_LENGTH_MAX, LABEL_LABEL_LENGTH_MAX,
17 MEDIA_GALLERY_ITEM_DESCRIPTION_LENGTH_MAX, MEDIA_GALLERY_ITEMS_MAX, MEDIA_GALLERY_ITEMS_MIN,
18 SECTION_COMPONENTS_MAX, SECTION_COMPONENTS_MIN, TEXT_DISPLAY_CONTENT_LENGTH_MAX,
19 THUMBNAIL_DESCRIPTION_LENGTH_MAX, component_v2, container, file_upload, label, media_gallery,
20 media_gallery_item, section, text_display, thumbnail,
21};
22
23pub const ACTION_ROW_COMPONENT_COUNT: usize = 5;
30
31pub const COMPONENT_COUNT: usize = 5;
38
39pub const COMPONENT_V2_COUNT: usize = 40;
46
47pub const COMPONENT_CUSTOM_ID_LENGTH: usize = 100;
57
58pub const COMPONENT_BUTTON_LABEL_LENGTH: usize = 80;
67
68pub const SELECT_MAXIMUM_VALUES_LIMIT: usize = 25;
76
77pub const SELECT_MAXIMUM_VALUES_REQUIREMENT: usize = 1;
85
86pub const SELECT_MINIMUM_VALUES_LIMIT: usize = 25;
94
95pub const SELECT_OPTION_COUNT: usize = 25;
102
103pub const SELECT_OPTION_DESCRIPTION_LENGTH: usize = 100;
110
111pub const SELECT_OPTION_LABEL_LENGTH: usize = 100;
118
119pub const SELECT_OPTION_VALUE_LENGTH: usize = 100;
126
127pub const SELECT_PLACEHOLDER_LENGTH: usize = 150;
134
135pub const TEXT_INPUT_LABEL_MAX: usize = 45;
141
142pub const TEXT_INPUT_LABEL_MIN: usize = 1;
148
149pub const TEXT_INPUT_LENGTH_MAX: usize = 4000;
155
156pub const TEXT_INPUT_LENGTH_MIN: usize = 1;
162
163pub const TEXT_INPUT_PLACEHOLDER_MAX: usize = 100;
169
170#[derive(Debug)]
175pub struct ComponentValidationError {
176 kind: ComponentValidationErrorType,
178}
179
180impl ComponentValidationError {
181 #[must_use = "retrieving the type has no effect if left unused"]
183 pub const fn kind(&self) -> &ComponentValidationErrorType {
184 &self.kind
185 }
186
187 #[allow(clippy::unused_self)]
189 #[must_use = "consuming the error and retrieving the source has no effect if left unused"]
190 pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
191 None
192 }
193
194 #[must_use = "consuming the error into its parts has no effect if left unused"]
196 pub fn into_parts(
197 self,
198 ) -> (
199 ComponentValidationErrorType,
200 Option<Box<dyn Error + Send + Sync>>,
201 ) {
202 (self.kind, None)
203 }
204}
205
206impl Display for ComponentValidationError {
207 #[allow(clippy::too_many_lines)]
208 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
209 match &self.kind {
210 ComponentValidationErrorType::ActionRowComponentCount { count } => {
211 f.write_str("an action row has ")?;
212 Display::fmt(&count, f)?;
213 f.write_str(" children, but the max is ")?;
214
215 Display::fmt(&ACTION_ROW_COMPONENT_COUNT, f)
216 }
217 ComponentValidationErrorType::ButtonConflict => {
218 f.write_str("button has both a custom id and url, which is never valid")
219 }
220 ComponentValidationErrorType::ButtonStyle { style } => {
221 f.write_str("button has a type of ")?;
222 Debug::fmt(style, f)?;
223 f.write_str(", which must have a ")?;
224
225 f.write_str(if *style == ButtonStyle::Link {
226 "url"
227 } else {
228 "custom id"
229 })?;
230
231 f.write_str(" configured")
232 }
233 ComponentValidationErrorType::ComponentCount { count } => {
234 Display::fmt(count, f)?;
235 f.write_str(" components were provided, but the max is ")?;
236
237 Display::fmt(&COMPONENT_COUNT, f)
238 }
239 ComponentValidationErrorType::ComponentCustomIdLength { chars } => {
240 f.write_str("a component's custom id is ")?;
241 Display::fmt(&chars, f)?;
242 f.write_str(" characters long, but the max is ")?;
243
244 Display::fmt(&COMPONENT_CUSTOM_ID_LENGTH, f)
245 }
246 ComponentValidationErrorType::ComponentLabelLength { chars } => {
247 f.write_str("a component's label is ")?;
248 Display::fmt(&chars, f)?;
249 f.write_str(" characters long, but the max is ")?;
250
251 Display::fmt(&COMPONENT_BUTTON_LABEL_LENGTH, f)
252 }
253 ComponentValidationErrorType::InvalidChildComponent { kind } => {
254 f.write_str("a '")?;
255 Display::fmt(&kind, f)?;
256
257 f.write_str(" component was provided, but can not be a child component")
258 }
259 ComponentValidationErrorType::InvalidRootComponent { kind } => {
260 f.write_str("a '")?;
261 Display::fmt(kind, f)?;
262
263 f.write_str("' component was provided, but can not be a root component")
264 }
265 ComponentValidationErrorType::SelectMaximumValuesCount { count } => {
266 f.write_str("maximum number of values that can be chosen is ")?;
267 Display::fmt(count, f)?;
268 f.write_str(", but must be greater than or equal to ")?;
269 Display::fmt(&SELECT_MAXIMUM_VALUES_REQUIREMENT, f)?;
270 f.write_str("and less than or equal to ")?;
271
272 Display::fmt(&SELECT_MAXIMUM_VALUES_LIMIT, f)
273 }
274 ComponentValidationErrorType::SelectMinimumValuesCount { count } => {
275 f.write_str("minimum number of values that must be chosen is ")?;
276 Display::fmt(count, f)?;
277 f.write_str(", but must be less than or equal to ")?;
278
279 Display::fmt(&SELECT_MINIMUM_VALUES_LIMIT, f)
280 }
281 ComponentValidationErrorType::SelectNotEnoughDefaultValues { provided, min } => {
282 f.write_str("a select menu provided ")?;
283 Display::fmt(provided, f)?;
284 f.write_str(" values, but it requires at least ")?;
285 Display::fmt(min, f)?;
286 f.write_str(" values")
287 }
288 ComponentValidationErrorType::SelectOptionsMissing => {
289 f.write_str("a text select menu doesn't specify the required options field")
290 }
291 ComponentValidationErrorType::SelectOptionDescriptionLength { chars } => {
292 f.write_str("a select menu option's description is ")?;
293 Display::fmt(&chars, f)?;
294 f.write_str(" characters long, but the max is ")?;
295
296 Display::fmt(&SELECT_OPTION_DESCRIPTION_LENGTH, f)
297 }
298 ComponentValidationErrorType::SelectOptionLabelLength { chars } => {
299 f.write_str("a select menu option's label is ")?;
300 Display::fmt(&chars, f)?;
301 f.write_str(" characters long, but the max is ")?;
302
303 Display::fmt(&SELECT_OPTION_LABEL_LENGTH, f)
304 }
305 ComponentValidationErrorType::SelectOptionValueLength { chars } => {
306 f.write_str("a select menu option's value is ")?;
307 Display::fmt(&chars, f)?;
308 f.write_str(" characters long, but the max is ")?;
309
310 Display::fmt(&SELECT_OPTION_VALUE_LENGTH, f)
311 }
312 ComponentValidationErrorType::SelectPlaceholderLength { chars } => {
313 f.write_str("a select menu's placeholder is ")?;
314 Display::fmt(&chars, f)?;
315 f.write_str(" characters long, but the max is ")?;
316
317 Display::fmt(&SELECT_PLACEHOLDER_LENGTH, f)
318 }
319 ComponentValidationErrorType::SelectOptionCount { count } => {
320 f.write_str("a select menu has ")?;
321 Display::fmt(&count, f)?;
322 f.write_str(" options, but the max is ")?;
323
324 Display::fmt(&SELECT_OPTION_COUNT, f)
325 }
326 ComponentValidationErrorType::SelectTooManyDefaultValues { provided, max } => {
327 f.write_str("a select menu provided ")?;
328 Display::fmt(provided, f)?;
329 f.write_str(" values, but it allows at most ")?;
330 Display::fmt(max, f)?;
331 f.write_str(" values")
332 }
333 ComponentValidationErrorType::SelectUnsupportedDefaultValues { kind } => {
334 f.write_str("a select menu has defined default_values, but its type, ")?;
335 Debug::fmt(kind, f)?;
336 f.write_str(", does not support them")
337 }
338 ComponentValidationErrorType::TextInputLabelLength { len: count } => {
339 f.write_str("a text input label length is ")?;
340 Display::fmt(count, f)?;
341 f.write_str(", but it must be at least ")?;
342 Display::fmt(&TEXT_INPUT_LABEL_MIN, f)?;
343 f.write_str(" and at most ")?;
344
345 Display::fmt(&TEXT_INPUT_LABEL_MAX, f)
346 }
347 ComponentValidationErrorType::TextInputMaxLength { len: count } => {
348 f.write_str("a text input max length is ")?;
349 Display::fmt(count, f)?;
350 f.write_str(", but it must be at least ")?;
351 Display::fmt(&TEXT_INPUT_LENGTH_MIN, f)?;
352 f.write_str(" and at most ")?;
353
354 Display::fmt(&TEXT_INPUT_LENGTH_MAX, f)
355 }
356 ComponentValidationErrorType::TextInputMinLength { len: count } => {
357 f.write_str("a text input min length is ")?;
358 Display::fmt(count, f)?;
359 f.write_str(", but it must be at most ")?;
360
361 Display::fmt(&TEXT_INPUT_LENGTH_MAX, f)
362 }
363 ComponentValidationErrorType::TextInputPlaceholderLength { chars } => {
364 f.write_str("a text input's placeholder is ")?;
365 Display::fmt(&chars, f)?;
366 f.write_str(" characters long, but the max is ")?;
367
368 Display::fmt(&TEXT_INPUT_PLACEHOLDER_MAX, f)
369 }
370 ComponentValidationErrorType::TextInputValueLength { chars } => {
371 f.write_str("a text input's value is ")?;
372 Display::fmt(&chars, f)?;
373 f.write_str(" characters long, but the max is ")?;
374
375 Display::fmt(&TEXT_INPUT_PLACEHOLDER_MAX, f)
376 }
377 ComponentValidationErrorType::TextInputDisallowedLabel => {
378 f.write_str("a text input contained a label when disallowed")
379 }
380 ComponentValidationErrorType::DisallowedV2Components => {
381 f.write_str("a V2 component was used in a component V1 message")
382 }
383 ComponentValidationErrorType::DisallowedChildren => {
384 f.write_str("a component contains a disallowed child component")
385 }
386 ComponentValidationErrorType::TextDisplayContentTooLong { len: count } => {
387 f.write_str("a text display content length is ")?;
388 Display::fmt(count, f)?;
389 f.write_str(" characters long, but the max is ")?;
390
391 Display::fmt(&TEXT_DISPLAY_CONTENT_LENGTH_MAX, f)
392 }
393 ComponentValidationErrorType::MediaGalleryItemCountOutOfRange { count } => {
394 f.write_str("a media gallery has ")?;
395 Display::fmt(count, f)?;
396 f.write_str(" items, but the min and max are ")?;
397 Display::fmt(&MEDIA_GALLERY_ITEMS_MIN, f)?;
398 f.write_str(" and ")?;
399 Display::fmt(&MEDIA_GALLERY_ITEMS_MAX, f)?;
400
401 f.write_str(" respectively")
402 }
403 ComponentValidationErrorType::MediaGalleryItemDescriptionTooLong { len } => {
404 f.write_str("a media gallery item description length is ")?;
405 Display::fmt(len, f)?;
406 f.write_str(" characters long, but the max is ")?;
407
408 Display::fmt(&MEDIA_GALLERY_ITEM_DESCRIPTION_LENGTH_MAX, f)
409 }
410 ComponentValidationErrorType::SectionComponentCountOutOfRange { count } => {
411 f.write_str("a section has ")?;
412 Display::fmt(count, f)?;
413 f.write_str(" components, but the min and max are ")?;
414 Display::fmt(&SECTION_COMPONENTS_MIN, f)?;
415 f.write_str(" and ")?;
416 Display::fmt(&SECTION_COMPONENTS_MAX, f)?;
417
418 f.write_str(" respectively")
419 }
420 ComponentValidationErrorType::SelectDisallowedDisabled => {
421 f.write_str("a select menu was disabled when disallowed")
422 }
423 ComponentValidationErrorType::ThumbnailDescriptionTooLong { len } => {
424 f.write_str("a thumbnail description length is ")?;
425 Display::fmt(len, f)?;
426 f.write_str(" characters long, but the max is ")?;
427
428 Display::fmt(&THUMBNAIL_DESCRIPTION_LENGTH_MAX, f)
429 }
430 ComponentValidationErrorType::LabelLabelTooLong { len } => {
431 f.write_str("a label text of a label component is ")?;
432 Display::fmt(len, f)?;
433 f.write_str(" characters long, but the max is ")?;
434
435 Display::fmt(&LABEL_LABEL_LENGTH_MAX, f)
436 }
437 ComponentValidationErrorType::LabelDescriptionTooLong { len } => {
438 f.write_str("a label description length is ")?;
439 Display::fmt(len, f)?;
440 f.write_str(" characters long, but the max is ")?;
441
442 Display::fmt(&LABEL_DESCRIPTION_LENGTH_MAX, f)
443 }
444 ComponentValidationErrorType::FileUploadMaximumValuesCount { count } => {
445 f.write_str("maximum number of files that can be uploaded is ")?;
446 Display::fmt(count, f)?;
447 f.write_str(", but must be less than or equal to ")?;
448
449 Display::fmt(&FILE_UPLOAD_MAXIMUM_VALUES_LIMIT, f)
450 }
451 ComponentValidationErrorType::FileUploadMinimumValuesCount { count } => {
452 f.write_str("minimum number of files that must be uploaded is ")?;
453 Display::fmt(count, f)?;
454 f.write_str(", but must be less than or equal to ")?;
455
456 Display::fmt(&FILE_UPLOAD_MINIMUM_VALUES_LIMIT, f)
457 }
458 }
459 }
460}
461
462impl Error for ComponentValidationError {}
463
464#[derive(Debug)]
466#[non_exhaustive]
467pub enum ComponentValidationErrorType {
468 ActionRowComponentCount {
471 count: usize,
473 },
474 ButtonConflict,
476 ButtonStyle {
481 style: ButtonStyle,
483 },
484 ComponentCount {
487 count: usize,
489 },
490 ComponentCustomIdLength {
493 chars: usize,
495 },
496 ComponentLabelLength {
498 chars: usize,
500 },
501 InvalidChildComponent {
503 kind: ComponentType,
505 },
506 InvalidRootComponent {
508 kind: ComponentType,
510 },
511 SelectMaximumValuesCount {
515 count: usize,
517 },
518 SelectMinimumValuesCount {
521 count: usize,
523 },
524 SelectNotEnoughDefaultValues {
526 provided: usize,
528 min: usize,
530 },
531 SelectOptionsMissing,
535 SelectOptionCount {
538 count: usize,
540 },
541 SelectOptionDescriptionLength {
544 chars: usize,
546 },
547 SelectOptionLabelLength {
550 chars: usize,
552 },
553 SelectOptionValueLength {
556 chars: usize,
558 },
559 SelectPlaceholderLength {
562 chars: usize,
564 },
565 SelectTooManyDefaultValues {
567 provided: usize,
569 max: usize,
571 },
572 SelectUnsupportedDefaultValues {
574 kind: SelectMenuType,
576 },
577 TextInputLabelLength {
579 len: usize,
581 },
582 TextInputMaxLength {
584 len: usize,
586 },
587 TextInputMinLength {
589 len: usize,
591 },
592 TextInputPlaceholderLength {
595 chars: usize,
597 },
598 TextInputValueLength {
601 chars: usize,
603 },
604 TextInputDisallowedLabel,
606 DisallowedV2Components,
608 DisallowedChildren,
610 TextDisplayContentTooLong {
612 len: usize,
614 },
615 MediaGalleryItemCountOutOfRange {
617 count: usize,
619 },
620 MediaGalleryItemDescriptionTooLong {
622 len: usize,
624 },
625 SectionComponentCountOutOfRange {
627 count: usize,
629 },
630 SelectDisallowedDisabled,
633 ThumbnailDescriptionTooLong {
635 len: usize,
637 },
638 LabelLabelTooLong {
640 len: usize,
642 },
643 LabelDescriptionTooLong {
645 len: usize,
647 },
648 FileUploadMaximumValuesCount {
651 count: u8,
655 },
656 FileUploadMinimumValuesCount {
659 count: u8,
663 },
664}
665
666pub fn component_v1(component: &Component) -> Result<(), ComponentValidationError> {
686 match component {
687 Component::ActionRow(action_row) => self::action_row(action_row, false)?,
688 other => {
689 return Err(ComponentValidationError {
690 kind: ComponentValidationErrorType::InvalidRootComponent { kind: other.kind() },
691 });
692 }
693 }
694
695 Ok(())
696}
697
698#[deprecated(note = "Use component_v1 for old components and component_v2 for new ones")]
716pub fn component(component: &Component) -> Result<(), ComponentValidationError> {
717 component_v1(component)
718}
719
720pub fn action_row(action_row: &ActionRow, is_v2: bool) -> Result<(), ComponentValidationError> {
752 self::component_action_row_components(&action_row.components)?;
753
754 for component in &action_row.components {
755 match component {
756 Component::ActionRow(_) | Component::Label(_) => {
757 return Err(ComponentValidationError {
758 kind: ComponentValidationErrorType::InvalidChildComponent {
759 kind: component.kind(),
760 },
761 });
762 }
763 Component::Button(button) => self::button(button)?,
764 Component::SelectMenu(select_menu) => self::select_menu(select_menu, true)?,
765 Component::TextInput(text_input) => self::text_input(text_input, true)?,
766 Component::Unknown(unknown) => {
767 return Err(ComponentValidationError {
768 kind: ComponentValidationErrorType::InvalidChildComponent {
769 kind: ComponentType::Unknown(*unknown),
770 },
771 });
772 }
773
774 Component::TextDisplay(_)
775 | Component::MediaGallery(_)
776 | Component::Separator(_)
777 | Component::File(_)
778 | Component::Section(_)
779 | Component::Container(_)
780 | Component::Thumbnail(_)
781 | Component::FileUpload(_) => {
782 return Err(ComponentValidationError {
783 kind: if is_v2 {
784 ComponentValidationErrorType::DisallowedChildren
785 } else {
786 ComponentValidationErrorType::DisallowedV2Components
787 },
788 });
789 }
790 }
791 }
792
793 Ok(())
794}
795
796pub fn button(button: &Button) -> Result<(), ComponentValidationError> {
818 let has_custom_id = button.custom_id.is_some();
819 let has_emoji = button.emoji.is_some();
820 let has_label = button.label.is_some();
821 let has_sku_id = button.sku_id.is_some();
822 let has_url = button.url.is_some();
823
824 if has_custom_id && has_url {
827 return Err(ComponentValidationError {
828 kind: ComponentValidationErrorType::ButtonConflict,
829 });
830 }
831
832 let is_premium = button.style == ButtonStyle::Premium;
837 if is_premium && (has_custom_id || has_url || has_label || has_emoji || !has_sku_id) {
838 return Err(ComponentValidationError {
839 kind: ComponentValidationErrorType::ButtonStyle {
840 style: button.style,
841 },
842 });
843 }
844
845 let is_link = button.style == ButtonStyle::Link;
850
851 if (is_link && !has_url) || (!is_link && !has_custom_id) {
852 return Err(ComponentValidationError {
853 kind: ComponentValidationErrorType::ButtonStyle {
854 style: button.style,
855 },
856 });
857 }
858
859 if let Some(custom_id) = button.custom_id.as_ref() {
860 self::component_custom_id(custom_id)?;
861 }
862
863 if let Some(label) = button.label.as_ref() {
864 self::component_button_label(label)?;
865 }
866
867 Ok(())
868}
869
870pub fn select_menu(
928 select_menu: &SelectMenu,
929 disabled_allowed: bool,
930) -> Result<(), ComponentValidationError> {
931 self::component_custom_id(&select_menu.custom_id)?;
932
933 if !disabled_allowed && select_menu.disabled {
934 return Err(ComponentValidationError {
935 kind: ComponentValidationErrorType::SelectDisallowedDisabled,
936 });
937 }
938
939 if let SelectMenuType::Text = &select_menu.kind {
941 let options = select_menu
942 .options
943 .as_ref()
944 .ok_or(ComponentValidationError {
945 kind: ComponentValidationErrorType::SelectOptionsMissing,
946 })?;
947 for option in options {
948 component_select_option_label(&option.label)?;
949 component_select_option_value(&option.value)?;
950
951 if let Some(description) = option.description.as_ref() {
952 component_option_description(description)?;
953 }
954 }
955 component_select_options(options)?;
956 }
957
958 if let Some(placeholder) = select_menu.placeholder.as_ref() {
959 self::component_select_placeholder(placeholder)?;
960 }
961
962 if let Some(max_values) = select_menu.max_values {
963 self::component_select_max_values(usize::from(max_values))?;
964 }
965
966 if let Some(min_values) = select_menu.min_values {
967 self::component_select_min_values(usize::from(min_values))?;
968 }
969
970 if let Some(default_values) = select_menu.default_values.as_ref() {
971 component_select_default_values_supported(select_menu.kind)?;
972 component_select_default_values_count(
973 select_menu.min_values,
974 select_menu.max_values,
975 default_values.len(),
976 )?;
977 }
978
979 Ok(())
980}
981
982pub fn text_input(
1016 text_input: &TextInput,
1017 label_allowed: bool,
1018) -> Result<(), ComponentValidationError> {
1019 self::component_custom_id(&text_input.custom_id)?;
1020
1021 #[allow(deprecated)]
1022 if let Some(label) = &text_input.label {
1023 if !label_allowed {
1024 return Err(ComponentValidationError {
1025 kind: ComponentValidationErrorType::TextInputDisallowedLabel,
1026 });
1027 }
1028
1029 self::component_text_input_label(label)?;
1030 }
1031
1032 if let Some(max_length) = text_input.max_length {
1033 self::component_text_input_max(max_length)?;
1034 }
1035
1036 if let Some(min_length) = text_input.min_length {
1037 self::component_text_input_min(min_length)?;
1038 }
1039
1040 if let Some(placeholder) = text_input.placeholder.as_ref() {
1041 self::component_text_input_placeholder(placeholder)?;
1042 }
1043
1044 if let Some(value) = text_input.value.as_ref() {
1045 self::component_text_input_value(value)?;
1046 }
1047
1048 Ok(())
1049}
1050
1051const fn component_action_row_components(
1064 components: &[Component],
1065) -> Result<(), ComponentValidationError> {
1066 let count = components.len();
1067
1068 if count > COMPONENT_COUNT {
1069 return Err(ComponentValidationError {
1070 kind: ComponentValidationErrorType::ActionRowComponentCount { count },
1071 });
1072 }
1073
1074 Ok(())
1075}
1076
1077fn component_button_label(label: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1086 let chars = label.as_ref().chars().count();
1087
1088 if chars > COMPONENT_BUTTON_LABEL_LENGTH {
1089 return Err(ComponentValidationError {
1090 kind: ComponentValidationErrorType::ComponentLabelLength { chars },
1091 });
1092 }
1093
1094 Ok(())
1095}
1096
1097fn component_custom_id(custom_id: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1106 let chars = custom_id.as_ref().chars().count();
1107
1108 if chars > COMPONENT_CUSTOM_ID_LENGTH {
1109 return Err(ComponentValidationError {
1110 kind: ComponentValidationErrorType::ComponentCustomIdLength { chars },
1111 });
1112 }
1113
1114 Ok(())
1115}
1116
1117fn component_option_description(
1127 description: impl AsRef<str>,
1128) -> Result<(), ComponentValidationError> {
1129 let chars = description.as_ref().chars().count();
1130
1131 if chars > SELECT_OPTION_DESCRIPTION_LENGTH {
1132 return Err(ComponentValidationError {
1133 kind: ComponentValidationErrorType::SelectOptionDescriptionLength { chars },
1134 });
1135 }
1136
1137 Ok(())
1138}
1139
1140const fn component_select_default_values_supported(
1147 menu_type: SelectMenuType,
1148) -> Result<(), ComponentValidationError> {
1149 if !matches!(
1150 menu_type,
1151 SelectMenuType::User
1152 | SelectMenuType::Role
1153 | SelectMenuType::Mentionable
1154 | SelectMenuType::Channel
1155 ) {
1156 return Err(ComponentValidationError {
1157 kind: ComponentValidationErrorType::SelectUnsupportedDefaultValues { kind: menu_type },
1158 });
1159 }
1160
1161 Ok(())
1162}
1163
1164const fn component_select_default_values_count(
1174 min_values: Option<u8>,
1175 max_values: Option<u8>,
1176 default_values: usize,
1177) -> Result<(), ComponentValidationError> {
1178 if let Some(min) = min_values {
1179 let min = min as usize;
1180 if default_values < min {
1181 return Err(ComponentValidationError {
1182 kind: ComponentValidationErrorType::SelectNotEnoughDefaultValues {
1183 provided: default_values,
1184 min,
1185 },
1186 });
1187 }
1188 }
1189 if let Some(max) = max_values {
1190 let max = max as usize;
1191 if default_values > max {
1192 return Err(ComponentValidationError {
1193 kind: ComponentValidationErrorType::SelectTooManyDefaultValues {
1194 provided: default_values,
1195 max,
1196 },
1197 });
1198 }
1199 }
1200
1201 Ok(())
1202}
1203
1204const fn component_select_max_values(count: usize) -> Result<(), ComponentValidationError> {
1216 if count > SELECT_MAXIMUM_VALUES_LIMIT {
1217 return Err(ComponentValidationError {
1218 kind: ComponentValidationErrorType::SelectMaximumValuesCount { count },
1219 });
1220 }
1221
1222 if count < SELECT_MAXIMUM_VALUES_REQUIREMENT {
1223 return Err(ComponentValidationError {
1224 kind: ComponentValidationErrorType::SelectMaximumValuesCount { count },
1225 });
1226 }
1227
1228 Ok(())
1229}
1230
1231const fn component_select_min_values(count: usize) -> Result<(), ComponentValidationError> {
1242 if count > SELECT_MINIMUM_VALUES_LIMIT {
1243 return Err(ComponentValidationError {
1244 kind: ComponentValidationErrorType::SelectMinimumValuesCount { count },
1245 });
1246 }
1247
1248 Ok(())
1249}
1250
1251fn component_select_option_label(label: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1261 let chars = label.as_ref().chars().count();
1262
1263 if chars > SELECT_OPTION_LABEL_LENGTH {
1264 return Err(ComponentValidationError {
1265 kind: ComponentValidationErrorType::SelectOptionLabelLength { chars },
1266 });
1267 }
1268
1269 Ok(())
1270}
1271
1272fn component_select_option_value(value: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1282 let chars = value.as_ref().chars().count();
1283
1284 if chars > SELECT_OPTION_VALUE_LENGTH {
1285 return Err(ComponentValidationError {
1286 kind: ComponentValidationErrorType::SelectOptionValueLength { chars },
1287 });
1288 }
1289
1290 Ok(())
1291}
1292
1293const fn component_select_options(
1308 options: &[SelectMenuOption],
1309) -> Result<(), ComponentValidationError> {
1310 let count = options.len();
1311
1312 if count > SELECT_OPTION_COUNT {
1313 return Err(ComponentValidationError {
1314 kind: ComponentValidationErrorType::SelectOptionCount { count },
1315 });
1316 }
1317
1318 Ok(())
1319}
1320
1321fn component_select_placeholder(
1331 placeholder: impl AsRef<str>,
1332) -> Result<(), ComponentValidationError> {
1333 let chars = placeholder.as_ref().chars().count();
1334
1335 if chars > SELECT_PLACEHOLDER_LENGTH {
1336 return Err(ComponentValidationError {
1337 kind: ComponentValidationErrorType::SelectPlaceholderLength { chars },
1338 });
1339 }
1340
1341 Ok(())
1342}
1343
1344fn component_text_input_label(label: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1356 let len = label.as_ref().len();
1357
1358 if (TEXT_INPUT_LABEL_MIN..=TEXT_INPUT_LABEL_MAX).contains(&len) {
1359 Ok(())
1360 } else {
1361 Err(ComponentValidationError {
1362 kind: ComponentValidationErrorType::TextInputLabelLength { len },
1363 })
1364 }
1365}
1366
1367const fn component_text_input_max(len: u16) -> Result<(), ComponentValidationError> {
1376 let len = len as usize;
1377
1378 if len >= TEXT_INPUT_LENGTH_MIN && len <= TEXT_INPUT_LENGTH_MAX {
1379 Ok(())
1380 } else {
1381 Err(ComponentValidationError {
1382 kind: ComponentValidationErrorType::TextInputMaxLength { len },
1383 })
1384 }
1385}
1386
1387const fn component_text_input_min(len: u16) -> Result<(), ComponentValidationError> {
1396 let len = len as usize;
1397
1398 if len <= TEXT_INPUT_LENGTH_MAX {
1399 Ok(())
1400 } else {
1401 Err(ComponentValidationError {
1402 kind: ComponentValidationErrorType::TextInputMinLength { len },
1403 })
1404 }
1405}
1406
1407fn component_text_input_placeholder(
1419 placeholder: impl AsRef<str>,
1420) -> Result<(), ComponentValidationError> {
1421 let chars = placeholder.as_ref().chars().count();
1422
1423 if chars <= TEXT_INPUT_PLACEHOLDER_MAX {
1424 Ok(())
1425 } else {
1426 Err(ComponentValidationError {
1427 kind: ComponentValidationErrorType::TextInputPlaceholderLength { chars },
1428 })
1429 }
1430}
1431
1432fn component_text_input_value(value: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1441 let chars = value.as_ref().chars().count();
1442
1443 if chars <= TEXT_INPUT_LENGTH_MAX {
1444 Ok(())
1445 } else {
1446 Err(ComponentValidationError {
1447 kind: ComponentValidationErrorType::TextInputValueLength { chars },
1448 })
1449 }
1450}
1451
1452#[allow(clippy::non_ascii_literal)]
1453#[cfg(test)]
1454mod tests {
1455 use super::*;
1456 use static_assertions::{assert_fields, assert_impl_all};
1457 use twilight_model::channel::message::EmojiReactionType;
1458
1459 assert_fields!(ComponentValidationErrorType::ActionRowComponentCount: count);
1460 assert_fields!(ComponentValidationErrorType::ComponentCount: count);
1461 assert_fields!(ComponentValidationErrorType::ComponentCustomIdLength: chars);
1462 assert_fields!(ComponentValidationErrorType::ComponentLabelLength: chars);
1463 assert_fields!(ComponentValidationErrorType::InvalidChildComponent: kind);
1464 assert_fields!(ComponentValidationErrorType::InvalidRootComponent: kind);
1465 assert_fields!(ComponentValidationErrorType::SelectMaximumValuesCount: count);
1466 assert_fields!(ComponentValidationErrorType::SelectMinimumValuesCount: count);
1467 assert_fields!(ComponentValidationErrorType::SelectOptionDescriptionLength: chars);
1468 assert_fields!(ComponentValidationErrorType::SelectOptionLabelLength: chars);
1469 assert_fields!(ComponentValidationErrorType::SelectOptionValueLength: chars);
1470 assert_fields!(ComponentValidationErrorType::SelectPlaceholderLength: chars);
1471 assert_impl_all!(ComponentValidationErrorType: Debug, Send, Sync);
1472 assert_impl_all!(ComponentValidationError: Debug, Send, Sync);
1473
1474 const ALL_BUTTON_STYLES: &[ButtonStyle] = &[
1476 ButtonStyle::Primary,
1477 ButtonStyle::Secondary,
1478 ButtonStyle::Success,
1479 ButtonStyle::Danger,
1480 ButtonStyle::Link,
1481 ButtonStyle::Premium,
1482 ];
1483
1484 #[test]
1485 fn component_action_row() {
1486 let button = Button {
1487 custom_id: None,
1488 disabled: false,
1489 emoji: Some(EmojiReactionType::Unicode {
1490 name: "📚".into()
1491 }),
1492 label: Some("Read".into()),
1493 style: ButtonStyle::Link,
1494 url: Some("https://abebooks.com".into()),
1495 sku_id: None,
1496 id: None,
1497 };
1498
1499 let select_menu = SelectMenu {
1500 channel_types: None,
1501 custom_id: "custom id 2".into(),
1502 disabled: false,
1503 default_values: None,
1504 kind: SelectMenuType::Text,
1505 max_values: Some(2),
1506 min_values: Some(1),
1507 options: Some(Vec::from([SelectMenuOption {
1508 default: true,
1509 description: Some("Book 1 of the Expanse".into()),
1510 emoji: None,
1511 label: "Leviathan Wakes".into(),
1512 value: "9780316129084".into(),
1513 }])),
1514 placeholder: Some("Choose a book".into()),
1515 id: None,
1516 required: None,
1517 };
1518
1519 let action_row = ActionRow {
1520 components: Vec::from([
1521 Component::SelectMenu(select_menu.clone()),
1522 Component::Button(button),
1523 ]),
1524 id: None,
1525 };
1526
1527 assert!(component_v1(&Component::ActionRow(action_row.clone())).is_ok());
1528
1529 assert!(component_v1(&Component::SelectMenu(select_menu.clone())).is_err());
1530
1531 assert!(super::action_row(&action_row, false).is_ok());
1532
1533 let invalid_action_row = Component::ActionRow(ActionRow {
1534 components: Vec::from([
1535 Component::SelectMenu(select_menu.clone()),
1536 Component::SelectMenu(select_menu.clone()),
1537 Component::SelectMenu(select_menu.clone()),
1538 Component::SelectMenu(select_menu.clone()),
1539 Component::SelectMenu(select_menu.clone()),
1540 Component::SelectMenu(select_menu),
1541 ]),
1542 id: None,
1543 });
1544
1545 assert!(component_v1(&invalid_action_row).is_err());
1546 }
1547
1548 #[test]
1551 fn button_conflict() {
1552 let button = Button {
1553 custom_id: Some("a".to_owned()),
1554 disabled: false,
1555 emoji: None,
1556 label: None,
1557 style: ButtonStyle::Primary,
1558 url: Some("https://twilight.rs".to_owned()),
1559 sku_id: None,
1560 id: None,
1561 };
1562
1563 assert!(matches!(
1564 super::button(&button),
1565 Err(ComponentValidationError {
1566 kind: ComponentValidationErrorType::ButtonConflict,
1567 }),
1568 ));
1569 }
1570
1571 #[test]
1574 fn button_style() {
1575 for style in ALL_BUTTON_STYLES {
1576 let button = Button {
1577 custom_id: None,
1578 disabled: false,
1579 emoji: None,
1580 label: Some("some label".to_owned()),
1581 style: *style,
1582 url: None,
1583 sku_id: None,
1584 id: None,
1585 };
1586
1587 assert!(matches!(
1588 super::button(&button),
1589 Err(ComponentValidationError {
1590 kind: ComponentValidationErrorType::ButtonStyle {
1591 style: error_style,
1592 }
1593 })
1594 if error_style == *style
1595 ));
1596 }
1597 }
1598
1599 #[test]
1600 fn component_label() {
1601 assert!(component_button_label("").is_ok());
1602 assert!(component_button_label("a").is_ok());
1603 assert!(component_button_label("a".repeat(80)).is_ok());
1604
1605 assert!(component_button_label("a".repeat(81)).is_err());
1606 }
1607
1608 #[test]
1609 fn component_custom_id_length() {
1610 assert!(component_custom_id("").is_ok());
1611 assert!(component_custom_id("a").is_ok());
1612 assert!(component_custom_id("a".repeat(100)).is_ok());
1613
1614 assert!(component_custom_id("a".repeat(101)).is_err());
1615 }
1616
1617 #[test]
1618 fn component_option_description_length() {
1619 assert!(component_option_description("").is_ok());
1620 assert!(component_option_description("a").is_ok());
1621 assert!(component_option_description("a".repeat(100)).is_ok());
1622
1623 assert!(component_option_description("a".repeat(101)).is_err());
1624 }
1625
1626 #[test]
1627 fn component_select_default_values_support() {
1628 assert!(component_select_default_values_supported(SelectMenuType::User).is_ok());
1629 assert!(component_select_default_values_supported(SelectMenuType::Role).is_ok());
1630 assert!(component_select_default_values_supported(SelectMenuType::Mentionable).is_ok());
1631 assert!(component_select_default_values_supported(SelectMenuType::Channel).is_ok());
1632
1633 assert!(component_select_default_values_supported(SelectMenuType::Text).is_err());
1634 }
1635
1636 #[test]
1637 fn component_select_num_default_values() {
1638 assert!(component_select_default_values_count(None, None, 0).is_ok());
1639 assert!(component_select_default_values_count(None, None, 1).is_ok());
1640 assert!(component_select_default_values_count(Some(1), None, 5).is_ok());
1641 assert!(component_select_default_values_count(Some(5), None, 5).is_ok());
1642 assert!(component_select_default_values_count(None, Some(5), 5).is_ok());
1643 assert!(component_select_default_values_count(None, Some(10), 5).is_ok());
1644 assert!(component_select_default_values_count(Some(5), Some(5), 5).is_ok());
1645 assert!(component_select_default_values_count(Some(1), Some(10), 5).is_ok());
1646
1647 assert!(component_select_default_values_count(Some(2), None, 1).is_err());
1648 assert!(component_select_default_values_count(None, Some(1), 2).is_err());
1649 assert!(component_select_default_values_count(Some(1), Some(1), 2).is_err());
1650 assert!(component_select_default_values_count(Some(2), Some(2), 1).is_err());
1651 }
1652
1653 #[test]
1654 fn component_select_max_values_count() {
1655 assert!(component_select_max_values(1).is_ok());
1656 assert!(component_select_max_values(25).is_ok());
1657
1658 assert!(component_select_max_values(0).is_err());
1659 assert!(component_select_max_values(26).is_err());
1660 }
1661
1662 #[test]
1663 fn component_select_min_values_count() {
1664 assert!(component_select_min_values(1).is_ok());
1665 assert!(component_select_min_values(25).is_ok());
1666
1667 assert!(component_select_min_values(26).is_err());
1668 }
1669
1670 #[test]
1671 fn component_select_option_value_length() {
1672 assert!(component_select_option_value("a").is_ok());
1673 assert!(component_select_option_value("a".repeat(100)).is_ok());
1674
1675 assert!(component_select_option_value("a".repeat(101)).is_err());
1676 }
1677
1678 #[test]
1679 fn component_select_options_count() {
1680 let select_menu_options = Vec::from([SelectMenuOption {
1681 default: false,
1682 description: None,
1683 emoji: None,
1684 label: "label".into(),
1685 value: "value".into(),
1686 }]);
1687
1688 assert!(component_select_options(&select_menu_options).is_ok());
1689
1690 let select_menu_options_25 = select_menu_options
1691 .iter()
1692 .cloned()
1693 .cycle()
1694 .take(25)
1695 .collect::<Vec<SelectMenuOption>>();
1696
1697 assert!(component_select_options(&select_menu_options_25).is_ok());
1698
1699 let select_menu_options_26 = select_menu_options
1700 .iter()
1701 .cloned()
1702 .cycle()
1703 .take(26)
1704 .collect::<Vec<SelectMenuOption>>();
1705
1706 assert!(component_select_options(&select_menu_options_26).is_err());
1707 }
1708
1709 #[test]
1710 fn component_select_placeholder_length() {
1711 assert!(component_select_placeholder("").is_ok());
1712 assert!(component_select_placeholder("a").is_ok());
1713 assert!(component_select_placeholder("a".repeat(150)).is_ok());
1714
1715 assert!(component_select_placeholder("a".repeat(151)).is_err());
1716 }
1717
1718 #[test]
1719 fn component_text_input_label_length() {
1720 assert!(component_text_input_label("a").is_ok());
1721 assert!(component_text_input_label("a".repeat(45)).is_ok());
1722
1723 assert!(component_text_input_label("").is_err());
1724 assert!(component_text_input_label("a".repeat(46)).is_err());
1725 }
1726
1727 #[test]
1728 fn component_text_input_max_count() {
1729 assert!(component_text_input_max(1).is_ok());
1730 assert!(component_text_input_max(4000).is_ok());
1731
1732 assert!(component_text_input_max(0).is_err());
1733 assert!(component_text_input_max(4001).is_err());
1734 }
1735
1736 #[test]
1737 fn component_text_input_min_count() {
1738 assert!(component_text_input_min(0).is_ok());
1739 assert!(component_text_input_min(1).is_ok());
1740 assert!(component_text_input_min(4000).is_ok());
1741
1742 assert!(component_text_input_min(4001).is_err());
1743 }
1744
1745 #[test]
1746 fn component_text_input_placeholder_length() {
1747 assert!(component_text_input_placeholder("").is_ok());
1748 assert!(component_text_input_placeholder("a").is_ok());
1749 assert!(component_text_input_placeholder("a".repeat(100)).is_ok());
1750
1751 assert!(component_text_input_placeholder("a".repeat(101)).is_err());
1752 }
1753
1754 #[test]
1755 fn component_text_input_value() {
1756 assert!(component_text_input_min(0).is_ok());
1757 assert!(component_text_input_min(1).is_ok());
1758 assert!(component_text_input_min(4000).is_ok());
1759
1760 assert!(component_text_input_min(4001).is_err());
1761 }
1762}