Skip to content
This repository was archived by the owner on Jan 8, 2022. It is now read-only.

Commit b5d0b15

Browse files
authored
fix(ApplicationCommandOptions): clean up code for builder options (#68)
1 parent a9addd5 commit b5d0b15

22 files changed

+318
-257
lines changed

__tests__/SlashCommands.test.ts

+32-10
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,22 @@ describe('Slash Commands', () => {
207207

208208
expect(() => {
209209
const option = getStringOption();
210-
option.autocomplete = true;
211-
option.choices = [{ name: 'Fancy Pants', value: 'fp_1' }];
210+
Reflect.set(option, 'autocomplete', true);
211+
Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]);
212+
return option.toJSON();
213+
}).toThrowError();
214+
215+
expect(() => {
216+
const option = getNumberOption();
217+
Reflect.set(option, 'autocomplete', true);
218+
Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]);
219+
return option.toJSON();
220+
}).toThrowError();
221+
222+
expect(() => {
223+
const option = getIntegerOption();
224+
Reflect.set(option, 'autocomplete', true);
225+
Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]);
212226
return option.toJSON();
213227
}).toThrowError();
214228
});
@@ -229,14 +243,6 @@ describe('Slash Commands', () => {
229243
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelType(100))).toThrowError();
230244

231245
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelTypes([100, 200]))).toThrowError();
232-
233-
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelType(100))).toThrowError();
234-
235-
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelType(1))).toThrowError();
236-
237-
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelType(1))).toThrowError();
238-
239-
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelTypes([1, 2, 3]))).toThrowError();
240246
});
241247

242248
test('GIVEN a builder with invalid number min/max options THEN does throw an error', () => {
@@ -324,6 +330,22 @@ describe('Slash Commands', () => {
324330
test('GIVEN valid builder with defaultPermission false THEN does not throw error', () => {
325331
expect(() => getBuilder().setName('foo').setDescription('foo').setDefaultPermission(false)).not.toThrowError();
326332
});
333+
334+
test('GIVEN an option that is autocompletable and has choices, THEN setting choices to an empty array should not throw an error', () => {
335+
expect(() =>
336+
getBuilder().addStringOption(getStringOption().setAutocomplete(true).setChoices([])),
337+
).not.toThrowError();
338+
});
339+
340+
test('GIVEN an option that is autocompletable and has choices, THEN setting choices should throw an error', () => {
341+
expect(() =>
342+
getBuilder().addStringOption(
343+
getStringOption()
344+
.setAutocomplete(true)
345+
.setChoices([['owo', 'uwu']]),
346+
),
347+
).toThrowError();
348+
});
327349
});
328350

329351
describe('Builder with subcommand (group) options', () => {

package-lock.json

+7-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"homepage": "https://github.com/discordjs/builders",
5757
"dependencies": {
5858
"@sindresorhus/is": "^4.2.0",
59-
"discord-api-types": "^0.25.2",
59+
"discord-api-types": "^0.26.0",
6060
"ts-mixer": "^6.0.0",
6161
"tslib": "^2.3.1",
6262
"zod": "^3.11.6"

src/interactions/slashCommands/Assertions.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import is from '@sindresorhus/is';
22
import type { APIApplicationCommandOptionChoice } from 'discord-api-types/v9';
33
import { z } from 'zod';
4-
import type { SlashCommandOptionBase } from './mixins/CommandOptionBase';
4+
import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase';
55
import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder';
66
import type { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands';
77

@@ -57,7 +57,7 @@ export function validateMaxChoicesLength(choices: APIApplicationCommandOptionCho
5757
}
5858

5959
export function assertReturnOfBuilder<
60-
T extends SlashCommandOptionBase | SlashCommandSubcommandBuilder | SlashCommandSubcommandGroupBuilder,
60+
T extends ApplicationCommandOptionBase | SlashCommandSubcommandBuilder | SlashCommandSubcommandGroupBuilder,
6161
>(input: unknown, ExpectedInstanceOf: new () => T): asserts input is T {
6262
const instanceName = ExpectedInstanceOf.name;
6363

src/interactions/slashCommands/SlashCommandBuilder.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
validateMaxOptionsLength,
77
validateRequiredParameters,
88
} from './Assertions';
9-
import { SharedSlashCommandOptions } from './mixins/CommandOptions';
9+
import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions';
1010
import { SharedNameAndDescription } from './mixins/NameAndDescription';
1111
import { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands';
1212

@@ -41,6 +41,7 @@ export class SlashCommandBuilder {
4141
*/
4242
public toJSON(): RESTPostAPIApplicationCommandsJSONBody {
4343
validateRequiredParameters(this.name, this.description, this.options);
44+
4445
return {
4546
name: this.name,
4647
description: this.description,

src/interactions/slashCommands/SlashCommandSubcommands.ts

+13-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
import { APIApplicationCommandSubCommandOptions, ApplicationCommandOptionType } from 'discord-api-types/v9';
1+
import {
2+
APIApplicationCommandSubcommandGroupOption,
3+
APIApplicationCommandSubcommandOption,
4+
ApplicationCommandOptionType,
5+
} from 'discord-api-types/v9';
26
import { mix } from 'ts-mixer';
37
import { assertReturnOfBuilder, validateMaxOptionsLength, validateRequiredParameters } from './Assertions';
4-
import { SharedSlashCommandOptions } from './mixins/CommandOptions';
8+
import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase';
59
import { SharedNameAndDescription } from './mixins/NameAndDescription';
10+
import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions';
611
import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder';
712

813
/**
@@ -25,7 +30,7 @@ export class SlashCommandSubcommandGroupBuilder implements ToAPIApplicationComma
2530
/**
2631
* The subcommands part of this subcommand group
2732
*/
28-
public readonly options: ToAPIApplicationCommandOptions[] = [];
33+
public readonly options: SlashCommandSubcommandBuilder[] = [];
2934

3035
/**
3136
* Adds a new subcommand to this group
@@ -53,8 +58,9 @@ export class SlashCommandSubcommandGroupBuilder implements ToAPIApplicationComma
5358
return this;
5459
}
5560

56-
public toJSON(): APIApplicationCommandSubCommandOptions {
61+
public toJSON(): APIApplicationCommandSubcommandGroupOption {
5762
validateRequiredParameters(this.name, this.description, this.options);
63+
5864
return {
5965
type: ApplicationCommandOptionType.SubcommandGroup,
6066
name: this.name,
@@ -86,10 +92,11 @@ export class SlashCommandSubcommandBuilder implements ToAPIApplicationCommandOpt
8692
/**
8793
* The options of this subcommand
8894
*/
89-
public readonly options: ToAPIApplicationCommandOptions[] = [];
95+
public readonly options: ApplicationCommandOptionBase[] = [];
9096

91-
public toJSON(): APIApplicationCommandSubCommandOptions {
97+
public toJSON(): APIApplicationCommandSubcommandOption {
9298
validateRequiredParameters(this.name, this.description, this.options);
99+
93100
return {
94101
type: ApplicationCommandOptionType.Subcommand,
95102
name: this.name,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export abstract class ApplicationCommandNumericOptionMinMaxValueMixin {
2+
protected readonly maxValue?: number;
3+
protected readonly minValue?: number;
4+
5+
/**
6+
* Sets the maximum number value of this option
7+
* @param max The maximum value this option can be
8+
*/
9+
public abstract setMaxValue(max: number): this;
10+
11+
/**
12+
* Sets the minimum number value of this option
13+
* @param min The minimum value this option can be
14+
*/
15+
public abstract setMinValue(min: number): this;
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { APIApplicationCommandBasicOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
2+
import { validateRequiredParameters, validateRequired } from '../Assertions';
3+
import { SharedNameAndDescription } from './NameAndDescription';
4+
5+
export abstract class ApplicationCommandOptionBase extends SharedNameAndDescription {
6+
public abstract readonly type: ApplicationCommandOptionType;
7+
8+
public readonly required = false;
9+
10+
/**
11+
* Marks the option as required
12+
*
13+
* @param required If this option should be required
14+
*/
15+
public setRequired(required: boolean) {
16+
// Assert that you actually passed a boolean
17+
validateRequired(required);
18+
19+
Reflect.set(this, 'required', required);
20+
21+
return this;
22+
}
23+
24+
public abstract toJSON(): APIApplicationCommandBasicOption;
25+
26+
protected runRequiredValidations() {
27+
validateRequiredParameters(this.name, this.description, []);
28+
29+
// Assert that you actually passed a boolean
30+
validateRequired(this.required);
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { ChannelType } from 'discord-api-types/v9';
2+
import { z, ZodLiteral } from 'zod';
3+
4+
// Only allow valid channel types to be used. (This can't be dynamic because const enums are erased at runtime)
5+
const allowedChannelTypes = [
6+
ChannelType.GuildText,
7+
ChannelType.GuildVoice,
8+
ChannelType.GuildCategory,
9+
ChannelType.GuildNews,
10+
ChannelType.GuildStore,
11+
ChannelType.GuildNewsThread,
12+
ChannelType.GuildPublicThread,
13+
ChannelType.GuildPrivateThread,
14+
ChannelType.GuildStageVoice,
15+
] as const;
16+
17+
export type ApplicationCommandOptionAllowedChannelTypes = typeof allowedChannelTypes[number];
18+
19+
const channelTypePredicate = z.union(
20+
allowedChannelTypes.map((type) => z.literal(type)) as [
21+
ZodLiteral<ChannelType>,
22+
ZodLiteral<ChannelType>,
23+
...ZodLiteral<ChannelType>[]
24+
],
25+
);
26+
27+
export class ApplicationCommandOptionChannelTypesMixin {
28+
public readonly channel_types?: ApplicationCommandOptionAllowedChannelTypes[];
29+
30+
/**
31+
* Adds a channel type to this option
32+
*
33+
* @param channelType The type of channel to allow
34+
*/
35+
public addChannelType(channelType: ApplicationCommandOptionAllowedChannelTypes) {
36+
if (this.channel_types === undefined) {
37+
Reflect.set(this, 'channel_types', []);
38+
}
39+
40+
channelTypePredicate.parse(channelType);
41+
this.channel_types!.push(channelType);
42+
43+
return this;
44+
}
45+
46+
/**
47+
* Adds channel types to this option
48+
*
49+
* @param channelTypes The channel types to add
50+
*/
51+
public addChannelTypes(channelTypes: ApplicationCommandOptionAllowedChannelTypes[]) {
52+
channelTypes.forEach((channelType) => this.addChannelType(channelType));
53+
return this;
54+
}
55+
}

0 commit comments

Comments
 (0)