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

Commit

Permalink
fix(ApplicationCommandOptions): clean up code for builder options (#68)
Browse files Browse the repository at this point in the history
  • Loading branch information
vladfrangu committed Dec 28, 2021
1 parent a9addd5 commit b5d0b15
Show file tree
Hide file tree
Showing 22 changed files with 318 additions and 257 deletions.
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 {
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;
}
}

0 comments on commit b5d0b15

Please sign in to comment.