diff --git a/__tests__/SlashCommands.test.ts b/__tests__/SlashCommands.test.ts index 3a1b3fa5..6b6e2061 100644 --- a/__tests__/SlashCommands.test.ts +++ b/__tests__/SlashCommands.test.ts @@ -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(); }); @@ -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', () => { @@ -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', () => { diff --git a/package-lock.json b/package-lock.json index 3f9f66a8..74bb2464 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "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" @@ -4949,9 +4949,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.25.2.tgz", - "integrity": "sha512-O243LXxb5gLLxubu5zgoppYQuolapGVWPw3ll0acN0+O8TnPUE2kFp9Bt3sTRYodw8xFIknOVxjSeyWYBpVcEQ==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.26.0.tgz", + "integrity": "sha512-bnUltSHpQLzTVZTMjm+iNgVhAbtm5oAKHrhtiPaZoxprbm1UtuCZCsG0yXM61NamWfeSz7xnLvgFc50YzVJ5cQ==", "engines": { "node": ">=12" } @@ -15384,9 +15384,9 @@ } }, "discord-api-types": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.25.2.tgz", - "integrity": "sha512-O243LXxb5gLLxubu5zgoppYQuolapGVWPw3ll0acN0+O8TnPUE2kFp9Bt3sTRYodw8xFIknOVxjSeyWYBpVcEQ==" + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.26.0.tgz", + "integrity": "sha512-bnUltSHpQLzTVZTMjm+iNgVhAbtm5oAKHrhtiPaZoxprbm1UtuCZCsG0yXM61NamWfeSz7xnLvgFc50YzVJ5cQ==" }, "doctrine": { "version": "3.0.0", diff --git a/package.json b/package.json index b82cafa7..7aefb73c 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/interactions/slashCommands/Assertions.ts b/src/interactions/slashCommands/Assertions.ts index 24f2240e..af00a214 100644 --- a/src/interactions/slashCommands/Assertions.ts +++ b/src/interactions/slashCommands/Assertions.ts @@ -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'; @@ -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; diff --git a/src/interactions/slashCommands/SlashCommandBuilder.ts b/src/interactions/slashCommands/SlashCommandBuilder.ts index f7e5e392..c9ddb2a9 100644 --- a/src/interactions/slashCommands/SlashCommandBuilder.ts +++ b/src/interactions/slashCommands/SlashCommandBuilder.ts @@ -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'; @@ -41,6 +41,7 @@ export class SlashCommandBuilder { */ public toJSON(): RESTPostAPIApplicationCommandsJSONBody { validateRequiredParameters(this.name, this.description, this.options); + return { name: this.name, description: this.description, diff --git a/src/interactions/slashCommands/SlashCommandSubcommands.ts b/src/interactions/slashCommands/SlashCommandSubcommands.ts index 6071a806..08ff9809 100644 --- a/src/interactions/slashCommands/SlashCommandSubcommands.ts +++ b/src/interactions/slashCommands/SlashCommandSubcommands.ts @@ -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'; /** @@ -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 @@ -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, @@ -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, diff --git a/src/interactions/slashCommands/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts b/src/interactions/slashCommands/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts new file mode 100644 index 00000000..7b837959 --- /dev/null +++ b/src/interactions/slashCommands/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.ts @@ -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; +} diff --git a/src/interactions/slashCommands/mixins/ApplicationCommandOptionBase.ts b/src/interactions/slashCommands/mixins/ApplicationCommandOptionBase.ts new file mode 100644 index 00000000..dd97c228 --- /dev/null +++ b/src/interactions/slashCommands/mixins/ApplicationCommandOptionBase.ts @@ -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); + } +} diff --git a/src/interactions/slashCommands/mixins/ApplicationCommandOptionChannelTypesMixin.ts b/src/interactions/slashCommands/mixins/ApplicationCommandOptionChannelTypesMixin.ts new file mode 100644 index 00000000..783cdd87 --- /dev/null +++ b/src/interactions/slashCommands/mixins/ApplicationCommandOptionChannelTypesMixin.ts @@ -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, + ZodLiteral, + ...ZodLiteral[] + ], +); + +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; + } +} diff --git a/src/interactions/slashCommands/mixins/CommandOptionWithChoices.ts b/src/interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin.ts similarity index 60% rename from src/interactions/slashCommands/mixins/CommandOptionWithChoices.ts rename to src/interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin.ts index ff37fa37..88169c8d 100644 --- a/src/interactions/slashCommands/mixins/CommandOptionWithChoices.ts +++ b/src/interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin.ts @@ -1,27 +1,19 @@ -import { - APIApplicationCommandOption, - APIApplicationCommandOptionChoice, - ApplicationCommandOptionType, -} from 'discord-api-types/v9'; +import { APIApplicationCommandOptionChoice, ApplicationCommandOptionType } from 'discord-api-types/v9'; import { z } from 'zod'; import { validateMaxChoicesLength } from '../Assertions'; -import type { ToAPIApplicationCommandOptions } from '../SlashCommandBuilder'; -import { SlashCommandOptionBase } from './CommandOptionBase'; const stringPredicate = z.string().min(1).max(100); const numberPredicate = z.number().gt(-Infinity).lt(Infinity); const choicesPredicate = z.tuple([stringPredicate, z.union([stringPredicate, numberPredicate])]).array(); const booleanPredicate = z.boolean(); -export abstract class ApplicationCommandOptionWithChoicesBase - extends SlashCommandOptionBase< - ApplicationCommandOptionType.String | ApplicationCommandOptionType.Number | ApplicationCommandOptionType.Integer - > - implements ToAPIApplicationCommandOptions -{ - public choices?: APIApplicationCommandOptionChoice[]; +export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin { + public readonly choices?: APIApplicationCommandOptionChoice[]; public readonly autocomplete?: boolean; + // Since this is present and this is a mixin, this is needed + public readonly type!: ApplicationCommandOptionType; + /** * Adds a choice for this option * @@ -33,18 +25,23 @@ export abstract class ApplicationCommandOptionWithChoicesBase( + choices: Input, + ): Input extends [] + ? this & Pick, 'setAutocomplete'> + : Omit { + if (choices.length > 0 && this.autocomplete) { + throw new RangeError('Autocomplete and choices are mutually exclusive to each other.'); + } + + choicesPredicate.parse(choices); + + Reflect.set(this, 'choices', []); + for (const [label, value] of choices) this.addChoice(label, value); + + return this; + } + /** * Marks the option as autocompletable * @param autocomplete If this option should be autocompletable @@ -73,7 +87,7 @@ export abstract class ApplicationCommandOptionWithChoicesBase - : this & Pick, 'addChoice' | 'addChoices'> { + : this & Pick, 'addChoice' | 'addChoices'> { // Assert that you actually passed a boolean booleanPredicate.parse(autocomplete); @@ -85,18 +99,4 @@ export abstract class ApplicationCommandOptionWithChoicesBase 0) { - throw new RangeError('Autocomplete and choices are mutually exclusive to each other.'); - } - - // TODO: Fix types - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return { - ...super.toJSON(), - choices: this.choices, - autocomplete: this.autocomplete, - } as APIApplicationCommandOption; - } } diff --git a/src/interactions/slashCommands/mixins/CommandChannelOptionBase.ts b/src/interactions/slashCommands/mixins/CommandChannelOptionBase.ts deleted file mode 100644 index 3986a958..00000000 --- a/src/interactions/slashCommands/mixins/CommandChannelOptionBase.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { APIApplicationCommandChannelOptions, ApplicationCommandOptionType, ChannelType } from 'discord-api-types/v9'; -import { z, ZodLiteral } from 'zod'; -import type { ToAPIApplicationCommandOptions } from '../../..'; -import { SlashCommandOptionBase } from './CommandOptionBase'; - -// Only allow valid channel types to be used. (This can't be dynamic because const enums are erased at runtime) -const allowedChannelTypes = [ - ChannelType.GuildCategory, - ChannelType.GuildNews, - ChannelType.GuildNewsThread, - ChannelType.GuildStore, - ChannelType.GuildStageVoice, - ChannelType.GuildText, - ChannelType.GuildVoice, - ChannelType.GuildPublicThread, - ChannelType.GuildPrivateThread, -]; - -const channelTypePredicate = z.union( - allowedChannelTypes.map((type) => z.literal(type)) as [ - ZodLiteral, - ZodLiteral, - ...ZodLiteral[] - ], -); - -export abstract class ApplicationCommandOptionWithChannelTypesBase - extends SlashCommandOptionBase - implements ToAPIApplicationCommandOptions -{ - public channelTypes?: Exclude[]; - - /** - * Adds a channel type to this option - * - * @param channelType The type of channel to allow - */ - public addChannelType(channelType: Exclude) { - this.channelTypes ??= []; - - channelTypePredicate.parse(channelType); - this.channelTypes.push(channelType); - - return this; - } - - /** - * Adds channel types to this option - * - * @param channelTypes The channel types to add - */ - public addChannelTypes(channelTypes: Exclude[]) { - channelTypes.forEach((channelType) => this.addChannelType(channelType)); - return this; - } - - public override toJSON(): APIApplicationCommandChannelOptions { - // TODO: Fix types - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return { - ...super.toJSON(), - type: this.type, - channel_types: this.channelTypes, - } as APIApplicationCommandChannelOptions; - } -} diff --git a/src/interactions/slashCommands/mixins/CommandNumberOptionBase.ts b/src/interactions/slashCommands/mixins/CommandNumberOptionBase.ts deleted file mode 100644 index 74b9eb09..00000000 --- a/src/interactions/slashCommands/mixins/CommandNumberOptionBase.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ApplicationCommandOptionWithChoicesBase } from './CommandOptionWithChoices'; - -export abstract class ApplicationCommandNumberOptionBase extends ApplicationCommandOptionWithChoicesBase { - protected maxValue?: number; - protected 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; - - // TODO: Update return type when discord-api-types is updated - public override toJSON() { - return { - ...super.toJSON(), - min_value: this.minValue, - max_value: this.maxValue, - }; - } -} diff --git a/src/interactions/slashCommands/mixins/CommandOptionBase.ts b/src/interactions/slashCommands/mixins/CommandOptionBase.ts deleted file mode 100644 index 4bbcad15..00000000 --- a/src/interactions/slashCommands/mixins/CommandOptionBase.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { APIApplicationCommandOption, ApplicationCommandOptionType } from 'discord-api-types/v9'; -import { validateRequiredParameters, validateRequired } from '../Assertions'; -import type { ToAPIApplicationCommandOptions } from '../SlashCommandBuilder'; -import { SharedNameAndDescription } from './NameAndDescription'; - -export class SlashCommandOptionBase - extends SharedNameAndDescription - implements ToAPIApplicationCommandOptions -{ - public required = false; - public readonly type: OptionType; - - public constructor(type: OptionType) { - super(); - this.type = type; - } - - /** - * 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); - - this.required = required; - - return this; - } - - public toJSON(): APIApplicationCommandOption { - validateRequiredParameters(this.name, this.description, []); - - // Assert that you actually passed a boolean - validateRequired(this.required); - - // TODO: Fix types - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - return { - type: this.type, - name: this.name, - description: this.description, - required: this.required, - } as APIApplicationCommandOption; - } -} diff --git a/src/interactions/slashCommands/mixins/CommandOptions.ts b/src/interactions/slashCommands/mixins/SharedSlashCommandOptions.ts similarity index 96% rename from src/interactions/slashCommands/mixins/CommandOptions.ts rename to src/interactions/slashCommands/mixins/SharedSlashCommandOptions.ts index 5b2c5d46..6d3c4be4 100644 --- a/src/interactions/slashCommands/mixins/CommandOptions.ts +++ b/src/interactions/slashCommands/mixins/SharedSlashCommandOptions.ts @@ -1,5 +1,5 @@ import { assertReturnOfBuilder, validateMaxOptionsLength } from '../Assertions'; -import type { SlashCommandOptionBase } from './CommandOptionBase'; +import type { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase'; import { SlashCommandBooleanOption } from '../options/boolean'; import { SlashCommandChannelOption } from '../options/channel'; import { SlashCommandIntegerOption } from '../options/integer'; @@ -124,7 +124,7 @@ export class SharedSlashCommandOptions { return this._sharedAddOptionMethod(input, SlashCommandNumberOption); } - private _sharedAddOptionMethod( + private _sharedAddOptionMethod( input: | T | Omit diff --git a/src/interactions/slashCommands/options/boolean.ts b/src/interactions/slashCommands/options/boolean.ts index dea8be6b..ae8a1d25 100644 --- a/src/interactions/slashCommands/options/boolean.ts +++ b/src/interactions/slashCommands/options/boolean.ts @@ -1,10 +1,12 @@ -import { ApplicationCommandOptionType } from 'discord-api-types/v9'; -import { SlashCommandOptionBase } from '../mixins/CommandOptionBase'; +import { APIApplicationCommandBooleanOption, ApplicationCommandOptionType } from 'discord-api-types/v9'; +import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase'; -export class SlashCommandBooleanOption extends SlashCommandOptionBase { - public override readonly type = ApplicationCommandOptionType.Boolean as const; +export class SlashCommandBooleanOption extends ApplicationCommandOptionBase { + public readonly type = ApplicationCommandOptionType.Boolean as const; - public constructor() { - super(ApplicationCommandOptionType.Boolean); + public toJSON(): APIApplicationCommandBooleanOption { + this.runRequiredValidations(); + + return { ...this }; } } diff --git a/src/interactions/slashCommands/options/channel.ts b/src/interactions/slashCommands/options/channel.ts index 522b6d46..5bd67f19 100644 --- a/src/interactions/slashCommands/options/channel.ts +++ b/src/interactions/slashCommands/options/channel.ts @@ -1,10 +1,17 @@ -import { ApplicationCommandOptionType } from 'discord-api-types/v9'; -import { ApplicationCommandOptionWithChannelTypesBase } from '../mixins/CommandChannelOptionBase'; +import { APIApplicationCommandChannelOption, ApplicationCommandOptionType } from 'discord-api-types/v9'; +import { mix } from 'ts-mixer'; +import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase'; +import { ApplicationCommandOptionChannelTypesMixin } from '../mixins/ApplicationCommandOptionChannelTypesMixin'; -export class SlashCommandChannelOption extends ApplicationCommandOptionWithChannelTypesBase { +@mix(ApplicationCommandOptionChannelTypesMixin) +export class SlashCommandChannelOption extends ApplicationCommandOptionBase { public override readonly type = ApplicationCommandOptionType.Channel as const; - public constructor() { - super(ApplicationCommandOptionType.Channel); + public toJSON(): APIApplicationCommandChannelOption { + this.runRequiredValidations(); + + return { ...this }; } } + +export interface SlashCommandChannelOption extends ApplicationCommandOptionChannelTypesMixin {} diff --git a/src/interactions/slashCommands/options/integer.ts b/src/interactions/slashCommands/options/integer.ts index 54abd076..f26a1934 100644 --- a/src/interactions/slashCommands/options/integer.ts +++ b/src/interactions/slashCommands/options/integer.ts @@ -1,25 +1,46 @@ -import { ApplicationCommandOptionType } from 'discord-api-types/v9'; -import { ApplicationCommandNumberOptionBase } from '../mixins/CommandNumberOptionBase'; +import { APIApplicationCommandIntegerOption, ApplicationCommandOptionType } from 'discord-api-types/v9'; +import { mix } from 'ts-mixer'; import { z } from 'zod'; +import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin'; +import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase'; +import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin'; const numberValidator = z.number().int().nonnegative(); -export class SlashCommandIntegerOption extends ApplicationCommandNumberOptionBase { - public override readonly type = ApplicationCommandOptionType.Integer as const; - - public constructor() { - super(ApplicationCommandOptionType.Integer); - } +@mix(ApplicationCommandNumericOptionMinMaxValueMixin, ApplicationCommandOptionWithChoicesAndAutocompleteMixin) +export class SlashCommandIntegerOption + extends ApplicationCommandOptionBase + implements ApplicationCommandNumericOptionMinMaxValueMixin +{ + public readonly type = ApplicationCommandOptionType.Integer as const; public setMaxValue(max: number): this { numberValidator.parse(max); - this.maxValue = max; + + Reflect.set(this, 'maxValue', max); + return this; } public setMinValue(min: number): this { numberValidator.parse(min); - this.minValue = min; + + Reflect.set(this, 'minValue', min); + return this; } + + public toJSON(): APIApplicationCommandIntegerOption { + this.runRequiredValidations(); + + if (this.autocomplete && Array.isArray(this.choices) && this.choices.length > 0) { + throw new RangeError('Autocomplete and choices are mutually exclusive to each other.'); + } + + return { ...this }; + } } + +export interface SlashCommandIntegerOption + extends ApplicationCommandNumericOptionMinMaxValueMixin, + ApplicationCommandOptionWithChoicesAndAutocompleteMixin {} diff --git a/src/interactions/slashCommands/options/mentionable.ts b/src/interactions/slashCommands/options/mentionable.ts index cef42765..be2c110e 100644 --- a/src/interactions/slashCommands/options/mentionable.ts +++ b/src/interactions/slashCommands/options/mentionable.ts @@ -1,10 +1,12 @@ -import { ApplicationCommandOptionType } from 'discord-api-types/v9'; -import { SlashCommandOptionBase } from '../mixins/CommandOptionBase'; +import { APIApplicationCommandMentionableOption, ApplicationCommandOptionType } from 'discord-api-types/v9'; +import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase'; -export class SlashCommandMentionableOption extends SlashCommandOptionBase { - public override readonly type = ApplicationCommandOptionType.Mentionable as const; +export class SlashCommandMentionableOption extends ApplicationCommandOptionBase { + public readonly type = ApplicationCommandOptionType.Mentionable as const; - public constructor() { - super(ApplicationCommandOptionType.Mentionable); + public toJSON(): APIApplicationCommandMentionableOption { + this.runRequiredValidations(); + + return { ...this }; } } diff --git a/src/interactions/slashCommands/options/number.ts b/src/interactions/slashCommands/options/number.ts index 3d58058e..6e7cefc5 100644 --- a/src/interactions/slashCommands/options/number.ts +++ b/src/interactions/slashCommands/options/number.ts @@ -1,25 +1,46 @@ -import { ApplicationCommandOptionType } from 'discord-api-types/v9'; -import { ApplicationCommandNumberOptionBase } from '../mixins/CommandNumberOptionBase'; +import { APIApplicationCommandNumberOption, ApplicationCommandOptionType } from 'discord-api-types/v9'; +import { mix } from 'ts-mixer'; import { z } from 'zod'; +import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin'; +import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase'; +import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin'; const numberValidator = z.number().nonnegative(); -export class SlashCommandNumberOption extends ApplicationCommandNumberOptionBase { - public override readonly type = ApplicationCommandOptionType.Number as const; - - public constructor() { - super(ApplicationCommandOptionType.Number); - } +@mix(ApplicationCommandNumericOptionMinMaxValueMixin, ApplicationCommandOptionWithChoicesAndAutocompleteMixin) +export class SlashCommandNumberOption + extends ApplicationCommandOptionBase + implements ApplicationCommandNumericOptionMinMaxValueMixin +{ + public readonly type = ApplicationCommandOptionType.Number as const; public setMaxValue(max: number): this { numberValidator.parse(max); - this.maxValue = max; + + Reflect.set(this, 'maxValue', max); + return this; } public setMinValue(min: number): this { numberValidator.parse(min); - this.minValue = min; + + Reflect.set(this, 'minValue', min); + return this; } + + public toJSON(): APIApplicationCommandNumberOption { + this.runRequiredValidations(); + + if (this.autocomplete && Array.isArray(this.choices) && this.choices.length > 0) { + throw new RangeError('Autocomplete and choices are mutually exclusive to each other.'); + } + + return { ...this }; + } } + +export interface SlashCommandNumberOption + extends ApplicationCommandNumericOptionMinMaxValueMixin, + ApplicationCommandOptionWithChoicesAndAutocompleteMixin {} diff --git a/src/interactions/slashCommands/options/role.ts b/src/interactions/slashCommands/options/role.ts index 9bee6038..5f2a3825 100644 --- a/src/interactions/slashCommands/options/role.ts +++ b/src/interactions/slashCommands/options/role.ts @@ -1,10 +1,12 @@ -import { ApplicationCommandOptionType } from 'discord-api-types/v9'; -import { SlashCommandOptionBase } from '../mixins/CommandOptionBase'; +import { APIApplicationCommandRoleOption, ApplicationCommandOptionType } from 'discord-api-types/v9'; +import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase'; -export class SlashCommandRoleOption extends SlashCommandOptionBase { +export class SlashCommandRoleOption extends ApplicationCommandOptionBase { public override readonly type = ApplicationCommandOptionType.Role as const; - public constructor() { - super(ApplicationCommandOptionType.Role); + public toJSON(): APIApplicationCommandRoleOption { + this.runRequiredValidations(); + + return { ...this }; } } diff --git a/src/interactions/slashCommands/options/string.ts b/src/interactions/slashCommands/options/string.ts index 340a93e2..4f127f0a 100644 --- a/src/interactions/slashCommands/options/string.ts +++ b/src/interactions/slashCommands/options/string.ts @@ -1,10 +1,21 @@ -import { ApplicationCommandOptionType } from 'discord-api-types/v9'; -import { ApplicationCommandOptionWithChoicesBase } from '../mixins/CommandOptionWithChoices'; +import { APIApplicationCommandStringOption, ApplicationCommandOptionType } from 'discord-api-types/v9'; +import { mix } from 'ts-mixer'; +import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase'; +import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin'; -export class SlashCommandStringOption extends ApplicationCommandOptionWithChoicesBase { - public override readonly type = ApplicationCommandOptionType.String as const; +@mix(ApplicationCommandOptionWithChoicesAndAutocompleteMixin) +export class SlashCommandStringOption extends ApplicationCommandOptionBase { + public readonly type = ApplicationCommandOptionType.String as const; - public constructor() { - super(ApplicationCommandOptionType.String); + public toJSON(): APIApplicationCommandStringOption { + this.runRequiredValidations(); + + if (this.autocomplete && Array.isArray(this.choices) && this.choices.length > 0) { + throw new RangeError('Autocomplete and choices are mutually exclusive to each other.'); + } + + return { ...this }; } } + +export interface SlashCommandStringOption extends ApplicationCommandOptionWithChoicesAndAutocompleteMixin {} diff --git a/src/interactions/slashCommands/options/user.ts b/src/interactions/slashCommands/options/user.ts index e7c095f9..0d5327fc 100644 --- a/src/interactions/slashCommands/options/user.ts +++ b/src/interactions/slashCommands/options/user.ts @@ -1,10 +1,12 @@ -import { ApplicationCommandOptionType } from 'discord-api-types/v9'; -import { SlashCommandOptionBase } from '../mixins/CommandOptionBase'; +import { APIApplicationCommandUserOption, ApplicationCommandOptionType } from 'discord-api-types/v9'; +import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase'; -export class SlashCommandUserOption extends SlashCommandOptionBase { - public override readonly type = ApplicationCommandOptionType.User as const; +export class SlashCommandUserOption extends ApplicationCommandOptionBase { + public readonly type = ApplicationCommandOptionType.User as const; - public constructor() { - super(ApplicationCommandOptionType.User); + public toJSON(): APIApplicationCommandUserOption { + this.runRequiredValidations(); + + return { ...this }; } }