Skip to content

Commit f4fcc4f

Browse files
committed
refactor: move full validation to ChatInputCommandBuilder
1 parent 02fc101 commit f4fcc4f

File tree

9 files changed

+119
-50
lines changed

9 files changed

+119
-50
lines changed

packages/builders/__tests__/interactions/ChatInputCommands/ChatInputCommands.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,17 @@ describe('ChatInput Commands', () => {
376376
});
377377
});
378378

379+
describe('Subcommand builder and subcommand group builder', () => {
380+
test('GIVEN both types THEN does not throw error', () => {
381+
expect(() =>
382+
getNamedBuilder()
383+
.addSubcommands(getSubcommand())
384+
.addSubcommandGroups(getSubcommandGroup().addSubcommands(getSubcommand()))
385+
.toJSON(),
386+
).not.toThrowError();
387+
});
388+
});
389+
379390
describe('ChatInput command localizations', () => {
380391
const expectedSingleLocale = { [Locale.EnglishUS]: 'foobar' };
381392
const expectedMultipleLocales = {

packages/builders/src/interactions/commands/chatInput/Assertions.ts

Lines changed: 76 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -47,38 +47,55 @@ const choiceBasePredicate = z.object({
4747
name: choiceValueStringPredicate,
4848
name_localizations: localeMapPredicate.optional(),
4949
});
50-
const choiceStringPredicate = choiceBasePredicate.extend({
50+
const choiceStringPredicate = z.object({
51+
...choiceBasePredicate.shape,
5152
value: choiceValueStringPredicate,
5253
});
53-
const choiceNumberPredicate = choiceBasePredicate.extend({
54+
const choiceNumberPredicate = z.object({
55+
...choiceBasePredicate.shape,
5456
value: choiceValueNumberPredicate,
5557
});
5658

5759
const choiceBaseMixinPredicate = z.object({
5860
autocomplete: z.literal(false).optional(),
5961
});
60-
const choiceStringMixinPredicate = choiceBaseMixinPredicate.extend({
62+
const choiceStringMixinPredicate = z.object({
63+
...choiceBaseMixinPredicate.shape,
6164
choices: choiceStringPredicate.array().max(25).optional(),
6265
});
63-
const choiceNumberMixinPredicate = choiceBaseMixinPredicate.extend({
66+
const choiceNumberMixinPredicate = z.object({
67+
...choiceBaseMixinPredicate.shape,
6468
choices: choiceNumberPredicate.array().max(25).optional(),
6569
});
6670

67-
const basicOptionTypesPredicate = z.literal([
68-
ApplicationCommandOptionType.Attachment,
69-
ApplicationCommandOptionType.Boolean,
70-
ApplicationCommandOptionType.Channel,
71-
ApplicationCommandOptionType.Integer,
72-
ApplicationCommandOptionType.Mentionable,
73-
ApplicationCommandOptionType.Number,
74-
ApplicationCommandOptionType.Role,
75-
ApplicationCommandOptionType.String,
76-
ApplicationCommandOptionType.User,
77-
]);
78-
79-
export const basicOptionPredicate = sharedNameAndDescriptionPredicate.extend({
71+
export const baseBasicOptionPredicate = z.object({
72+
...sharedNameAndDescriptionPredicate.shape,
8073
required: z.boolean().optional(),
81-
type: basicOptionTypesPredicate,
74+
});
75+
76+
export const attachmentOptionPredicate = z.object({
77+
...baseBasicOptionPredicate.shape,
78+
type: z.literal(ApplicationCommandOptionType.Attachment),
79+
});
80+
81+
export const booleanOptionPredicate = z.object({
82+
...baseBasicOptionPredicate.shape,
83+
type: z.literal(ApplicationCommandOptionType.Boolean),
84+
});
85+
86+
export const mentionableOptionPredicate = z.object({
87+
...baseBasicOptionPredicate.shape,
88+
type: z.literal(ApplicationCommandOptionType.Mentionable),
89+
});
90+
91+
export const roleOptionPredicate = z.object({
92+
...baseBasicOptionPredicate.shape,
93+
type: z.literal(ApplicationCommandOptionType.Role),
94+
});
95+
96+
export const userOptionPredicate = z.object({
97+
...baseBasicOptionPredicate.shape,
98+
type: z.literal(ApplicationCommandOptionType.User),
8299
});
83100

84101
const autocompleteOrStringChoicesMixinOptionPredicate = z.discriminatedUnion('autocomplete', [
@@ -92,58 +109,70 @@ const autocompleteOrNumberChoicesMixinOptionPredicate = z.discriminatedUnion('au
92109
]);
93110

94111
export const channelOptionPredicate = z.object({
95-
...basicOptionPredicate.shape,
112+
...baseBasicOptionPredicate.shape,
96113
...channelMixinOptionPredicate.shape,
114+
type: z.literal(ApplicationCommandOptionType.Channel),
97115
});
98116

99117
export const integerOptionPredicate = z
100118
.object({
101-
...basicOptionPredicate.shape,
119+
...baseBasicOptionPredicate.shape,
102120
...numericMixinIntegerOptionPredicate.shape,
121+
type: z.literal(ApplicationCommandOptionType.Integer),
103122
})
104123
.and(autocompleteOrNumberChoicesMixinOptionPredicate);
105124

106125
export const numberOptionPredicate = z
107126
.object({
108-
...basicOptionPredicate.shape,
127+
...baseBasicOptionPredicate.shape,
109128
...numericMixinNumberOptionPredicate.shape,
129+
type: z.literal(ApplicationCommandOptionType.Number),
110130
})
111131
.and(autocompleteOrNumberChoicesMixinOptionPredicate);
112132

113-
export const stringOptionPredicate = basicOptionPredicate
114-
.extend({
133+
export const stringOptionPredicate = z
134+
.object({
135+
...baseBasicOptionPredicate.shape,
115136
max_length: z.number().min(0).max(6_000).optional(),
116137
min_length: z.number().min(1).max(6_000).optional(),
138+
type: z.literal(ApplicationCommandOptionType.String),
117139
})
118140
.and(autocompleteOrStringChoicesMixinOptionPredicate);
119141

120-
const baseChatInputCommandPredicate = sharedNameAndDescriptionPredicate.extend({
121-
contexts: z.array(z.enum(InteractionContextType)).optional(),
122-
default_member_permissions: memberPermissionsPredicate.optional(),
123-
integration_types: z.array(z.enum(ApplicationIntegrationType)).optional(),
124-
nsfw: z.boolean().optional(),
125-
});
126-
127-
// Because you can only add options via builders, there's no need to validate whole objects here otherwise
128-
const chatInputCommandOptionsPredicate = z.union([
129-
z.object({ type: basicOptionTypesPredicate }).array(),
130-
z.object({ type: z.literal(ApplicationCommandOptionType.Subcommand) }).array(),
131-
z.object({ type: z.literal(ApplicationCommandOptionType.SubcommandGroup) }).array(),
132-
]);
133-
134-
export const chatInputCommandPredicate = baseChatInputCommandPredicate.extend({
135-
options: chatInputCommandOptionsPredicate.optional(),
142+
const basicOptionPredicates = [
143+
attachmentOptionPredicate,
144+
booleanOptionPredicate,
145+
channelOptionPredicate,
146+
integerOptionPredicate,
147+
mentionableOptionPredicate,
148+
numberOptionPredicate,
149+
roleOptionPredicate,
150+
stringOptionPredicate,
151+
userOptionPredicate,
152+
];
153+
154+
export const chatInputCommandSubcommandPredicate = z.object({
155+
...sharedNameAndDescriptionPredicate.shape,
156+
type: z.literal(ApplicationCommandOptionType.Subcommand),
157+
options: z.array(z.union(basicOptionPredicates)).max(25),
136158
});
137159

138-
export const chatInputCommandSubcommandGroupPredicate = sharedNameAndDescriptionPredicate.extend({
160+
export const chatInputCommandSubcommandGroupPredicate = z.object({
161+
...sharedNameAndDescriptionPredicate.shape,
139162
type: z.literal(ApplicationCommandOptionType.SubcommandGroup),
140-
options: z
141-
.array(z.object({ type: z.literal(ApplicationCommandOptionType.Subcommand) }))
142-
.min(1)
143-
.max(25),
163+
options: z.array(chatInputCommandSubcommandPredicate).min(1).max(25),
144164
});
145165

146-
export const chatInputCommandSubcommandPredicate = sharedNameAndDescriptionPredicate.extend({
147-
type: z.literal(ApplicationCommandOptionType.Subcommand),
148-
options: z.array(z.object({ type: basicOptionTypesPredicate })).max(25),
166+
export const chatInputCommandPredicate = z.object({
167+
...sharedNameAndDescriptionPredicate.shape,
168+
contexts: z.array(z.enum(InteractionContextType)).optional(),
169+
default_member_permissions: memberPermissionsPredicate.optional(),
170+
integration_types: z.array(z.enum(ApplicationIntegrationType)).optional(),
171+
nsfw: z.boolean().optional(),
172+
options: z
173+
.union([
174+
z.array(z.union(basicOptionPredicates)).max(25),
175+
z.array(z.union([chatInputCommandSubcommandPredicate, chatInputCommandSubcommandGroupPredicate])).max(25),
176+
])
177+
.optional(),
149178
});

packages/builders/src/interactions/commands/chatInput/ChatInputCommand.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class ChatInputCommandBuilder extends Mixin(
3030
const data: RESTPostAPIChatInputApplicationCommandsJSONBody = {
3131
...structuredClone(rest as Omit<RESTPostAPIChatInputApplicationCommandsJSONBody, 'options'>),
3232
type: ApplicationCommandType.ChatInput,
33-
options: options?.map((option) => option.toJSON(validationOverride)),
33+
options: options?.map((option) => option.toJSON(false)),
3434
};
3535

3636
validate(chatInputCommandPredicate, data, validationOverride);

packages/builders/src/interactions/commands/chatInput/options/ApplicationCommandOptionBase.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import type { z } from 'zod';
88
import { validate } from '../../../../util/validation.js';
99
import type { SharedNameAndDescriptionData } from '../../SharedNameAndDescription.js';
1010
import { SharedNameAndDescription } from '../../SharedNameAndDescription.js';
11-
import { basicOptionPredicate } from '../Assertions.js';
1211

1312
export interface ApplicationCommandOptionBaseData extends Partial<Pick<APIApplicationCommandOption, 'required'>> {
1413
type: ApplicationCommandOptionType;
@@ -24,7 +23,7 @@ export abstract class ApplicationCommandOptionBase
2423
/**
2524
* @internal
2625
*/
27-
protected static readonly predicate: z.ZodType = basicOptionPredicate;
26+
protected static readonly predicate: z.ZodType;
2827

2928
/**
3029
* @internal

packages/builders/src/interactions/commands/chatInput/options/attachment.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
2+
import { attachmentOptionPredicate } from '../Assertions.js';
23
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
34

45
/**
56
* A chat input command attachment option.
67
*/
78
export class ChatInputCommandAttachmentOption extends ApplicationCommandOptionBase {
9+
/**
10+
* @internal
11+
*/
12+
protected static override readonly predicate = attachmentOptionPredicate;
13+
814
/**
915
* Creates a new attachment option.
1016
*/

packages/builders/src/interactions/commands/chatInput/options/boolean.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
2+
import { booleanOptionPredicate } from '../Assertions.js';
23
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
34

45
/**
56
* A chat input command boolean option.
67
*/
78
export class ChatInputCommandBooleanOption extends ApplicationCommandOptionBase {
9+
/**
10+
* @internal
11+
*/
12+
protected static override readonly predicate = booleanOptionPredicate;
13+
814
/**
915
* Creates a new boolean option.
1016
*/

packages/builders/src/interactions/commands/chatInput/options/mentionable.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
2+
import { mentionableOptionPredicate } from '../Assertions.js';
23
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
34

45
/**
56
* A chat input command mentionable option.
67
*/
78
export class ChatInputCommandMentionableOption extends ApplicationCommandOptionBase {
9+
/**
10+
* @internal
11+
*/
12+
protected static override readonly predicate = mentionableOptionPredicate;
13+
814
/**
915
* Creates a new mentionable option.
1016
*/

packages/builders/src/interactions/commands/chatInput/options/role.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
2+
import { roleOptionPredicate } from '../Assertions.js';
23
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
34

45
/**
56
* A chat input command role option.
67
*/
78
export class ChatInputCommandRoleOption extends ApplicationCommandOptionBase {
9+
/**
10+
* @internal
11+
*/
12+
protected static override readonly predicate = roleOptionPredicate;
13+
814
/**
915
* Creates a new role option.
1016
*/

packages/builders/src/interactions/commands/chatInput/options/user.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
2+
import { userOptionPredicate } from '../Assertions.js';
23
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
34

45
/**
56
* A chat input command user option.
67
*/
78
export class ChatInputCommandUserOption extends ApplicationCommandOptionBase {
9+
/**
10+
* @internal
11+
*/
12+
protected static override readonly predicate = userOptionPredicate;
13+
814
/**
915
* Creates a new user option.
1016
*/

0 commit comments

Comments
 (0)