Refactor validation to use full predicates and pass false to nested toJSON

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-11-26 11:30:17 +00:00
parent e3252b0564
commit 3f3341cb6c
3 changed files with 65 additions and 14 deletions

View File

@@ -117,6 +117,48 @@ export const stringOptionPredicate = basicOptionPredicate
}) })
.and(autocompleteOrStringChoicesMixinOptionPredicate); .and(autocompleteOrStringChoicesMixinOptionPredicate);
// Full predicate for all basic option types (used for nested validation in subcommands)
// This validates all possible properties that any basic option might have
const fullBasicOptionPredicate = sharedNameAndDescriptionPredicate
.extend({
type: basicOptionTypesPredicate,
required: z.boolean().optional(),
// String option properties
max_length: z.number().min(0).max(6_000).optional(),
min_length: z.number().min(1).max(6_000).optional(),
// Number/Integer option properties
max_value: z.union([z.int(), z.float32()]).optional(),
min_value: z.union([z.int(), z.float32()]).optional(),
// Channel option properties
channel_types: z.literal(ApplicationCommandOptionAllowedChannelTypes).array().optional(),
// Choice/Autocomplete properties (shared by String, Number, Integer)
autocomplete: z.boolean().optional(),
choices: z
.union([choiceStringPredicate.array().max(25), choiceNumberPredicate.array().max(25)])
.optional(),
})
.refine(
(data) => {
// Validate autocomplete and choices are mutually exclusive
if (data.autocomplete === true && data.choices && data.choices.length > 0) {
return false;
}
return true;
},
{ message: 'Autocomplete and choices are mutually exclusive' },
)
.refine(
(data) => {
// Validate integer min/max are integers
if (data.type === ApplicationCommandOptionType.Integer) {
if (data.max_value !== undefined && !Number.isInteger(data.max_value)) return false;
if (data.min_value !== undefined && !Number.isInteger(data.min_value)) return false;
}
return true;
},
{ message: 'Integer options must have integer min/max values' },
);
const baseChatInputCommandPredicate = sharedNameAndDescriptionPredicate.extend({ const baseChatInputCommandPredicate = sharedNameAndDescriptionPredicate.extend({
contexts: z.array(z.enum(InteractionContextType)).optional(), contexts: z.array(z.enum(InteractionContextType)).optional(),
default_member_permissions: memberPermissionsPredicate.optional(), default_member_permissions: memberPermissionsPredicate.optional(),
@@ -124,26 +166,35 @@ const baseChatInputCommandPredicate = sharedNameAndDescriptionPredicate.extend({
nsfw: z.boolean().optional(), nsfw: z.boolean().optional(),
}); });
// Because you can only add options via builders, there's no need to validate whole objects here otherwise // Full predicate for subcommand (used for nested validation in subcommand groups)
const chatInputCommandOptionsPredicate = z.union([ const fullSubcommandPredicate = sharedNameAndDescriptionPredicate.extend({
z.object({ type: basicOptionTypesPredicate }).array(), type: z.literal(ApplicationCommandOptionType.Subcommand),
z.object({ type: z.literal(ApplicationCommandOptionType.Subcommand) }).array(), options: z.array(fullBasicOptionPredicate).max(25).optional(),
z.object({ type: z.literal(ApplicationCommandOptionType.SubcommandGroup) }).array(), });
// Full predicate for subcommand group (used for nested validation in chat input commands)
const fullSubcommandGroupPredicate = sharedNameAndDescriptionPredicate.extend({
type: z.literal(ApplicationCommandOptionType.SubcommandGroup),
options: z.array(fullSubcommandPredicate).min(1).max(25),
});
// Full options predicate for chat input commands (validates complete nested structure)
const fullChatInputCommandOptionsPredicate = z.union([
z.array(fullBasicOptionPredicate),
z.array(fullSubcommandPredicate),
z.array(fullSubcommandGroupPredicate),
]); ]);
export const chatInputCommandPredicate = baseChatInputCommandPredicate.extend({ export const chatInputCommandPredicate = baseChatInputCommandPredicate.extend({
options: chatInputCommandOptionsPredicate.optional(), options: fullChatInputCommandOptionsPredicate.optional(),
}); });
export const chatInputCommandSubcommandGroupPredicate = sharedNameAndDescriptionPredicate.extend({ export const chatInputCommandSubcommandGroupPredicate = sharedNameAndDescriptionPredicate.extend({
type: z.literal(ApplicationCommandOptionType.SubcommandGroup), type: z.literal(ApplicationCommandOptionType.SubcommandGroup),
options: z options: z.array(fullSubcommandPredicate).min(1).max(25),
.array(z.object({ type: z.literal(ApplicationCommandOptionType.Subcommand) }))
.min(1)
.max(25),
}); });
export const chatInputCommandSubcommandPredicate = sharedNameAndDescriptionPredicate.extend({ export const chatInputCommandSubcommandPredicate = sharedNameAndDescriptionPredicate.extend({
type: z.literal(ApplicationCommandOptionType.Subcommand), type: z.literal(ApplicationCommandOptionType.Subcommand),
options: z.array(z.object({ type: basicOptionTypesPredicate })).max(25), options: z.array(fullBasicOptionPredicate).max(25).optional(),
}); });

View File

@@ -30,7 +30,7 @@ export class ChatInputCommandBuilder extends Mixin(
const data: RESTPostAPIChatInputApplicationCommandsJSONBody = { const data: RESTPostAPIChatInputApplicationCommandsJSONBody = {
...structuredClone(rest as Omit<RESTPostAPIChatInputApplicationCommandsJSONBody, 'options'>), ...structuredClone(rest as Omit<RESTPostAPIChatInputApplicationCommandsJSONBody, 'options'>),
type: ApplicationCommandType.ChatInput, type: ApplicationCommandType.ChatInput,
options: options?.map((option) => option.toJSON(validationOverride)), options: options?.map((option) => option.toJSON(false)),
}; };
validate(chatInputCommandPredicate, data, validationOverride); validate(chatInputCommandPredicate, data, validationOverride);

View File

@@ -74,7 +74,7 @@ export class ChatInputCommandSubcommandGroupBuilder
const data = { const data = {
...(structuredClone(rest) as Omit<APIApplicationCommandSubcommandGroupOption, 'type'>), ...(structuredClone(rest) as Omit<APIApplicationCommandSubcommandGroupOption, 'type'>),
type: ApplicationCommandOptionType.SubcommandGroup as const, type: ApplicationCommandOptionType.SubcommandGroup as const,
options: options?.map((option) => option.toJSON(validationOverride)) ?? [], options: options?.map((option) => option.toJSON(false)) ?? [],
}; };
validate(chatInputCommandSubcommandGroupPredicate, data, validationOverride); validate(chatInputCommandSubcommandGroupPredicate, data, validationOverride);
@@ -107,7 +107,7 @@ export class ChatInputCommandSubcommandBuilder
const data = { const data = {
...(structuredClone(rest) as Omit<APIApplicationCommandSubcommandOption, 'type'>), ...(structuredClone(rest) as Omit<APIApplicationCommandSubcommandOption, 'type'>),
type: ApplicationCommandOptionType.Subcommand as const, type: ApplicationCommandOptionType.Subcommand as const,
options: options?.map((option) => option.toJSON(validationOverride)) ?? [], options: options?.map((option) => option.toJSON(false)) ?? [],
}; };
validate(chatInputCommandSubcommandPredicate, data, validationOverride); validate(chatInputCommandSubcommandPredicate, data, validationOverride);