///|
/// The limits Discord documents for an application command declaration
/// ():
///
/// - command and option names are 1 to 32 characters, and CHAT_INPUT command
/// and option names follow the naming rule (lowercase where a lowercase
/// variant exists, no spaces or punctuation other than `-` and `_`);
/// - CHAT_INPUT descriptions and option descriptions are 1 to 100 characters;
/// - localizations follow the rules of the field they localize;
/// - each options array holds at most 25 entries with unique names;
/// - an option has at most 25 choices, whose names are 1 to 100 characters and
/// whose string values are at most 100 characters;
/// - `min_length` is 0 to 6000 and `max_length` is 1 to 6000;
/// - a CHAT_INPUT command's names, descriptions, and string choice values
/// total at most 8000 characters, counting only the longest localization of
/// each field.
///
/// The naming rule is checked for ASCII characters only: a non-ASCII
/// character is never reported, so every reported name is certainly invalid.
pub fn command_limit_violations(
typ~ : ApplicationCommandType,
name~ : String,
name_localizations~ : Map[String, String]?,
description~ : String,
description_localizations~ : Map[String, String]?,
options~ : Array[CommandOption],
) -> Array[LimitViolation] {
let checker = LimitChecker::new()
let chat_input = typ == ChatInput
checker.command_name("name", name, chat_input~)
if name_localizations is Some(localized) {
for locale, value in localized {
checker.command_name("name_localizations.\{locale}", value, chat_input~)
}
}
if chat_input {
checker.text("description", Some(description), min=1, max=100)
if description_localizations is Some(localized) {
for locale, value in localized {
checker.text(
"description_localizations.\{locale}",
Some(value),
min=1,
max=100,
)
}
}
checker.command_options("options", options)
let total = longest_localized(name, name_localizations) +
longest_localized(description, description_localizations) +
options.fold(init=0, (sum, option) => sum + option_text_length(option))
if total > 8000 {
checker.report(
"name",
"\{total} characters of names, descriptions, and choice values (at most 8000)",
)
}
}
checker.violations
}
///|
/// Report a command or option name outside 1 to 32 characters, or one that breaks
/// the CHAT_INPUT naming rule in its ASCII characters.
fn LimitChecker::command_name(
self : LimitChecker,
path : String,
name : String,
chat_input~ : Bool,
) -> Unit {
self.text(path, Some(name), min=1, max=32)
guard chat_input else { return }
for char in name {
if char.is_whitespace() ||
char.is_ascii_uppercase() ||
(char.to_int() < 0x80 && !(char is ('a'..<='z' | '0'..<='9' | '-' | '_'))) {
self.report(
path,
"\{repr_char(char)} is not allowed (lowercase letters, digits, `-`, and `_`)",
)
return
}
}
}
///|
fn repr_char(char : Char) -> String {
if char == ' ' {
"a space"
} else {
"`\{char}`"
}
}
///|
fn nullable_map(map : Nullable[Map[String, String]]?) -> Map[String, String]? {
match map {
Some(Value(map)) => Some(map)
Some(Null) | None => None
}
}
///|
fn LimitChecker::command_options(
self : LimitChecker,
path : String,
options : Array[CommandOption],
) -> Unit {
self.count(path, Some(options.length()), min=0, max=25)
let seen : Set[String] = Set([])
for index, option in options {
let option_path = "\{path}[\{index}]"
if seen.contains(option.name) {
self.report(
"\{option_path}.name",
"`\{option.name}` repeats an earlier option name",
)
}
seen.add(option.name)
self.command_name("\{option_path}.name", option.name, chat_input=true)
if nullable_map(option.name_localizations) is Some(localized) {
for locale, value in localized {
self.command_name(
"\{option_path}.name_localizations.\{locale}",
value,
chat_input=true,
)
}
}
self.text(
"\{option_path}.description",
Some(option.description),
min=1,
max=100,
)
if nullable_map(option.description_localizations) is Some(localized) {
for locale, value in localized {
self.text(
"\{option_path}.description_localizations.\{locale}",
Some(value),
min=1,
max=100,
)
}
}
if option.choices is Some(choices) {
self.count(
"\{option_path}.choices",
Some(choices.length()),
min=0,
max=25,
)
for choice_index, choice in choices {
let choice_path = "\{option_path}.choices[\{choice_index}]"
self.text("\{choice_path}.name", Some(choice.name), min=1, max=100)
if nullable_map(choice.name_localizations) is Some(localized) {
for locale, value in localized {
self.text(
"\{choice_path}.name_localizations.\{locale}",
Some(value),
min=1,
max=100,
)
}
}
if choice.value is String(value) {
self.text("\{choice_path}.value", Some(value), max=100)
}
}
}
self.count("\{option_path}.min_length", option.min_length, min=0, max=6000)
self.count("\{option_path}.max_length", option.max_length, min=1, max=6000)
if option.options is Some(children) {
self.command_options("\{option_path}.options", children)
}
}
}
///|
/// A field's contribution to the 8000-unit command total: the longest of
/// the default value and its localizations.
fn longest_localized(value : String, localized : Map[String, String]?) -> Int32 {
let mut longest = text_length(value)
if localized is Some(localized) {
for _, text in localized {
longest = longest.max(text_length(text))
}
}
longest
}
///|
fn option_text_length(option : CommandOption) -> Int32 {
let mut total = longest_localized(
option.name,
nullable_map(option.name_localizations),
) +
longest_localized(
option.description,
nullable_map(option.description_localizations),
)
if option.choices is Some(choices) {
for choice in choices {
total += longest_localized(
choice.name,
nullable_map(choice.name_localizations),
)
if choice.value is String(value) {
total += text_length(value)
}
}
}
if option.options is Some(children) {
for child in children {
total += option_text_length(child)
}
}
total
}