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

fix(ApplicationCommandOptions): clean up code for builder options #68

Merged
merged 2 commits into from
Dec 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 32 additions & 10 deletions __tests__/SlashCommands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,22 @@ describe('Slash Commands', () => {

expect(() => {
const option = getStringOption();
option.autocomplete = true;
option.choices = [{ name: 'Fancy Pants', value: 'fp_1' }];
Reflect.set(option, 'autocomplete', true);
Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]);
return option.toJSON();
}).toThrowError();

expect(() => {
const option = getNumberOption();
Reflect.set(option, 'autocomplete', true);
Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]);
return option.toJSON();
}).toThrowError();

expect(() => {
const option = getIntegerOption();
Reflect.set(option, 'autocomplete', true);
Reflect.set(option, 'choices', [{ name: 'Fancy Pants', value: 'fp_1' }]);
return option.toJSON();
}).toThrowError();
});
Expand All @@ -229,14 +243,6 @@ describe('Slash Commands', () => {
expect(() => getBuilder().addChannelOption(getChannelOption().addChannelType(100))).toThrowError();

expect(() => getBuilder().addChannelOption(getChannelOption().addChannelTypes([100, 200]))).toThrowError();

expect(() => getBuilder().addChannelOption(getChannelOption().addChannelType(100))).toThrowError();

expect(() => getBuilder().addChannelOption(getChannelOption().addChannelType(1))).toThrowError();

expect(() => getBuilder().addChannelOption(getChannelOption().addChannelType(1))).toThrowError();

expect(() => getBuilder().addChannelOption(getChannelOption().addChannelTypes([1, 2, 3]))).toThrowError();
});

test('GIVEN a builder with invalid number min/max options THEN does throw an error', () => {
Expand Down Expand Up @@ -324,6 +330,22 @@ describe('Slash Commands', () => {
test('GIVEN valid builder with defaultPermission false THEN does not throw error', () => {
expect(() => getBuilder().setName('foo').setDescription('foo').setDefaultPermission(false)).not.toThrowError();
});

test('GIVEN an option that is autocompletable and has choices, THEN setting choices to an empty array should not throw an error', () => {
expect(() =>
getBuilder().addStringOption(getStringOption().setAutocomplete(true).setChoices([])),
).not.toThrowError();
});

test('GIVEN an option that is autocompletable and has choices, THEN setting choices should throw an error', () => {
expect(() =>
getBuilder().addStringOption(
getStringOption()
.setAutocomplete(true)
.setChoices([['owo', 'uwu']]),
),
).toThrowError();
});
});

describe('Builder with subcommand (group) options', () => {
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"homepage": "https://github.com/discordjs/builders",
"dependencies": {
"@sindresorhus/is": "^4.2.0",
"discord-api-types": "^0.25.2",
"discord-api-types": "^0.26.0",
"ts-mixer": "^6.0.0",
"tslib": "^2.3.1",
"zod": "^3.11.6"
Expand Down
4 changes: 2 additions & 2 deletions src/interactions/slashCommands/Assertions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import is from '@sindresorhus/is';
import type { APIApplicationCommandOptionChoice } from 'discord-api-types/v9';
import { z } from 'zod';
import type { SlashCommandOptionBase } from './mixins/CommandOptionBase';
import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase';
import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder';
import type { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands';

Expand Down Expand Up @@ -57,7 +57,7 @@ export function validateMaxChoicesLength(choices: APIApplicationCommandOptionCho
}

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

Expand Down
3 changes: 2 additions & 1 deletion src/interactions/slashCommands/SlashCommandBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
validateMaxOptionsLength,
validateRequiredParameters,
} from './Assertions';
import { SharedSlashCommandOptions } from './mixins/CommandOptions';
import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions';
import { SharedNameAndDescription } from './mixins/NameAndDescription';
import { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands';

Expand Down Expand Up @@ -41,6 +41,7 @@ export class SlashCommandBuilder {
*/
public toJSON(): RESTPostAPIApplicationCommandsJSONBody {
validateRequiredParameters(this.name, this.description, this.options);

return {
name: this.name,
description: this.description,
Expand Down
19 changes: 13 additions & 6 deletions src/interactions/slashCommands/SlashCommandSubcommands.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { APIApplicationCommandSubCommandOptions, ApplicationCommandOptionType } from 'discord-api-types/v9';
import {
APIApplicationCommandSubcommandGroupOption,
APIApplicationCommandSubcommandOption,
ApplicationCommandOptionType,
} from 'discord-api-types/v9';
import { mix } from 'ts-mixer';
import { assertReturnOfBuilder, validateMaxOptionsLength, validateRequiredParameters } from './Assertions';
import { SharedSlashCommandOptions } from './mixins/CommandOptions';
import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase';
import { SharedNameAndDescription } from './mixins/NameAndDescription';
import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions';
import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder';

/**
Expand All @@ -25,7 +30,7 @@ export class SlashCommandSubcommandGroupBuilder implements ToAPIApplicationComma
/**
* The subcommands part of this subcommand group
*/
public readonly options: ToAPIApplicationCommandOptions[] = [];
public readonly options: SlashCommandSubcommandBuilder[] = [];

/**
* Adds a new subcommand to this group
Expand Down Expand Up @@ -53,8 +58,9 @@ export class SlashCommandSubcommandGroupBuilder implements ToAPIApplicationComma
return this;
}

public toJSON(): APIApplicationCommandSubCommandOptions {
public toJSON(): APIApplicationCommandSubcommandGroupOption {
validateRequiredParameters(this.name, this.description, this.options);

return {
type: ApplicationCommandOptionType.SubcommandGroup,
name: this.name,
Expand Down Expand Up @@ -86,10 +92,11 @@ export class SlashCommandSubcommandBuilder implements ToAPIApplicationCommandOpt
/**
* The options of this subcommand
*/
public readonly options: ToAPIApplicationCommandOptions[] = [];
public readonly options: ApplicationCommandOptionBase[] = [];

public toJSON(): APIApplicationCommandSubCommandOptions {
public toJSON(): APIApplicationCommandSubcommandOption {
validateRequiredParameters(this.name, this.description, this.options);

return {
type: ApplicationCommandOptionType.Subcommand,
name: this.name,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export abstract class ApplicationCommandNumericOptionMinMaxValueMixin {
protected readonly maxValue?: number;
protected readonly minValue?: number;

/**
* Sets the maximum number value of this option
* @param max The maximum value this option can be
*/
public abstract setMaxValue(max: number): this;

/**
* Sets the minimum number value of this option
* @param min The minimum value this option can be
*/
public abstract setMinValue(min: number): this;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { APIApplicationCommandBasicOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
import { validateRequiredParameters, validateRequired } from '../Assertions';
import { SharedNameAndDescription } from './NameAndDescription';

export abstract class ApplicationCommandOptionBase extends SharedNameAndDescription {
public abstract readonly type: ApplicationCommandOptionType;

public readonly required = false;

/**
* Marks the option as required
*
* @param required If this option should be required
*/
public setRequired(required: boolean) {
// Assert that you actually passed a boolean
validateRequired(required);

Reflect.set(this, 'required', required);

return this;
}

public abstract toJSON(): APIApplicationCommandBasicOption;

protected runRequiredValidations() {
validateRequiredParameters(this.name, this.description, []);

// Assert that you actually passed a boolean
validateRequired(this.required);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { ChannelType } from 'discord-api-types/v9';
import { z, ZodLiteral } from 'zod';

// Only allow valid channel types to be used. (This can't be dynamic because const enums are erased at runtime)
const allowedChannelTypes = [
ChannelType.GuildText,
ChannelType.GuildVoice,
ChannelType.GuildCategory,
ChannelType.GuildNews,
ChannelType.GuildStore,
ChannelType.GuildNewsThread,
ChannelType.GuildPublicThread,
ChannelType.GuildPrivateThread,
ChannelType.GuildStageVoice,
] as const;

export type ApplicationCommandOptionAllowedChannelTypes = typeof allowedChannelTypes[number];

const channelTypePredicate = z.union(
allowedChannelTypes.map((type) => z.literal(type)) as [
ZodLiteral<ChannelType>,
ZodLiteral<ChannelType>,
...ZodLiteral<ChannelType>[]
],
);

export class ApplicationCommandOptionChannelTypesMixin {
vladfrangu marked this conversation as resolved.
Show resolved Hide resolved
public readonly channel_types?: ApplicationCommandOptionAllowedChannelTypes[];

/**
* Adds a channel type to this option
*
* @param channelType The type of channel to allow
*/
public addChannelType(channelType: ApplicationCommandOptionAllowedChannelTypes) {
if (this.channel_types === undefined) {
Reflect.set(this, 'channel_types', []);
}

channelTypePredicate.parse(channelType);
this.channel_types!.push(channelType);

return this;
}

/**
* Adds channel types to this option
*
* @param channelTypes The channel types to add
*/
public addChannelTypes(channelTypes: ApplicationCommandOptionAllowedChannelTypes[]) {
channelTypes.forEach((channelType) => this.addChannelType(channelType));
return this;
}
}
Loading