1use std::{
4 collections::{HashMap, HashSet},
5 error::Error,
6 fmt::{Display, Formatter, Result as FmtResult},
7};
8use twilight_model::application::command::{
9 Command, CommandOption, CommandOptionChoice, CommandOptionChoiceValue, CommandOptionType,
10 CommandType,
11};
12
13pub const CHOICES_LIMIT: usize = 25;
15
16pub const COMMAND_TOTAL_LENGTH: usize = 4000;
18
19pub const DESCRIPTION_LENGTH_MAX: usize = 100;
21
22pub const DESCRIPTION_LENGTH_MIN: usize = 1;
24
25pub const NAME_LENGTH_MAX: usize = 32;
27
28pub const NAME_LENGTH_MIN: usize = 1;
30
31pub const OPTIONS_LIMIT: usize = 25;
33
34pub const OPTION_CHOICE_NAME_LENGTH_MAX: usize = 100;
36
37pub const OPTION_CHOICE_NAME_LENGTH_MIN: usize = 1;
39
40pub const OPTION_CHOICE_STRING_VALUE_LENGTH_MAX: usize = 100;
42
43pub const OPTION_CHOICE_STRING_VALUE_LENGTH_MIN: usize = 1;
45
46pub const OPTION_DESCRIPTION_LENGTH_MAX: usize = 100;
48
49pub const OPTION_DESCRIPTION_LENGTH_MIN: usize = 1;
51
52pub const OPTION_NAME_LENGTH_MAX: usize = 32;
54
55pub const OPTION_NAME_LENGTH_MIN: usize = 1;
57
58pub const GUILD_COMMAND_LIMIT: usize = 100;
61
62pub const GUILD_COMMAND_PERMISSION_LIMIT: usize = 10;
65
66#[derive(Debug)]
68pub struct CommandValidationError {
69 kind: CommandValidationErrorType,
71}
72
73impl CommandValidationError {
74 pub const COMMAND_COUNT_INVALID: CommandValidationError = CommandValidationError {
79 kind: CommandValidationErrorType::CountInvalid,
80 };
81
82 #[must_use = "retrieving the type has no effect if left unused"]
84 pub const fn kind(&self) -> &CommandValidationErrorType {
85 &self.kind
86 }
87
88 #[allow(clippy::unused_self)]
90 #[must_use = "consuming the error and retrieving the source has no effect if left unused"]
91 pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
92 None
93 }
94
95 #[must_use = "consuming the error into its parts has no effect if left unused"]
97 pub fn into_parts(
98 self,
99 ) -> (
100 CommandValidationErrorType,
101 Option<Box<dyn Error + Send + Sync>>,
102 ) {
103 (self.kind, None)
104 }
105
106 #[must_use = "creating an error has no effect if left unused"]
111 pub const fn option_name_not_unique(option_index: usize) -> Self {
112 Self {
113 kind: CommandValidationErrorType::OptionNameNotUnique { option_index },
114 }
115 }
116
117 #[must_use = "creating an error has no effect if left unused"]
121 pub const fn option_required_first(index: usize) -> Self {
122 Self {
123 kind: CommandValidationErrorType::OptionsRequiredFirst { index },
124 }
125 }
126}
127
128impl Display for CommandValidationError {
129 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
130 match &self.kind {
131 CommandValidationErrorType::CountInvalid => {
132 f.write_str("more than ")?;
133 Display::fmt(&GUILD_COMMAND_LIMIT, f)?;
134
135 f.write_str(" commands were set")
136 }
137 CommandValidationErrorType::CommandTooLarge { characters } => {
138 f.write_str("the combined total length of the command is ")?;
139 Display::fmt(characters, f)?;
140 f.write_str(" characters long, but the max is ")?;
141
142 Display::fmt(&COMMAND_TOTAL_LENGTH, f)
143 }
144 CommandValidationErrorType::DescriptionInvalid => {
145 f.write_str("command description must be between ")?;
146 Display::fmt(&DESCRIPTION_LENGTH_MIN, f)?;
147 f.write_str(" and ")?;
148 Display::fmt(&DESCRIPTION_LENGTH_MAX, f)?;
149
150 f.write_str(" characters")
151 }
152 CommandValidationErrorType::DescriptionNotAllowed => f.write_str(
153 "command description must be a empty string on message and user commands",
154 ),
155 CommandValidationErrorType::NameLengthInvalid => {
156 f.write_str("command name must be between ")?;
157 Display::fmt(&NAME_LENGTH_MIN, f)?;
158 f.write_str(" and ")?;
159
160 Display::fmt(&NAME_LENGTH_MAX, f)
161 }
162 CommandValidationErrorType::NameCharacterInvalid { character } => {
163 f.write_str(
164 "command name must only contain lowercase alphanumeric characters, found `",
165 )?;
166 Display::fmt(character, f)?;
167
168 f.write_str("`")
169 }
170 CommandValidationErrorType::OptionDescriptionInvalid => {
171 f.write_str("command option description must be between ")?;
172 Display::fmt(&OPTION_DESCRIPTION_LENGTH_MIN, f)?;
173 f.write_str(" and ")?;
174 Display::fmt(&OPTION_DESCRIPTION_LENGTH_MAX, f)?;
175
176 f.write_str(" characters")
177 }
178 CommandValidationErrorType::OptionNameNotUnique { option_index } => {
179 f.write_str("command option at index ")?;
180 Display::fmt(option_index, f)?;
181
182 f.write_str(" has the same name as another option")
183 }
184 CommandValidationErrorType::OptionNameLengthInvalid => {
185 f.write_str("command option name must be between ")?;
186 Display::fmt(&OPTION_NAME_LENGTH_MIN, f)?;
187 f.write_str(" and ")?;
188
189 Display::fmt(&OPTION_NAME_LENGTH_MAX, f)
190 }
191 CommandValidationErrorType::OptionNameCharacterInvalid { character } => {
192 f.write_str("command option name must only contain lowercase alphanumeric characters, found `")?;
193 Display::fmt(character, f)?;
194
195 f.write_str("`")
196 }
197 CommandValidationErrorType::OptionChoiceNameLengthInvalid => {
198 f.write_str("command option choice name must be between ")?;
199 Display::fmt(&OPTION_CHOICE_NAME_LENGTH_MIN, f)?;
200 f.write_str(" and ")?;
201 Display::fmt(&OPTION_CHOICE_NAME_LENGTH_MAX, f)?;
202
203 f.write_str(" characters")
204 }
205 CommandValidationErrorType::OptionChoiceStringValueLengthInvalid => {
206 f.write_str("command option choice string value must be between ")?;
207 Display::fmt(&OPTION_CHOICE_STRING_VALUE_LENGTH_MIN, f)?;
208 f.write_str(" and ")?;
209 Display::fmt(&OPTION_CHOICE_STRING_VALUE_LENGTH_MAX, f)?;
210
211 f.write_str(" characters")
212 }
213 CommandValidationErrorType::OptionsCountInvalid => {
214 f.write_str("more than ")?;
215 Display::fmt(&OPTIONS_LIMIT, f)?;
216
217 f.write_str(" options were set")
218 }
219 CommandValidationErrorType::OptionsRequiredFirst { .. } => {
220 f.write_str("optional command options must be added after required")
221 }
222 CommandValidationErrorType::PermissionsCountInvalid => {
223 f.write_str("more than ")?;
224 Display::fmt(&GUILD_COMMAND_PERMISSION_LIMIT, f)?;
225
226 f.write_str(" permission overwrites were set")
227 }
228 }
229 }
230}
231
232impl Error for CommandValidationError {}
233
234#[derive(Debug)]
236#[non_exhaustive]
237pub enum CommandValidationErrorType {
238 CountInvalid,
243 CommandTooLarge {
252 characters: usize,
254 },
255 DescriptionInvalid,
257 DescriptionNotAllowed,
259 NameLengthInvalid,
261 NameCharacterInvalid {
263 character: char,
265 },
266 OptionDescriptionInvalid,
268 OptionNameLengthInvalid,
270 OptionNameNotUnique {
272 option_index: usize,
274 },
275 OptionNameCharacterInvalid {
277 character: char,
279 },
280 OptionChoiceNameLengthInvalid,
282 OptionChoiceStringValueLengthInvalid,
284 OptionsCountInvalid,
286 OptionsRequiredFirst {
288 index: usize,
290 },
291 PermissionsCountInvalid,
293}
294
295pub fn command(value: &Command) -> Result<(), CommandValidationError> {
309 let characters = self::command_characters(value);
310
311 if characters > COMMAND_TOTAL_LENGTH {
312 return Err(CommandValidationError {
313 kind: CommandValidationErrorType::CommandTooLarge { characters },
314 });
315 }
316
317 let Command {
318 description,
319 description_localizations,
320 name,
321 name_localizations,
322 kind,
323 ..
324 } = value;
325
326 if *kind == CommandType::ChatInput {
327 self::description(description)?;
328 if let Some(description_localizations) = description_localizations {
329 for description in description_localizations.values() {
330 self::description(description)?;
331 }
332 }
333 } else if !description.is_empty() {
334 return Err(CommandValidationError {
335 kind: CommandValidationErrorType::DescriptionNotAllowed,
336 });
337 };
338
339 if let Some(name_localizations) = name_localizations {
340 for name in name_localizations.values() {
341 match kind {
342 CommandType::ChatInput => self::chat_input_name(name)?,
343 CommandType::User | CommandType::Message => {
344 self::name(name)?;
345 }
346 CommandType::Unknown(_) => (),
347 _ => unimplemented!(),
348 }
349 }
350 }
351
352 match kind {
353 CommandType::ChatInput => self::chat_input_name(name),
354 CommandType::User | CommandType::Message => self::name(name),
355 CommandType::Unknown(_) => Ok(()),
356 _ => unimplemented!(),
357 }
358}
359
360pub fn command_characters(command: &Command) -> usize {
362 let mut characters =
363 longest_localization_characters(&command.name, command.name_localizations.as_ref())
364 + longest_localization_characters(
365 &command.description,
366 command.description_localizations.as_ref(),
367 );
368
369 for option in &command.options {
370 characters += option_characters(option);
371 }
372
373 characters
374}
375
376pub fn option_characters(option: &CommandOption) -> usize {
378 let mut characters = 0;
379
380 characters += longest_localization_characters(&option.name, option.name_localizations.as_ref());
381 characters += longest_localization_characters(
382 &option.description,
383 option.description_localizations.as_ref(),
384 );
385
386 match option.kind {
387 CommandOptionType::String => {
388 if let Some(choices) = option.choices.as_ref() {
389 for choice in choices {
390 if let CommandOptionChoiceValue::String(string_choice) = &choice.value {
391 characters += longest_localization_characters(
392 &choice.name,
393 choice.name_localizations.as_ref(),
394 ) + string_choice.len();
395 }
396 }
397 }
398 }
399 CommandOptionType::SubCommandGroup | CommandOptionType::SubCommand => {
400 if let Some(options) = option.options.as_ref() {
401 for option in options {
402 characters += option_characters(option);
403 }
404 }
405 }
406 _ => {}
407 }
408
409 characters
410}
411
412fn longest_localization_characters(
419 default: &str,
420 localizations: Option<&HashMap<String, String>>,
421) -> usize {
422 let mut characters = default.len();
423
424 if let Some(localizations) = localizations {
425 for localization in localizations.values() {
426 if localization.len() > characters {
427 characters = localization.len();
428 }
429 }
430 }
431
432 characters
433}
434
435pub fn description(value: impl AsRef<str>) -> Result<(), CommandValidationError> {
447 let len = value.as_ref().chars().count();
448
449 if (DESCRIPTION_LENGTH_MIN..=DESCRIPTION_LENGTH_MAX).contains(&len) {
451 Ok(())
452 } else {
453 Err(CommandValidationError {
454 kind: CommandValidationErrorType::DescriptionInvalid,
455 })
456 }
457}
458
459pub fn name(value: impl AsRef<str>) -> Result<(), CommandValidationError> {
475 let len = value.as_ref().chars().count();
476
477 if (NAME_LENGTH_MIN..=NAME_LENGTH_MAX).contains(&len) {
479 Ok(())
480 } else {
481 Err(CommandValidationError {
482 kind: CommandValidationErrorType::NameLengthInvalid,
483 })
484 }
485}
486
487pub fn chat_input_name(value: impl AsRef<str>) -> Result<(), CommandValidationError> {
506 self::name(&value)?;
507
508 self::name_characters(value)?;
509
510 Ok(())
511}
512
513pub fn option_name(value: impl AsRef<str>) -> Result<(), CommandValidationError> {
531 let len = value.as_ref().chars().count();
532
533 if !(OPTION_NAME_LENGTH_MIN..=OPTION_NAME_LENGTH_MAX).contains(&len) {
534 return Err(CommandValidationError {
535 kind: CommandValidationErrorType::NameLengthInvalid,
536 });
537 }
538
539 self::name_characters(value)?;
540
541 Ok(())
542}
543
544fn name_characters(value: impl AsRef<str>) -> Result<(), CommandValidationError> {
559 let chars = value.as_ref().chars();
560
561 for char in chars {
562 if !char.is_alphanumeric() && char != '_' && char != '-' {
563 return Err(CommandValidationError {
564 kind: CommandValidationErrorType::NameCharacterInvalid { character: char },
565 });
566 }
567
568 if char.to_lowercase().next() != Some(char) {
569 return Err(CommandValidationError {
570 kind: CommandValidationErrorType::NameCharacterInvalid { character: char },
571 });
572 }
573 }
574
575 Ok(())
576}
577
578pub fn choice_name(name: &str) -> Result<(), CommandValidationError> {
587 let len = name.chars().count();
588
589 if (OPTION_CHOICE_NAME_LENGTH_MIN..=OPTION_CHOICE_NAME_LENGTH_MAX).contains(&len) {
590 Ok(())
591 } else {
592 Err(CommandValidationError {
593 kind: CommandValidationErrorType::OptionChoiceNameLengthInvalid,
594 })
595 }
596}
597
598pub fn choice(choice: &CommandOptionChoice) -> Result<(), CommandValidationError> {
607 self::choice_name(&choice.name)?;
608
609 if let CommandOptionChoiceValue::String(value) = &choice.value {
610 let value_len = value.chars().count();
611
612 if !(OPTION_CHOICE_STRING_VALUE_LENGTH_MIN..=OPTION_CHOICE_STRING_VALUE_LENGTH_MAX)
613 .contains(&value_len)
614 {
615 return Err(CommandValidationError {
616 kind: CommandValidationErrorType::OptionChoiceStringValueLengthInvalid,
617 });
618 }
619 }
620
621 if let Some(name_localizations) = &choice.name_localizations {
622 name_localizations
623 .values()
624 .try_for_each(|name| self::choice_name(name))?;
625 }
626
627 Ok(())
628}
629
630pub fn option(option: &CommandOption) -> Result<(), CommandValidationError> {
644 let description_len = option.description.chars().count();
645 if !(OPTION_DESCRIPTION_LENGTH_MIN..=OPTION_DESCRIPTION_LENGTH_MAX).contains(&description_len) {
646 return Err(CommandValidationError {
647 kind: CommandValidationErrorType::OptionDescriptionInvalid,
648 });
649 }
650
651 if let Some(choices) = &option.choices {
652 choices.iter().try_for_each(self::choice)?;
653 }
654
655 self::option_name(&option.name)
656}
657
658pub fn options(options: &[CommandOption]) -> Result<(), CommandValidationError> {
671 if options.len() > OPTIONS_LIMIT {
673 return Err(CommandValidationError {
674 kind: CommandValidationErrorType::OptionsCountInvalid,
675 });
676 }
677
678 let mut names = HashSet::with_capacity(options.len());
679
680 for (option_index, option) in options.iter().enumerate() {
681 if !names.insert(&option.name) {
682 return Err(CommandValidationError::option_name_not_unique(option_index));
683 }
684 }
685
686 options
688 .iter()
689 .zip(options.iter().skip(1))
690 .enumerate()
691 .try_for_each(|(index, (first, second))| {
692 if !first.required.unwrap_or_default() && second.required.unwrap_or_default() {
693 Err(CommandValidationError::option_required_first(index))
694 } else {
695 Ok(())
696 }
697 })?;
698
699 options.iter().try_for_each(|option| {
701 if let Some(options) = &option.options {
702 self::options(options)
703 } else {
704 self::option(option)
705 }
706 })?;
707
708 Ok(())
709}
710
711pub const fn guild_permissions(count: usize) -> Result<(), CommandValidationError> {
723 if count <= GUILD_COMMAND_PERMISSION_LIMIT {
725 Ok(())
726 } else {
727 Err(CommandValidationError {
728 kind: CommandValidationErrorType::PermissionsCountInvalid,
729 })
730 }
731}
732
733#[cfg(test)]
734mod tests {
735 use super::*;
736 use twilight_model::id::Id;
737
738 #[test]
739 fn choice_name_limit() {
740 let valid_choice = CommandOptionChoice {
741 name: "a".repeat(100),
742 name_localizations: None,
743 value: CommandOptionChoiceValue::String("a".to_string()),
744 };
745
746 assert!(choice(&valid_choice).is_ok());
747
748 let invalid_choice = CommandOptionChoice {
749 name: "a".repeat(101),
750 name_localizations: None,
751 value: CommandOptionChoiceValue::String("b".to_string()),
752 };
753
754 assert!(choice(&invalid_choice).is_err());
755
756 let invalid_choice = CommandOptionChoice {
757 name: String::new(),
758 name_localizations: None,
759 value: CommandOptionChoiceValue::String("c".to_string()),
760 };
761
762 assert!(choice(&invalid_choice).is_err());
763 }
764
765 #[test]
766 fn choice_name_localizations() {
767 let mut name_localizations = HashMap::new();
768 name_localizations.insert("en-US".to_string(), "a".repeat(100));
769
770 let valid_choice = CommandOptionChoice {
771 name: "a".to_string(),
772 name_localizations: Some(name_localizations),
773 value: CommandOptionChoiceValue::String("a".to_string()),
774 };
775
776 assert!(choice(&valid_choice).is_ok());
777
778 let mut name_localizations = HashMap::new();
779 name_localizations.insert("en-US".to_string(), "a".repeat(101));
780
781 let invalid_choice = CommandOptionChoice {
782 name: "a".to_string(),
783 name_localizations: Some(name_localizations),
784 value: CommandOptionChoiceValue::String("b".to_string()),
785 };
786
787 assert!(choice(&invalid_choice).is_err());
788
789 let mut name_localizations = HashMap::new();
790 name_localizations.insert("en-US".to_string(), String::new());
791
792 let invalid_choice = CommandOptionChoice {
793 name: "a".to_string(),
794 name_localizations: Some(name_localizations),
795 value: CommandOptionChoiceValue::String("c".to_string()),
796 };
797
798 assert!(choice(&invalid_choice).is_err());
799
800 let mut name_localizations = HashMap::new();
801 name_localizations.insert("en-US".to_string(), String::from("a"));
802 name_localizations.insert("en-GB".to_string(), "a".repeat(101));
803 name_localizations.insert("es-ES".to_string(), "a".repeat(100));
804
805 let invalid_choice = CommandOptionChoice {
806 name: "a".to_string(),
807 name_localizations: Some(name_localizations),
808 value: CommandOptionChoiceValue::String("c".to_string()),
809 };
810
811 assert!(choice(&invalid_choice).is_err());
812 }
813
814 #[test]
815 fn choice_string_value() {
816 let valid_choice = CommandOptionChoice {
817 name: "a".to_string(),
818 name_localizations: None,
819 value: CommandOptionChoiceValue::String("a".to_string()),
820 };
821
822 assert!(choice(&valid_choice).is_ok());
823
824 let invalid_choice = CommandOptionChoice {
825 name: "b".to_string(),
826 name_localizations: None,
827 value: CommandOptionChoiceValue::String("b".repeat(101)),
828 };
829
830 assert!(choice(&invalid_choice).is_err());
831
832 let invalid_choice = CommandOptionChoice {
833 name: "c".to_string(),
834 name_localizations: None,
835 value: CommandOptionChoiceValue::String(String::new()),
836 };
837
838 assert!(choice(&invalid_choice).is_err());
839 }
840
841 #[test]
843 #[allow(deprecated)]
844 fn command_length() {
845 let valid_command = Command {
846 application_id: Some(Id::new(1)),
847 contexts: None,
848 default_member_permissions: None,
849 dm_permission: None,
850 description: "a".repeat(100),
851 description_localizations: Some(HashMap::from([(
852 "en-US".to_string(),
853 "a".repeat(100),
854 )])),
855 guild_id: Some(Id::new(2)),
856 id: Some(Id::new(3)),
857 integration_types: None,
858 kind: CommandType::ChatInput,
859 name: "b".repeat(32),
860 name_localizations: Some(HashMap::from([("en-US".to_string(), "b".repeat(32))])),
861 nsfw: None,
862 options: Vec::new(),
863 version: Id::new(4),
864 };
865
866 assert!(command(&valid_command).is_ok());
867
868 let invalid_message_command = Command {
869 description: "c".repeat(101),
870 name: "d".repeat(33),
871 ..valid_command.clone()
872 };
873 assert!(command(&invalid_message_command).is_err());
874
875 let valid_context_menu_command = Command {
876 description: String::new(),
877 kind: CommandType::Message,
878 ..valid_command.clone()
879 };
880
881 assert!(command(&valid_context_menu_command).is_ok());
882
883 let invalid_context_menu_command = Command {
884 description: "example description".to_string(),
885 kind: CommandType::Message,
886 ..valid_command
887 };
888
889 assert!(command(&invalid_context_menu_command).is_err());
890 }
891
892 #[test]
893 fn name_allowed_characters() {
894 assert!(name_characters("hello-command").is_ok()); assert!(name_characters("Hello").is_err()); assert!(name_characters("hello!").is_err()); assert!(name_characters("здрасти").is_ok()); assert!(name_characters("Здрасти").is_err()); assert!(name_characters("здрасти!").is_err()); assert!(name_characters("你好").is_ok()); assert!(name_characters("你好。").is_err()); }
905
906 #[test]
907 fn guild_permissions_count() {
908 assert!(guild_permissions(0).is_ok());
909 assert!(guild_permissions(1).is_ok());
910 assert!(guild_permissions(10).is_ok());
911
912 assert!(guild_permissions(11).is_err());
913 }
914
915 #[test]
916 #[allow(deprecated)]
917 fn command_combined_limit() {
918 let mut command = Command {
919 application_id: Some(Id::new(1)),
920 default_member_permissions: None,
921 dm_permission: None,
922 description: "a".repeat(10),
923 description_localizations: Some(HashMap::from([(
924 "en-US".to_string(),
925 "a".repeat(100),
926 )])),
927 guild_id: Some(Id::new(2)),
928 id: Some(Id::new(3)),
929 kind: CommandType::ChatInput,
930 name: "b".repeat(10),
931 name_localizations: Some(HashMap::from([("en-US".to_string(), "b".repeat(32))])),
932 nsfw: None,
933 options: Vec::from([CommandOption {
934 autocomplete: None,
935 channel_types: None,
936 choices: None,
937 description: "a".repeat(10),
938 description_localizations: Some(HashMap::from([(
939 "en-US".to_string(),
940 "a".repeat(100),
941 )])),
942 kind: CommandOptionType::SubCommandGroup,
943 max_length: None,
944 max_value: None,
945 min_length: None,
946 min_value: None,
947 name: "b".repeat(10),
948 name_localizations: Some(HashMap::from([("en-US".to_string(), "b".repeat(32))])),
949 options: Some(Vec::from([CommandOption {
950 autocomplete: None,
951 channel_types: None,
952 choices: None,
953 description: "a".repeat(100),
954 description_localizations: Some(HashMap::from([(
955 "en-US".to_string(),
956 "a".repeat(10),
957 )])),
958 kind: CommandOptionType::SubCommand,
959 max_length: None,
960 max_value: None,
961 min_length: None,
962 min_value: None,
963 name: "b".repeat(32),
964 name_localizations: Some(HashMap::from([(
965 "en-US".to_string(),
966 "b".repeat(10),
967 )])),
968 options: Some(Vec::from([CommandOption {
969 autocomplete: Some(false),
970 channel_types: None,
971 choices: Some(Vec::from([CommandOptionChoice {
972 name: "b".repeat(32),
973 name_localizations: Some(HashMap::from([(
974 "en-US".to_string(),
975 "b".repeat(10),
976 )])),
977 value: CommandOptionChoiceValue::String("c".repeat(100)),
978 }])),
979 description: "a".repeat(100),
980 description_localizations: Some(HashMap::from([(
981 "en-US".to_string(),
982 "a".repeat(10),
983 )])),
984 kind: CommandOptionType::String,
985 max_length: None,
986 max_value: None,
987 min_length: None,
988 min_value: None,
989 name: "b".repeat(32),
990 name_localizations: Some(HashMap::from([(
991 "en-US".to_string(),
992 "b".repeat(10),
993 )])),
994 options: None,
995 required: Some(false),
996 }])),
997 required: None,
998 }])),
999 required: None,
1000 }]),
1001 version: Id::new(4),
1002 contexts: None,
1003 integration_types: None,
1004 };
1005
1006 assert_eq!(command_characters(&command), 660);
1007 assert!(super::command(&command).is_ok());
1008
1009 command.description = "a".repeat(3441);
1010 assert_eq!(command_characters(&command), 4001);
1011
1012 assert!(matches!(
1013 super::command(&command).unwrap_err().kind(),
1014 CommandValidationErrorType::CommandTooLarge { characters: 4001 }
1015 ));
1016 }
1017
1018 #[test]
1020 fn option_name_uniqueness() {
1021 let option = CommandOption {
1022 autocomplete: None,
1023 channel_types: None,
1024 choices: None,
1025 description: "a description".to_owned(),
1026 description_localizations: None,
1027 kind: CommandOptionType::String,
1028 max_length: None,
1029 max_value: None,
1030 min_length: None,
1031 min_value: None,
1032 name: "name".to_owned(),
1033 name_localizations: None,
1034 options: None,
1035 required: None,
1036 };
1037 let mut options = Vec::from([option.clone()]);
1038 assert!(super::options(&options).is_ok());
1039 options.push(option);
1040 assert!(matches!(super::options(&options).unwrap_err().kind(),
1041 CommandValidationErrorType::OptionNameNotUnique { option_index } if *option_index == 1));
1042 }
1043
1044 #[test]
1046 fn option_description_length() {
1047 let base = CommandOption {
1048 autocomplete: None,
1049 channel_types: None,
1050 choices: None,
1051 description: String::new(),
1052 description_localizations: None,
1053 kind: CommandOptionType::Boolean,
1054 max_length: None,
1055 max_value: None,
1056 min_length: None,
1057 min_value: None,
1058 name: "testcommand".to_string(),
1059 name_localizations: None,
1060 options: None,
1061 required: None,
1062 };
1063 let toolong = CommandOption {
1064 description: "e".repeat(OPTION_DESCRIPTION_LENGTH_MAX + 1),
1065 ..base.clone()
1066 };
1067 let tooshort = CommandOption {
1068 description: "e".repeat(OPTION_DESCRIPTION_LENGTH_MIN - 1),
1069 ..base.clone()
1070 };
1071 let maxlen = CommandOption {
1072 description: "e".repeat(OPTION_DESCRIPTION_LENGTH_MAX),
1073 ..base.clone()
1074 };
1075 #[allow(clippy::repeat_once)]
1077 let minlen = CommandOption {
1078 description: "e".repeat(OPTION_DESCRIPTION_LENGTH_MIN),
1079 ..base
1080 };
1081 assert!(option(&toolong).is_err());
1082 assert!(option(&tooshort).is_err());
1083 assert!(option(&maxlen).is_ok());
1084 assert!(option(&minlen).is_ok());
1085 }
1086}