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.chars().count();
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.chars().count();
423
424 if let Some(localizations) = localizations {
425 for localization in localizations.values() {
426 let len = localization.chars().count();
427 if len > characters {
428 characters = len;
429 }
430 }
431 }
432
433 characters
434}
435
436pub fn description(value: impl AsRef<str>) -> Result<(), CommandValidationError> {
448 let len = value.as_ref().chars().count();
449
450 if (DESCRIPTION_LENGTH_MIN..=DESCRIPTION_LENGTH_MAX).contains(&len) {
452 Ok(())
453 } else {
454 Err(CommandValidationError {
455 kind: CommandValidationErrorType::DescriptionInvalid,
456 })
457 }
458}
459
460pub fn name(value: impl AsRef<str>) -> Result<(), CommandValidationError> {
476 let len = value.as_ref().chars().count();
477
478 if (NAME_LENGTH_MIN..=NAME_LENGTH_MAX).contains(&len) {
480 Ok(())
481 } else {
482 Err(CommandValidationError {
483 kind: CommandValidationErrorType::NameLengthInvalid,
484 })
485 }
486}
487
488pub fn chat_input_name(value: impl AsRef<str>) -> Result<(), CommandValidationError> {
507 self::name(&value)?;
508
509 self::name_characters(value)?;
510
511 Ok(())
512}
513
514pub fn option_name(value: impl AsRef<str>) -> Result<(), CommandValidationError> {
532 let len = value.as_ref().chars().count();
533
534 if !(OPTION_NAME_LENGTH_MIN..=OPTION_NAME_LENGTH_MAX).contains(&len) {
535 return Err(CommandValidationError {
536 kind: CommandValidationErrorType::NameLengthInvalid,
537 });
538 }
539
540 self::name_characters(value)?;
541
542 Ok(())
543}
544
545fn name_characters(value: impl AsRef<str>) -> Result<(), CommandValidationError> {
560 let chars = value.as_ref().chars();
561
562 for char in chars {
563 if !char.is_alphanumeric() && char != '_' && char != '-' {
564 return Err(CommandValidationError {
565 kind: CommandValidationErrorType::NameCharacterInvalid { character: char },
566 });
567 }
568
569 if char.to_lowercase().next() != Some(char) {
570 return Err(CommandValidationError {
571 kind: CommandValidationErrorType::NameCharacterInvalid { character: char },
572 });
573 }
574 }
575
576 Ok(())
577}
578
579pub fn choice_name(name: &str) -> Result<(), CommandValidationError> {
588 let len = name.chars().count();
589
590 if (OPTION_CHOICE_NAME_LENGTH_MIN..=OPTION_CHOICE_NAME_LENGTH_MAX).contains(&len) {
591 Ok(())
592 } else {
593 Err(CommandValidationError {
594 kind: CommandValidationErrorType::OptionChoiceNameLengthInvalid,
595 })
596 }
597}
598
599pub fn choice(choice: &CommandOptionChoice) -> Result<(), CommandValidationError> {
608 self::choice_name(&choice.name)?;
609
610 if let CommandOptionChoiceValue::String(value) = &choice.value {
611 let value_len = value.chars().count();
612
613 if !(OPTION_CHOICE_STRING_VALUE_LENGTH_MIN..=OPTION_CHOICE_STRING_VALUE_LENGTH_MAX)
614 .contains(&value_len)
615 {
616 return Err(CommandValidationError {
617 kind: CommandValidationErrorType::OptionChoiceStringValueLengthInvalid,
618 });
619 }
620 }
621
622 if let Some(name_localizations) = &choice.name_localizations {
623 name_localizations
624 .values()
625 .try_for_each(|name| self::choice_name(name))?;
626 }
627
628 Ok(())
629}
630
631pub fn option(option: &CommandOption) -> Result<(), CommandValidationError> {
645 let description_len = option.description.chars().count();
646 if !(OPTION_DESCRIPTION_LENGTH_MIN..=OPTION_DESCRIPTION_LENGTH_MAX).contains(&description_len) {
647 return Err(CommandValidationError {
648 kind: CommandValidationErrorType::OptionDescriptionInvalid,
649 });
650 }
651
652 if let Some(choices) = &option.choices {
653 choices.iter().try_for_each(self::choice)?;
654 }
655
656 self::option_name(&option.name)
657}
658
659pub fn options(options: &[CommandOption]) -> Result<(), CommandValidationError> {
672 if options.len() > OPTIONS_LIMIT {
674 return Err(CommandValidationError {
675 kind: CommandValidationErrorType::OptionsCountInvalid,
676 });
677 }
678
679 let mut names = HashSet::with_capacity(options.len());
680
681 for (option_index, option) in options.iter().enumerate() {
682 if !names.insert(&option.name) {
683 return Err(CommandValidationError::option_name_not_unique(option_index));
684 }
685 }
686
687 options
689 .iter()
690 .zip(options.iter().skip(1))
691 .enumerate()
692 .try_for_each(|(index, (first, second))| {
693 if !first.required.unwrap_or_default() && second.required.unwrap_or_default() {
694 Err(CommandValidationError::option_required_first(index))
695 } else {
696 Ok(())
697 }
698 })?;
699
700 options.iter().try_for_each(|option| {
702 if let Some(options) = &option.options {
703 self::options(options)
704 } else {
705 self::option(option)
706 }
707 })?;
708
709 Ok(())
710}
711
712pub const fn guild_permissions(count: usize) -> Result<(), CommandValidationError> {
724 if count <= GUILD_COMMAND_PERMISSION_LIMIT {
726 Ok(())
727 } else {
728 Err(CommandValidationError {
729 kind: CommandValidationErrorType::PermissionsCountInvalid,
730 })
731 }
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737 use twilight_model::id::Id;
738
739 #[test]
740 fn choice_name_limit() {
741 let valid_choice = CommandOptionChoice {
742 name: "a".repeat(100),
743 name_localizations: None,
744 value: CommandOptionChoiceValue::String("a".to_string()),
745 };
746
747 assert!(choice(&valid_choice).is_ok());
748
749 let invalid_choice = CommandOptionChoice {
750 name: "a".repeat(101),
751 name_localizations: None,
752 value: CommandOptionChoiceValue::String("b".to_string()),
753 };
754
755 assert!(choice(&invalid_choice).is_err());
756
757 let invalid_choice = CommandOptionChoice {
758 name: String::new(),
759 name_localizations: None,
760 value: CommandOptionChoiceValue::String("c".to_string()),
761 };
762
763 assert!(choice(&invalid_choice).is_err());
764 }
765
766 #[test]
767 fn choice_name_localizations() {
768 let mut name_localizations = HashMap::new();
769 name_localizations.insert("en-US".to_string(), "a".repeat(100));
770
771 let valid_choice = CommandOptionChoice {
772 name: "a".to_string(),
773 name_localizations: Some(name_localizations),
774 value: CommandOptionChoiceValue::String("a".to_string()),
775 };
776
777 assert!(choice(&valid_choice).is_ok());
778
779 let mut name_localizations = HashMap::new();
780 name_localizations.insert("en-US".to_string(), "a".repeat(101));
781
782 let invalid_choice = CommandOptionChoice {
783 name: "a".to_string(),
784 name_localizations: Some(name_localizations),
785 value: CommandOptionChoiceValue::String("b".to_string()),
786 };
787
788 assert!(choice(&invalid_choice).is_err());
789
790 let mut name_localizations = HashMap::new();
791 name_localizations.insert("en-US".to_string(), String::new());
792
793 let invalid_choice = CommandOptionChoice {
794 name: "a".to_string(),
795 name_localizations: Some(name_localizations),
796 value: CommandOptionChoiceValue::String("c".to_string()),
797 };
798
799 assert!(choice(&invalid_choice).is_err());
800
801 let mut name_localizations = HashMap::new();
802 name_localizations.insert("en-US".to_string(), String::from("a"));
803 name_localizations.insert("en-GB".to_string(), "a".repeat(101));
804 name_localizations.insert("es-ES".to_string(), "a".repeat(100));
805
806 let invalid_choice = CommandOptionChoice {
807 name: "a".to_string(),
808 name_localizations: Some(name_localizations),
809 value: CommandOptionChoiceValue::String("c".to_string()),
810 };
811
812 assert!(choice(&invalid_choice).is_err());
813 }
814
815 #[test]
816 fn choice_string_value() {
817 let valid_choice = CommandOptionChoice {
818 name: "a".to_string(),
819 name_localizations: None,
820 value: CommandOptionChoiceValue::String("a".to_string()),
821 };
822
823 assert!(choice(&valid_choice).is_ok());
824
825 let invalid_choice = CommandOptionChoice {
826 name: "b".to_string(),
827 name_localizations: None,
828 value: CommandOptionChoiceValue::String("b".repeat(101)),
829 };
830
831 assert!(choice(&invalid_choice).is_err());
832
833 let invalid_choice = CommandOptionChoice {
834 name: "c".to_string(),
835 name_localizations: None,
836 value: CommandOptionChoiceValue::String(String::new()),
837 };
838
839 assert!(choice(&invalid_choice).is_err());
840 }
841
842 #[test]
844 #[allow(deprecated)]
845 fn command_length() {
846 let valid_command = Command {
847 application_id: Some(Id::new(1)),
848 contexts: None,
849 default_member_permissions: None,
850 dm_permission: None,
851 description: "a".repeat(100),
852 description_localizations: Some(HashMap::from([(
853 "en-US".to_string(),
854 "a".repeat(100),
855 )])),
856 guild_id: Some(Id::new(2)),
857 id: Some(Id::new(3)),
858 integration_types: None,
859 kind: CommandType::ChatInput,
860 name: "b".repeat(32),
861 name_localizations: Some(HashMap::from([("en-US".to_string(), "b".repeat(32))])),
862 nsfw: None,
863 options: Vec::new(),
864 version: Id::new(4),
865 };
866
867 assert!(command(&valid_command).is_ok());
868
869 let invalid_message_command = Command {
870 description: "c".repeat(101),
871 name: "d".repeat(33),
872 ..valid_command.clone()
873 };
874 assert!(command(&invalid_message_command).is_err());
875
876 let valid_context_menu_command = Command {
877 description: String::new(),
878 kind: CommandType::Message,
879 ..valid_command.clone()
880 };
881
882 assert!(command(&valid_context_menu_command).is_ok());
883
884 let invalid_context_menu_command = Command {
885 description: "example description".to_string(),
886 kind: CommandType::Message,
887 ..valid_command
888 };
889
890 assert!(command(&invalid_context_menu_command).is_err());
891 }
892
893 #[test]
894 fn name_allowed_characters() {
895 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()); }
906
907 #[test]
908 fn guild_permissions_count() {
909 assert!(guild_permissions(0).is_ok());
910 assert!(guild_permissions(1).is_ok());
911 assert!(guild_permissions(10).is_ok());
912
913 assert!(guild_permissions(11).is_err());
914 }
915
916 #[test]
917 #[allow(deprecated)]
918 fn command_combined_limit() {
919 let mut command = Command {
920 application_id: Some(Id::new(1)),
921 default_member_permissions: None,
922 dm_permission: None,
923 description: "a".repeat(10),
924 description_localizations: Some(HashMap::from([(
925 "en-US".to_string(),
926 "a".repeat(100),
927 )])),
928 guild_id: Some(Id::new(2)),
929 id: Some(Id::new(3)),
930 kind: CommandType::ChatInput,
931 name: "b".repeat(10),
932 name_localizations: Some(HashMap::from([("en-US".to_string(), "b".repeat(32))])),
933 nsfw: None,
934 options: Vec::from([CommandOption {
935 autocomplete: None,
936 channel_types: None,
937 choices: None,
938 description: "a".repeat(10),
939 description_localizations: Some(HashMap::from([(
940 "en-US".to_string(),
941 "a".repeat(100),
942 )])),
943 kind: CommandOptionType::SubCommandGroup,
944 max_length: None,
945 max_value: None,
946 min_length: None,
947 min_value: None,
948 name: "b".repeat(10),
949 name_localizations: Some(HashMap::from([("en-US".to_string(), "b".repeat(32))])),
950 options: Some(Vec::from([CommandOption {
951 autocomplete: None,
952 channel_types: None,
953 choices: None,
954 description: "a".repeat(100),
955 description_localizations: Some(HashMap::from([(
956 "en-US".to_string(),
957 "a".repeat(10),
958 )])),
959 kind: CommandOptionType::SubCommand,
960 max_length: None,
961 max_value: None,
962 min_length: None,
963 min_value: None,
964 name: "b".repeat(32),
965 name_localizations: Some(HashMap::from([(
966 "en-US".to_string(),
967 "b".repeat(10),
968 )])),
969 options: Some(Vec::from([CommandOption {
970 autocomplete: Some(false),
971 channel_types: None,
972 choices: Some(Vec::from([CommandOptionChoice {
973 name: "b".repeat(32),
974 name_localizations: Some(HashMap::from([(
975 "en-US".to_string(),
976 "b".repeat(10),
977 )])),
978 value: CommandOptionChoiceValue::String("c".repeat(100)),
979 }])),
980 description: "a".repeat(100),
981 description_localizations: Some(HashMap::from([(
982 "en-US".to_string(),
983 "a".repeat(10),
984 )])),
985 kind: CommandOptionType::String,
986 max_length: None,
987 max_value: None,
988 min_length: None,
989 min_value: None,
990 name: "b".repeat(32),
991 name_localizations: Some(HashMap::from([(
992 "en-US".to_string(),
993 "b".repeat(10),
994 )])),
995 options: None,
996 required: Some(false),
997 }])),
998 required: None,
999 }])),
1000 required: None,
1001 }]),
1002 version: Id::new(4),
1003 contexts: None,
1004 integration_types: None,
1005 };
1006
1007 assert_eq!(command_characters(&command), 660);
1008 assert!(super::command(&command).is_ok());
1009
1010 command.description = "a".repeat(3441);
1011 assert_eq!(command_characters(&command), 4001);
1012
1013 assert!(matches!(
1014 super::command(&command).unwrap_err().kind(),
1015 CommandValidationErrorType::CommandTooLarge { characters: 4001 }
1016 ));
1017 }
1018
1019 #[test]
1021 fn option_name_uniqueness() {
1022 let option = CommandOption {
1023 autocomplete: None,
1024 channel_types: None,
1025 choices: None,
1026 description: "a description".to_owned(),
1027 description_localizations: None,
1028 kind: CommandOptionType::String,
1029 max_length: None,
1030 max_value: None,
1031 min_length: None,
1032 min_value: None,
1033 name: "name".to_owned(),
1034 name_localizations: None,
1035 options: None,
1036 required: None,
1037 };
1038 let mut options = Vec::from([option.clone()]);
1039 assert!(super::options(&options).is_ok());
1040 options.push(option);
1041 assert!(matches!(super::options(&options).unwrap_err().kind(),
1042 CommandValidationErrorType::OptionNameNotUnique { option_index } if *option_index == 1));
1043 }
1044
1045 #[test]
1047 fn option_description_length() {
1048 let base = CommandOption {
1049 autocomplete: None,
1050 channel_types: None,
1051 choices: None,
1052 description: String::new(),
1053 description_localizations: None,
1054 kind: CommandOptionType::Boolean,
1055 max_length: None,
1056 max_value: None,
1057 min_length: None,
1058 min_value: None,
1059 name: "testcommand".to_string(),
1060 name_localizations: None,
1061 options: None,
1062 required: None,
1063 };
1064 let toolong = CommandOption {
1065 description: "e".repeat(OPTION_DESCRIPTION_LENGTH_MAX + 1),
1066 ..base.clone()
1067 };
1068 let tooshort = CommandOption {
1069 description: "e".repeat(OPTION_DESCRIPTION_LENGTH_MIN - 1),
1070 ..base.clone()
1071 };
1072 let maxlen = CommandOption {
1073 description: "e".repeat(OPTION_DESCRIPTION_LENGTH_MAX),
1074 ..base.clone()
1075 };
1076 #[allow(clippy::repeat_once)]
1078 let minlen = CommandOption {
1079 description: "e".repeat(OPTION_DESCRIPTION_LENGTH_MIN),
1080 ..base
1081 };
1082 assert!(option(&toolong).is_err());
1083 assert!(option(&tooshort).is_err());
1084 assert!(option(&maxlen).is_ok());
1085 assert!(option(&minlen).is_ok());
1086 }
1087}