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
14use crate::component::component_v2::{
15 MEDIA_GALLERY_ITEMS_MAX, MEDIA_GALLERY_ITEMS_MIN, MEDIA_GALLERY_ITEM_DESCRIPTION_LENGTH_MAX,
16 SECTION_COMPONENTS_MAX, SECTION_COMPONENTS_MIN, TEXT_DISPLAY_CONTENT_LENGTH_MAX,
17 THUMBNAIL_DESCRIPTION_LENGTH_MAX,
18};
19pub use component_v2::{component_v2, container, media_gallery, section, text_display, thumbnail};
20
21pub const ACTION_ROW_COMPONENT_COUNT: usize = 5;
28
29pub const COMPONENT_COUNT: usize = 5;
36
37pub const COMPONENT_V2_COUNT: usize = 40;
44
45pub const COMPONENT_CUSTOM_ID_LENGTH: usize = 100;
55
56pub const COMPONENT_BUTTON_LABEL_LENGTH: usize = 80;
65
66pub const SELECT_MAXIMUM_VALUES_LIMIT: usize = 25;
74
75pub const SELECT_MAXIMUM_VALUES_REQUIREMENT: usize = 1;
83
84pub const SELECT_MINIMUM_VALUES_LIMIT: usize = 25;
92
93pub const SELECT_OPTION_COUNT: usize = 25;
100
101pub const SELECT_OPTION_DESCRIPTION_LENGTH: usize = 100;
108
109pub const SELECT_OPTION_LABEL_LENGTH: usize = 100;
116
117pub const SELECT_OPTION_VALUE_LENGTH: usize = 100;
124
125pub const SELECT_PLACEHOLDER_LENGTH: usize = 150;
132
133pub const TEXT_INPUT_LABEL_MAX: usize = 45;
139
140pub const TEXT_INPUT_LABEL_MIN: usize = 1;
146
147pub const TEXT_INPUT_LENGTH_MAX: usize = 4000;
153
154pub const TEXT_INPUT_LENGTH_MIN: usize = 1;
160
161pub const TEXT_INPUT_PLACEHOLDER_MAX: usize = 100;
167
168#[derive(Debug)]
173pub struct ComponentValidationError {
174 kind: ComponentValidationErrorType,
176}
177
178impl ComponentValidationError {
179 #[must_use = "retrieving the type has no effect if left unused"]
181 pub const fn kind(&self) -> &ComponentValidationErrorType {
182 &self.kind
183 }
184
185 #[allow(clippy::unused_self)]
187 #[must_use = "consuming the error and retrieving the source has no effect if left unused"]
188 pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
189 None
190 }
191
192 #[must_use = "consuming the error into its parts has no effect if left unused"]
194 pub fn into_parts(
195 self,
196 ) -> (
197 ComponentValidationErrorType,
198 Option<Box<dyn Error + Send + Sync>>,
199 ) {
200 (self.kind, None)
201 }
202}
203
204impl Display for ComponentValidationError {
205 #[allow(clippy::too_many_lines)]
206 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
207 match &self.kind {
208 ComponentValidationErrorType::ActionRowComponentCount { count } => {
209 f.write_str("an action row has ")?;
210 Display::fmt(&count, f)?;
211 f.write_str(" children, but the max is ")?;
212
213 Display::fmt(&ACTION_ROW_COMPONENT_COUNT, f)
214 }
215 ComponentValidationErrorType::ButtonConflict => {
216 f.write_str("button has both a custom id and url, which is never valid")
217 }
218 ComponentValidationErrorType::ButtonStyle { style } => {
219 f.write_str("button has a type of ")?;
220 Debug::fmt(style, f)?;
221 f.write_str(", which must have a ")?;
222
223 f.write_str(if *style == ButtonStyle::Link {
224 "url"
225 } else {
226 "custom id"
227 })?;
228
229 f.write_str(" configured")
230 }
231 ComponentValidationErrorType::ComponentCount { count } => {
232 Display::fmt(count, f)?;
233 f.write_str(" components were provided, but the max is ")?;
234
235 Display::fmt(&COMPONENT_COUNT, f)
236 }
237 ComponentValidationErrorType::ComponentCustomIdLength { chars } => {
238 f.write_str("a component's custom id is ")?;
239 Display::fmt(&chars, f)?;
240 f.write_str(" characters long, but the max is ")?;
241
242 Display::fmt(&COMPONENT_CUSTOM_ID_LENGTH, f)
243 }
244 ComponentValidationErrorType::ComponentLabelLength { chars } => {
245 f.write_str("a component's label is ")?;
246 Display::fmt(&chars, f)?;
247 f.write_str(" characters long, but the max is ")?;
248
249 Display::fmt(&COMPONENT_BUTTON_LABEL_LENGTH, f)
250 }
251 ComponentValidationErrorType::InvalidChildComponent { kind } => {
252 f.write_str("a '")?;
253 Display::fmt(&kind, f)?;
254
255 f.write_str(" component was provided, but can not be a child component")
256 }
257 ComponentValidationErrorType::InvalidRootComponent { kind } => {
258 f.write_str("a '")?;
259 Display::fmt(kind, f)?;
260
261 f.write_str("' component was provided, but can not be a root component")
262 }
263 ComponentValidationErrorType::SelectMaximumValuesCount { count } => {
264 f.write_str("maximum number of values that can be chosen is ")?;
265 Display::fmt(count, f)?;
266 f.write_str(", but must be greater than or equal to ")?;
267 Display::fmt(&SELECT_MAXIMUM_VALUES_REQUIREMENT, f)?;
268 f.write_str("and less than or equal to ")?;
269
270 Display::fmt(&SELECT_MAXIMUM_VALUES_LIMIT, f)
271 }
272 ComponentValidationErrorType::SelectMinimumValuesCount { count } => {
273 f.write_str("maximum number of values that must be chosen is ")?;
274 Display::fmt(count, f)?;
275 f.write_str(", but must be less than or equal to ")?;
276
277 Display::fmt(&SELECT_MAXIMUM_VALUES_LIMIT, f)
278 }
279 ComponentValidationErrorType::SelectNotEnoughDefaultValues { provided, min } => {
280 f.write_str("a select menu provided ")?;
281 Display::fmt(provided, f)?;
282 f.write_str(" values, but it requires at least ")?;
283 Display::fmt(min, f)?;
284 f.write_str(" values")
285 }
286 ComponentValidationErrorType::SelectOptionsMissing => {
287 f.write_str("a text select menu doesn't specify the required options field")
288 }
289 ComponentValidationErrorType::SelectOptionDescriptionLength { chars } => {
290 f.write_str("a select menu option's description is ")?;
291 Display::fmt(&chars, f)?;
292 f.write_str(" characters long, but the max is ")?;
293
294 Display::fmt(&SELECT_OPTION_DESCRIPTION_LENGTH, f)
295 }
296 ComponentValidationErrorType::SelectOptionLabelLength { chars } => {
297 f.write_str("a select menu option's label is ")?;
298 Display::fmt(&chars, f)?;
299 f.write_str(" characters long, but the max is ")?;
300
301 Display::fmt(&SELECT_OPTION_LABEL_LENGTH, f)
302 }
303 ComponentValidationErrorType::SelectOptionValueLength { chars } => {
304 f.write_str("a select menu option's value is ")?;
305 Display::fmt(&chars, f)?;
306 f.write_str(" characters long, but the max is ")?;
307
308 Display::fmt(&SELECT_OPTION_VALUE_LENGTH, f)
309 }
310 ComponentValidationErrorType::SelectPlaceholderLength { chars } => {
311 f.write_str("a select menu's placeholder is ")?;
312 Display::fmt(&chars, f)?;
313 f.write_str(" characters long, but the max is ")?;
314
315 Display::fmt(&SELECT_PLACEHOLDER_LENGTH, f)
316 }
317 ComponentValidationErrorType::SelectOptionCount { count } => {
318 f.write_str("a select menu has ")?;
319 Display::fmt(&count, f)?;
320 f.write_str(" options, but the max is ")?;
321
322 Display::fmt(&SELECT_OPTION_COUNT, f)
323 }
324 ComponentValidationErrorType::SelectTooManyDefaultValues { provided, max } => {
325 f.write_str("a select menu provided ")?;
326 Display::fmt(provided, f)?;
327 f.write_str(" values, but it allows at most ")?;
328 Display::fmt(max, f)?;
329 f.write_str(" values")
330 }
331 ComponentValidationErrorType::SelectUnsupportedDefaultValues { kind } => {
332 f.write_str("a select menu has defined default_values, but its type, ")?;
333 Debug::fmt(kind, f)?;
334 f.write_str(", does not support them")
335 }
336 ComponentValidationErrorType::TextInputLabelLength { len: count } => {
337 f.write_str("a text input label length is ")?;
338 Display::fmt(count, f)?;
339 f.write_str(", but it must be at least ")?;
340 Display::fmt(&TEXT_INPUT_LABEL_MIN, f)?;
341 f.write_str(" and at most ")?;
342
343 Display::fmt(&TEXT_INPUT_LABEL_MAX, f)
344 }
345 ComponentValidationErrorType::TextInputMaxLength { len: count } => {
346 f.write_str("a text input max length is ")?;
347 Display::fmt(count, f)?;
348 f.write_str(", but it must be at least ")?;
349 Display::fmt(&TEXT_INPUT_LENGTH_MIN, f)?;
350 f.write_str(" and at most ")?;
351
352 Display::fmt(&TEXT_INPUT_LENGTH_MAX, f)
353 }
354 ComponentValidationErrorType::TextInputMinLength { len: count } => {
355 f.write_str("a text input min length is ")?;
356 Display::fmt(count, f)?;
357 f.write_str(", but it must be at most ")?;
358
359 Display::fmt(&TEXT_INPUT_LENGTH_MAX, f)
360 }
361 ComponentValidationErrorType::TextInputPlaceholderLength { chars } => {
362 f.write_str("a text input's placeholder is ")?;
363 Display::fmt(&chars, f)?;
364 f.write_str(" characters long, but the max is ")?;
365
366 Display::fmt(&TEXT_INPUT_PLACEHOLDER_MAX, f)
367 }
368 ComponentValidationErrorType::TextInputValueLength { chars } => {
369 f.write_str("a text input's value is ")?;
370 Display::fmt(&chars, f)?;
371 f.write_str(" characters long, but the max is ")?;
372
373 Display::fmt(&TEXT_INPUT_PLACEHOLDER_MAX, f)
374 }
375 ComponentValidationErrorType::DisallowedV2Components => {
376 f.write_str("a V2 component was used in a component V1 message")
377 }
378 ComponentValidationErrorType::DisallowedChildren => {
379 f.write_str("a component contains a disallowed child component")
380 }
381 ComponentValidationErrorType::TextDisplayContentTooLong { len: count } => {
382 f.write_str("a text display content length is ")?;
383 Display::fmt(count, f)?;
384 f.write_str(" characters long, but the max is ")?;
385
386 Display::fmt(&TEXT_DISPLAY_CONTENT_LENGTH_MAX, f)
387 }
388 ComponentValidationErrorType::MediaGalleryItemCountOutOfRange { count } => {
389 f.write_str("a media gallery has ")?;
390 Display::fmt(count, f)?;
391 f.write_str(" items, but the min and max are ")?;
392 Display::fmt(&MEDIA_GALLERY_ITEMS_MIN, f)?;
393 f.write_str(" and ")?;
394 Display::fmt(&MEDIA_GALLERY_ITEMS_MAX, f)?;
395
396 f.write_str(" respectively")
397 }
398 ComponentValidationErrorType::MediaGalleryItemDescriptionTooLong { len } => {
399 f.write_str("a media gallery item description length is ")?;
400 Display::fmt(len, f)?;
401 f.write_str(" characters long, but the max is ")?;
402
403 Display::fmt(&MEDIA_GALLERY_ITEM_DESCRIPTION_LENGTH_MAX, f)
404 }
405 ComponentValidationErrorType::SectionComponentCountOutOfRange { count } => {
406 f.write_str("a section has ")?;
407 Display::fmt(count, f)?;
408 f.write_str(" components, but the min and max are ")?;
409 Display::fmt(&SECTION_COMPONENTS_MIN, f)?;
410 f.write_str(" and ")?;
411 Display::fmt(&SECTION_COMPONENTS_MAX, f)?;
412
413 f.write_str(" respectively")
414 }
415 ComponentValidationErrorType::ThumbnailDescriptionTooLong { len } => {
416 f.write_str("a thumbnail description length is ")?;
417 Display::fmt(len, f)?;
418 f.write_str(" characters long, but the max is ")?;
419
420 Display::fmt(&THUMBNAIL_DESCRIPTION_LENGTH_MAX, f)
421 }
422 }
423 }
424}
425
426impl Error for ComponentValidationError {}
427
428#[derive(Debug)]
430#[non_exhaustive]
431pub enum ComponentValidationErrorType {
432 ActionRowComponentCount {
435 count: usize,
437 },
438 ButtonConflict,
440 ButtonStyle {
445 style: ButtonStyle,
447 },
448 ComponentCount {
451 count: usize,
453 },
454 ComponentCustomIdLength {
457 chars: usize,
459 },
460 ComponentLabelLength {
462 chars: usize,
464 },
465 InvalidChildComponent {
467 kind: ComponentType,
469 },
470 InvalidRootComponent {
472 kind: ComponentType,
474 },
475 SelectMaximumValuesCount {
479 count: usize,
481 },
482 SelectMinimumValuesCount {
485 count: usize,
487 },
488 SelectNotEnoughDefaultValues {
490 provided: usize,
492 min: usize,
494 },
495 SelectOptionsMissing,
499 SelectOptionCount {
502 count: usize,
504 },
505 SelectOptionDescriptionLength {
508 chars: usize,
510 },
511 SelectOptionLabelLength {
514 chars: usize,
516 },
517 SelectOptionValueLength {
520 chars: usize,
522 },
523 SelectPlaceholderLength {
526 chars: usize,
528 },
529 SelectTooManyDefaultValues {
531 provided: usize,
533 max: usize,
535 },
536 SelectUnsupportedDefaultValues {
538 kind: SelectMenuType,
540 },
541 TextInputLabelLength {
543 len: usize,
545 },
546 TextInputMaxLength {
548 len: usize,
550 },
551 TextInputMinLength {
553 len: usize,
555 },
556 TextInputPlaceholderLength {
559 chars: usize,
561 },
562 TextInputValueLength {
565 chars: usize,
567 },
568 DisallowedV2Components,
570 DisallowedChildren,
572 TextDisplayContentTooLong {
574 len: usize,
576 },
577 MediaGalleryItemCountOutOfRange {
579 count: usize,
581 },
582 MediaGalleryItemDescriptionTooLong {
584 len: usize,
586 },
587 SectionComponentCountOutOfRange {
589 count: usize,
591 },
592 ThumbnailDescriptionTooLong {
594 len: usize,
596 },
597}
598
599pub fn component_v1(component: &Component) -> Result<(), ComponentValidationError> {
619 match component {
620 Component::ActionRow(action_row) => self::action_row(action_row, false)?,
621 other => {
622 return Err(ComponentValidationError {
623 kind: ComponentValidationErrorType::InvalidRootComponent { kind: other.kind() },
624 });
625 }
626 }
627
628 Ok(())
629}
630
631#[deprecated(note = "Use component_v1 for old components and component_v2 for new ones")]
649pub fn component(component: &Component) -> Result<(), ComponentValidationError> {
650 component_v1(component)
651}
652
653pub fn action_row(action_row: &ActionRow, is_v2: bool) -> Result<(), ComponentValidationError> {
676 self::component_action_row_components(&action_row.components)?;
677
678 for component in &action_row.components {
679 match component {
680 Component::ActionRow(_) => {
681 return Err(ComponentValidationError {
682 kind: ComponentValidationErrorType::InvalidChildComponent {
683 kind: ComponentType::ActionRow,
684 },
685 });
686 }
687 Component::Button(button) => self::button(button)?,
688 Component::SelectMenu(select_menu) => self::select_menu(select_menu)?,
689 Component::TextInput(text_input) => self::text_input(text_input)?,
690 Component::Unknown(unknown) => {
691 return Err(ComponentValidationError {
692 kind: ComponentValidationErrorType::InvalidChildComponent {
693 kind: ComponentType::Unknown(*unknown),
694 },
695 })
696 }
697
698 Component::TextDisplay(_)
699 | Component::MediaGallery(_)
700 | Component::Separator(_)
701 | Component::File(_)
702 | Component::Section(_)
703 | Component::Container(_)
704 | Component::Thumbnail(_) => {
705 return Err(ComponentValidationError {
706 kind: if is_v2 {
707 ComponentValidationErrorType::DisallowedChildren
708 } else {
709 ComponentValidationErrorType::DisallowedV2Components
710 },
711 })
712 }
713 }
714 }
715
716 Ok(())
717}
718
719pub fn button(button: &Button) -> Result<(), ComponentValidationError> {
741 let has_custom_id = button.custom_id.is_some();
742 let has_emoji = button.emoji.is_some();
743 let has_label = button.label.is_some();
744 let has_sku_id = button.sku_id.is_some();
745 let has_url = button.url.is_some();
746
747 if has_custom_id && has_url {
750 return Err(ComponentValidationError {
751 kind: ComponentValidationErrorType::ButtonConflict,
752 });
753 }
754
755 let is_premium = button.style == ButtonStyle::Premium;
760 if is_premium && (has_custom_id || has_url || has_label || has_emoji || !has_sku_id) {
761 return Err(ComponentValidationError {
762 kind: ComponentValidationErrorType::ButtonStyle {
763 style: button.style,
764 },
765 });
766 }
767
768 let is_link = button.style == ButtonStyle::Link;
773
774 if (is_link && !has_url) || (!is_link && !has_custom_id) {
775 return Err(ComponentValidationError {
776 kind: ComponentValidationErrorType::ButtonStyle {
777 style: button.style,
778 },
779 });
780 }
781
782 if let Some(custom_id) = button.custom_id.as_ref() {
783 self::component_custom_id(custom_id)?;
784 }
785
786 if let Some(label) = button.label.as_ref() {
787 self::component_button_label(label)?;
788 }
789
790 Ok(())
791}
792
793pub fn select_menu(select_menu: &SelectMenu) -> Result<(), ComponentValidationError> {
843 self::component_custom_id(&select_menu.custom_id)?;
844
845 if let SelectMenuType::Text = &select_menu.kind {
847 let options = select_menu
848 .options
849 .as_ref()
850 .ok_or(ComponentValidationError {
851 kind: ComponentValidationErrorType::SelectOptionsMissing,
852 })?;
853 for option in options {
854 component_select_option_label(&option.label)?;
855 component_select_option_value(&option.value)?;
856
857 if let Some(description) = option.description.as_ref() {
858 component_option_description(description)?;
859 }
860 }
861 component_select_options(options)?;
862 }
863
864 if let Some(placeholder) = select_menu.placeholder.as_ref() {
865 self::component_select_placeholder(placeholder)?;
866 }
867
868 if let Some(max_values) = select_menu.max_values {
869 self::component_select_max_values(usize::from(max_values))?;
870 }
871
872 if let Some(min_values) = select_menu.min_values {
873 self::component_select_min_values(usize::from(min_values))?;
874 }
875
876 if let Some(default_values) = select_menu.default_values.as_ref() {
877 component_select_default_values_supported(select_menu.kind)?;
878 component_select_default_values_count(
879 select_menu.min_values,
880 select_menu.max_values,
881 default_values.len(),
882 )?;
883 }
884
885 Ok(())
886}
887
888pub fn text_input(text_input: &TextInput) -> Result<(), ComponentValidationError> {
914 self::component_custom_id(&text_input.custom_id)?;
915 self::component_text_input_label(&text_input.label)?;
916
917 if let Some(max_length) = text_input.max_length {
918 self::component_text_input_max(max_length)?;
919 }
920
921 if let Some(min_length) = text_input.min_length {
922 self::component_text_input_min(min_length)?;
923 }
924
925 if let Some(placeholder) = text_input.placeholder.as_ref() {
926 self::component_text_input_placeholder(placeholder)?;
927 }
928
929 if let Some(value) = text_input.value.as_ref() {
930 self::component_text_input_value(value)?;
931 }
932
933 Ok(())
934}
935
936const fn component_action_row_components(
949 components: &[Component],
950) -> Result<(), ComponentValidationError> {
951 let count = components.len();
952
953 if count > COMPONENT_COUNT {
954 return Err(ComponentValidationError {
955 kind: ComponentValidationErrorType::ActionRowComponentCount { count },
956 });
957 }
958
959 Ok(())
960}
961
962fn component_button_label(label: impl AsRef<str>) -> Result<(), ComponentValidationError> {
971 let chars = label.as_ref().chars().count();
972
973 if chars > COMPONENT_BUTTON_LABEL_LENGTH {
974 return Err(ComponentValidationError {
975 kind: ComponentValidationErrorType::ComponentLabelLength { chars },
976 });
977 }
978
979 Ok(())
980}
981
982fn component_custom_id(custom_id: impl AsRef<str>) -> Result<(), ComponentValidationError> {
991 let chars = custom_id.as_ref().chars().count();
992
993 if chars > COMPONENT_CUSTOM_ID_LENGTH {
994 return Err(ComponentValidationError {
995 kind: ComponentValidationErrorType::ComponentCustomIdLength { chars },
996 });
997 }
998
999 Ok(())
1000}
1001
1002fn component_option_description(
1012 description: impl AsRef<str>,
1013) -> Result<(), ComponentValidationError> {
1014 let chars = description.as_ref().chars().count();
1015
1016 if chars > SELECT_OPTION_DESCRIPTION_LENGTH {
1017 return Err(ComponentValidationError {
1018 kind: ComponentValidationErrorType::SelectOptionDescriptionLength { chars },
1019 });
1020 }
1021
1022 Ok(())
1023}
1024
1025const fn component_select_default_values_supported(
1032 menu_type: SelectMenuType,
1033) -> Result<(), ComponentValidationError> {
1034 if !matches!(
1035 menu_type,
1036 SelectMenuType::User
1037 | SelectMenuType::Role
1038 | SelectMenuType::Mentionable
1039 | SelectMenuType::Channel
1040 ) {
1041 return Err(ComponentValidationError {
1042 kind: ComponentValidationErrorType::SelectUnsupportedDefaultValues { kind: menu_type },
1043 });
1044 }
1045
1046 Ok(())
1047}
1048
1049const fn component_select_default_values_count(
1059 min_values: Option<u8>,
1060 max_values: Option<u8>,
1061 default_values: usize,
1062) -> Result<(), ComponentValidationError> {
1063 if let Some(min) = min_values {
1064 let min = min as usize;
1065 if default_values < min {
1066 return Err(ComponentValidationError {
1067 kind: ComponentValidationErrorType::SelectNotEnoughDefaultValues {
1068 provided: default_values,
1069 min,
1070 },
1071 });
1072 }
1073 }
1074 if let Some(max) = max_values {
1075 let max = max as usize;
1076 if default_values > max {
1077 return Err(ComponentValidationError {
1078 kind: ComponentValidationErrorType::SelectTooManyDefaultValues {
1079 provided: default_values,
1080 max,
1081 },
1082 });
1083 }
1084 }
1085
1086 Ok(())
1087}
1088
1089const fn component_select_max_values(count: usize) -> Result<(), ComponentValidationError> {
1101 if count > SELECT_MAXIMUM_VALUES_LIMIT {
1102 return Err(ComponentValidationError {
1103 kind: ComponentValidationErrorType::SelectMaximumValuesCount { count },
1104 });
1105 }
1106
1107 if count < SELECT_MAXIMUM_VALUES_REQUIREMENT {
1108 return Err(ComponentValidationError {
1109 kind: ComponentValidationErrorType::SelectMaximumValuesCount { count },
1110 });
1111 }
1112
1113 Ok(())
1114}
1115
1116const fn component_select_min_values(count: usize) -> Result<(), ComponentValidationError> {
1127 if count > SELECT_MINIMUM_VALUES_LIMIT {
1128 return Err(ComponentValidationError {
1129 kind: ComponentValidationErrorType::SelectMinimumValuesCount { count },
1130 });
1131 }
1132
1133 Ok(())
1134}
1135
1136fn component_select_option_label(label: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1146 let chars = label.as_ref().chars().count();
1147
1148 if chars > SELECT_OPTION_LABEL_LENGTH {
1149 return Err(ComponentValidationError {
1150 kind: ComponentValidationErrorType::SelectOptionLabelLength { chars },
1151 });
1152 }
1153
1154 Ok(())
1155}
1156
1157fn component_select_option_value(value: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1167 let chars = value.as_ref().chars().count();
1168
1169 if chars > SELECT_OPTION_VALUE_LENGTH {
1170 return Err(ComponentValidationError {
1171 kind: ComponentValidationErrorType::SelectOptionValueLength { chars },
1172 });
1173 }
1174
1175 Ok(())
1176}
1177
1178const fn component_select_options(
1193 options: &[SelectMenuOption],
1194) -> Result<(), ComponentValidationError> {
1195 let count = options.len();
1196
1197 if count > SELECT_OPTION_COUNT {
1198 return Err(ComponentValidationError {
1199 kind: ComponentValidationErrorType::SelectOptionCount { count },
1200 });
1201 }
1202
1203 Ok(())
1204}
1205
1206fn component_select_placeholder(
1216 placeholder: impl AsRef<str>,
1217) -> Result<(), ComponentValidationError> {
1218 let chars = placeholder.as_ref().chars().count();
1219
1220 if chars > SELECT_PLACEHOLDER_LENGTH {
1221 return Err(ComponentValidationError {
1222 kind: ComponentValidationErrorType::SelectPlaceholderLength { chars },
1223 });
1224 }
1225
1226 Ok(())
1227}
1228
1229fn component_text_input_label(label: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1241 let len = label.as_ref().len();
1242
1243 if (TEXT_INPUT_LABEL_MIN..=TEXT_INPUT_LABEL_MAX).contains(&len) {
1244 Ok(())
1245 } else {
1246 Err(ComponentValidationError {
1247 kind: ComponentValidationErrorType::TextInputLabelLength { len },
1248 })
1249 }
1250}
1251
1252const fn component_text_input_max(len: u16) -> Result<(), ComponentValidationError> {
1261 let len = len as usize;
1262
1263 if len >= TEXT_INPUT_LENGTH_MIN && len <= TEXT_INPUT_LENGTH_MAX {
1264 Ok(())
1265 } else {
1266 Err(ComponentValidationError {
1267 kind: ComponentValidationErrorType::TextInputMaxLength { len },
1268 })
1269 }
1270}
1271
1272const fn component_text_input_min(len: u16) -> Result<(), ComponentValidationError> {
1281 let len = len as usize;
1282
1283 if len <= TEXT_INPUT_LENGTH_MAX {
1284 Ok(())
1285 } else {
1286 Err(ComponentValidationError {
1287 kind: ComponentValidationErrorType::TextInputMinLength { len },
1288 })
1289 }
1290}
1291
1292fn component_text_input_placeholder(
1304 placeholder: impl AsRef<str>,
1305) -> Result<(), ComponentValidationError> {
1306 let chars = placeholder.as_ref().chars().count();
1307
1308 if chars <= TEXT_INPUT_PLACEHOLDER_MAX {
1309 Ok(())
1310 } else {
1311 Err(ComponentValidationError {
1312 kind: ComponentValidationErrorType::TextInputPlaceholderLength { chars },
1313 })
1314 }
1315}
1316
1317fn component_text_input_value(value: impl AsRef<str>) -> Result<(), ComponentValidationError> {
1326 let chars = value.as_ref().chars().count();
1327
1328 if chars <= TEXT_INPUT_LENGTH_MAX {
1329 Ok(())
1330 } else {
1331 Err(ComponentValidationError {
1332 kind: ComponentValidationErrorType::TextInputValueLength { chars },
1333 })
1334 }
1335}
1336
1337#[allow(clippy::non_ascii_literal)]
1338#[cfg(test)]
1339mod tests {
1340 use super::*;
1341 use static_assertions::{assert_fields, assert_impl_all};
1342 use twilight_model::channel::message::EmojiReactionType;
1343
1344 assert_fields!(ComponentValidationErrorType::ActionRowComponentCount: count);
1345 assert_fields!(ComponentValidationErrorType::ComponentCount: count);
1346 assert_fields!(ComponentValidationErrorType::ComponentCustomIdLength: chars);
1347 assert_fields!(ComponentValidationErrorType::ComponentLabelLength: chars);
1348 assert_fields!(ComponentValidationErrorType::InvalidChildComponent: kind);
1349 assert_fields!(ComponentValidationErrorType::InvalidRootComponent: kind);
1350 assert_fields!(ComponentValidationErrorType::SelectMaximumValuesCount: count);
1351 assert_fields!(ComponentValidationErrorType::SelectMinimumValuesCount: count);
1352 assert_fields!(ComponentValidationErrorType::SelectOptionDescriptionLength: chars);
1353 assert_fields!(ComponentValidationErrorType::SelectOptionLabelLength: chars);
1354 assert_fields!(ComponentValidationErrorType::SelectOptionValueLength: chars);
1355 assert_fields!(ComponentValidationErrorType::SelectPlaceholderLength: chars);
1356 assert_impl_all!(ComponentValidationErrorType: Debug, Send, Sync);
1357 assert_impl_all!(ComponentValidationError: Debug, Send, Sync);
1358
1359 const ALL_BUTTON_STYLES: &[ButtonStyle] = &[
1361 ButtonStyle::Primary,
1362 ButtonStyle::Secondary,
1363 ButtonStyle::Success,
1364 ButtonStyle::Danger,
1365 ButtonStyle::Link,
1366 ButtonStyle::Premium,
1367 ];
1368
1369 #[test]
1370 fn component_action_row() {
1371 let button = Button {
1372 custom_id: None,
1373 disabled: false,
1374 emoji: Some(EmojiReactionType::Unicode {
1375 name: "📚".into()
1376 }),
1377 label: Some("Read".into()),
1378 style: ButtonStyle::Link,
1379 url: Some("https://abebooks.com".into()),
1380 sku_id: None,
1381 id: None,
1382 };
1383
1384 let select_menu = SelectMenu {
1385 channel_types: None,
1386 custom_id: "custom id 2".into(),
1387 disabled: false,
1388 default_values: None,
1389 kind: SelectMenuType::Text,
1390 max_values: Some(2),
1391 min_values: Some(1),
1392 options: Some(Vec::from([SelectMenuOption {
1393 default: true,
1394 description: Some("Book 1 of the Expanse".into()),
1395 emoji: None,
1396 label: "Leviathan Wakes".into(),
1397 value: "9780316129084".into(),
1398 }])),
1399 placeholder: Some("Choose a book".into()),
1400 id: None,
1401 };
1402
1403 let action_row = ActionRow {
1404 components: Vec::from([
1405 Component::SelectMenu(select_menu.clone()),
1406 Component::Button(button),
1407 ]),
1408 id: None,
1409 };
1410
1411 assert!(component_v1(&Component::ActionRow(action_row.clone())).is_ok());
1412
1413 assert!(component_v1(&Component::SelectMenu(select_menu.clone())).is_err());
1414
1415 assert!(super::action_row(&action_row, false).is_ok());
1416
1417 let invalid_action_row = Component::ActionRow(ActionRow {
1418 components: Vec::from([
1419 Component::SelectMenu(select_menu.clone()),
1420 Component::SelectMenu(select_menu.clone()),
1421 Component::SelectMenu(select_menu.clone()),
1422 Component::SelectMenu(select_menu.clone()),
1423 Component::SelectMenu(select_menu.clone()),
1424 Component::SelectMenu(select_menu),
1425 ]),
1426 id: None,
1427 });
1428
1429 assert!(component_v1(&invalid_action_row).is_err());
1430 }
1431
1432 #[test]
1435 fn button_conflict() {
1436 let button = Button {
1437 custom_id: Some("a".to_owned()),
1438 disabled: false,
1439 emoji: None,
1440 label: None,
1441 style: ButtonStyle::Primary,
1442 url: Some("https://twilight.rs".to_owned()),
1443 sku_id: None,
1444 id: None,
1445 };
1446
1447 assert!(matches!(
1448 super::button(&button),
1449 Err(ComponentValidationError {
1450 kind: ComponentValidationErrorType::ButtonConflict,
1451 }),
1452 ));
1453 }
1454
1455 #[test]
1458 fn button_style() {
1459 for style in ALL_BUTTON_STYLES {
1460 let button = Button {
1461 custom_id: None,
1462 disabled: false,
1463 emoji: None,
1464 label: Some("some label".to_owned()),
1465 style: *style,
1466 url: None,
1467 sku_id: None,
1468 id: None,
1469 };
1470
1471 assert!(matches!(
1472 super::button(&button),
1473 Err(ComponentValidationError {
1474 kind: ComponentValidationErrorType::ButtonStyle {
1475 style: error_style,
1476 }
1477 })
1478 if error_style == *style
1479 ));
1480 }
1481 }
1482
1483 #[test]
1484 fn component_label() {
1485 assert!(component_button_label("").is_ok());
1486 assert!(component_button_label("a").is_ok());
1487 assert!(component_button_label("a".repeat(80)).is_ok());
1488
1489 assert!(component_button_label("a".repeat(81)).is_err());
1490 }
1491
1492 #[test]
1493 fn component_custom_id_length() {
1494 assert!(component_custom_id("").is_ok());
1495 assert!(component_custom_id("a").is_ok());
1496 assert!(component_custom_id("a".repeat(100)).is_ok());
1497
1498 assert!(component_custom_id("a".repeat(101)).is_err());
1499 }
1500
1501 #[test]
1502 fn component_option_description_length() {
1503 assert!(component_option_description("").is_ok());
1504 assert!(component_option_description("a").is_ok());
1505 assert!(component_option_description("a".repeat(100)).is_ok());
1506
1507 assert!(component_option_description("a".repeat(101)).is_err());
1508 }
1509
1510 #[test]
1511 fn component_select_default_values_support() {
1512 assert!(component_select_default_values_supported(SelectMenuType::User).is_ok());
1513 assert!(component_select_default_values_supported(SelectMenuType::Role).is_ok());
1514 assert!(component_select_default_values_supported(SelectMenuType::Mentionable).is_ok());
1515 assert!(component_select_default_values_supported(SelectMenuType::Channel).is_ok());
1516
1517 assert!(component_select_default_values_supported(SelectMenuType::Text).is_err());
1518 }
1519
1520 #[test]
1521 fn component_select_num_default_values() {
1522 assert!(component_select_default_values_count(None, None, 0).is_ok());
1523 assert!(component_select_default_values_count(None, None, 1).is_ok());
1524 assert!(component_select_default_values_count(Some(1), None, 5).is_ok());
1525 assert!(component_select_default_values_count(Some(5), None, 5).is_ok());
1526 assert!(component_select_default_values_count(None, Some(5), 5).is_ok());
1527 assert!(component_select_default_values_count(None, Some(10), 5).is_ok());
1528 assert!(component_select_default_values_count(Some(5), Some(5), 5).is_ok());
1529 assert!(component_select_default_values_count(Some(1), Some(10), 5).is_ok());
1530
1531 assert!(component_select_default_values_count(Some(2), None, 1).is_err());
1532 assert!(component_select_default_values_count(None, Some(1), 2).is_err());
1533 assert!(component_select_default_values_count(Some(1), Some(1), 2).is_err());
1534 assert!(component_select_default_values_count(Some(2), Some(2), 1).is_err());
1535 }
1536
1537 #[test]
1538 fn component_select_max_values_count() {
1539 assert!(component_select_max_values(1).is_ok());
1540 assert!(component_select_max_values(25).is_ok());
1541
1542 assert!(component_select_max_values(0).is_err());
1543 assert!(component_select_max_values(26).is_err());
1544 }
1545
1546 #[test]
1547 fn component_select_min_values_count() {
1548 assert!(component_select_min_values(1).is_ok());
1549 assert!(component_select_min_values(25).is_ok());
1550
1551 assert!(component_select_min_values(26).is_err());
1552 }
1553
1554 #[test]
1555 fn component_select_option_value_length() {
1556 assert!(component_select_option_value("a").is_ok());
1557 assert!(component_select_option_value("a".repeat(100)).is_ok());
1558
1559 assert!(component_select_option_value("a".repeat(101)).is_err());
1560 }
1561
1562 #[test]
1563 fn component_select_options_count() {
1564 let select_menu_options = Vec::from([SelectMenuOption {
1565 default: false,
1566 description: None,
1567 emoji: None,
1568 label: "label".into(),
1569 value: "value".into(),
1570 }]);
1571
1572 assert!(component_select_options(&select_menu_options).is_ok());
1573
1574 let select_menu_options_25 = select_menu_options
1575 .iter()
1576 .cloned()
1577 .cycle()
1578 .take(25)
1579 .collect::<Vec<SelectMenuOption>>();
1580
1581 assert!(component_select_options(&select_menu_options_25).is_ok());
1582
1583 let select_menu_options_26 = select_menu_options
1584 .iter()
1585 .cloned()
1586 .cycle()
1587 .take(26)
1588 .collect::<Vec<SelectMenuOption>>();
1589
1590 assert!(component_select_options(&select_menu_options_26).is_err());
1591 }
1592
1593 #[test]
1594 fn component_select_placeholder_length() {
1595 assert!(component_select_placeholder("").is_ok());
1596 assert!(component_select_placeholder("a").is_ok());
1597 assert!(component_select_placeholder("a".repeat(150)).is_ok());
1598
1599 assert!(component_select_placeholder("a".repeat(151)).is_err());
1600 }
1601
1602 #[test]
1603 fn component_text_input_label_length() {
1604 assert!(component_text_input_label("a").is_ok());
1605 assert!(component_text_input_label("a".repeat(45)).is_ok());
1606
1607 assert!(component_text_input_label("").is_err());
1608 assert!(component_text_input_label("a".repeat(46)).is_err());
1609 }
1610
1611 #[test]
1612 fn component_text_input_max_count() {
1613 assert!(component_text_input_max(1).is_ok());
1614 assert!(component_text_input_max(4000).is_ok());
1615
1616 assert!(component_text_input_max(0).is_err());
1617 assert!(component_text_input_max(4001).is_err());
1618 }
1619
1620 #[test]
1621 fn component_text_input_min_count() {
1622 assert!(component_text_input_min(0).is_ok());
1623 assert!(component_text_input_min(1).is_ok());
1624 assert!(component_text_input_min(4000).is_ok());
1625
1626 assert!(component_text_input_min(4001).is_err());
1627 }
1628
1629 #[test]
1630 fn component_text_input_placeholder_length() {
1631 assert!(component_text_input_placeholder("").is_ok());
1632 assert!(component_text_input_placeholder("a").is_ok());
1633 assert!(component_text_input_placeholder("a".repeat(100)).is_ok());
1634
1635 assert!(component_text_input_placeholder("a".repeat(101)).is_err());
1636 }
1637
1638 #[test]
1639 fn component_text_input_value() {
1640 assert!(component_text_input_min(0).is_ok());
1641 assert!(component_text_input_min(1).is_ok());
1642 assert!(component_text_input_min(4000).is_ok());
1643
1644 assert!(component_text_input_min(4001).is_err());
1645 }
1646}