Skip to content

Commit

Permalink
refactor: make builders types great again (#10026)
Browse files Browse the repository at this point in the history
* refactor: make builders types great again

* fix: subcommands only type

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
Qjuh and kodiakhq[bot] authored Jan 22, 2024
1 parent fed7f34 commit a0c83a2
Show file tree
Hide file tree
Showing 11 changed files with 270 additions and 242 deletions.
4 changes: 3 additions & 1 deletion packages/builders/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ export * from './interactions/slashCommands/options/user.js';
export * from './interactions/slashCommands/mixins/ApplicationCommandNumericOptionMinMaxValueMixin.js';
export * from './interactions/slashCommands/mixins/ApplicationCommandOptionBase.js';
export * from './interactions/slashCommands/mixins/ApplicationCommandOptionChannelTypesMixin.js';
export * from './interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin.js';
export * from './interactions/slashCommands/mixins/ApplicationCommandOptionWithAutocompleteMixin.js';
export * from './interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesMixin.js';
export * from './interactions/slashCommands/mixins/NameAndDescription.js';
export * from './interactions/slashCommands/mixins/SharedSlashCommandOptions.js';
export * from './interactions/slashCommands/mixins/SharedSubcommands.js';

export * as ContextMenuCommandAssertions from './interactions/contextMenuCommands/Assertions.js';
export * from './interactions/contextMenuCommands/ContextMenuCommandBuilder.js';
Expand Down
170 changes: 12 additions & 158 deletions packages/builders/src/interactions/slashCommands/SlashCommandBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,13 @@
import type {
APIApplicationCommandOption,
LocalizationMap,
Permissions,
RESTPostAPIChatInputApplicationCommandsJSONBody,
} from 'discord-api-types/v10';
import type { APIApplicationCommandOption, LocalizationMap, Permissions } from 'discord-api-types/v10';
import { mix } from 'ts-mixer';
import {
assertReturnOfBuilder,
validateDefaultMemberPermissions,
validateDefaultPermission,
validateLocalizationMap,
validateDMPermission,
validateMaxOptionsLength,
validateRequiredParameters,
validateNSFW,
} from './Assertions.js';
import { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands.js';
import { SharedNameAndDescription } from './mixins/NameAndDescription.js';
import { SharedSlashCommandOptions } from './mixins/SharedSlashCommandOptions.js';
import { SharedSlashCommandSubcommands } from './mixins/SharedSubcommands.js';

/**
* A builder that creates API-compatible JSON data for slash commands.
*/
@mix(SharedSlashCommandOptions, SharedNameAndDescription)
@mix(SharedSlashCommandOptions, SharedNameAndDescription, SharedSlashCommandSubcommands)
export class SlashCommandBuilder {
/**
* The name of this command.
Expand Down Expand Up @@ -52,7 +37,7 @@ export class SlashCommandBuilder {
/**
* Whether this command is enabled by default when the application is added to a guild.
*
* @deprecated Use {@link ContextMenuCommandBuilder.setDefaultMemberPermissions} or {@link ContextMenuCommandBuilder.setDMPermission} instead.
* @deprecated Use {@link SharedSlashCommandSubcommands.setDefaultMemberPermissions} or {@link SharedSlashCommandSubcommands.setDMPermission} instead.
*/
public readonly default_permission: boolean | undefined = undefined;

Expand All @@ -73,158 +58,27 @@ export class SlashCommandBuilder {
* Whether this command is NSFW.
*/
public readonly nsfw: boolean | undefined = undefined;

/**
* Sets whether the command is enabled by default when the application is added to a guild.
*
* @remarks
* If set to `false`, you will have to later `PUT` the permissions for this command.
* @param value - Whether or not to enable this command by default
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
* @deprecated Use {@link SlashCommandBuilder.setDefaultMemberPermissions} or {@link SlashCommandBuilder.setDMPermission} instead.
*/
public setDefaultPermission(value: boolean) {
// Assert the value matches the conditions
validateDefaultPermission(value);

Reflect.set(this, 'default_permission', value);

return this;
}

/**
* Sets the default permissions a member should have in order to run the command.
*
* @remarks
* You can set this to `'0'` to disable the command by default.
* @param permissions - The permissions bit field to set
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
*/
public setDefaultMemberPermissions(permissions: Permissions | bigint | number | null | undefined) {
// Assert the value and parse it
const permissionValue = validateDefaultMemberPermissions(permissions);

Reflect.set(this, 'default_member_permissions', permissionValue);

return this;
}

/**
* Sets if the command is available in direct messages with the application.
*
* @remarks
* By default, commands are visible. This method is only for global commands.
* @param enabled - Whether the command should be enabled in direct messages
* @see {@link https://discord.com/developers/docs/interactions/application-commands#permissions}
*/
public setDMPermission(enabled: boolean | null | undefined) {
// Assert the value matches the conditions
validateDMPermission(enabled);

Reflect.set(this, 'dm_permission', enabled);

return this;
}

/**
* Sets whether this command is NSFW.
*
* @param nsfw - Whether this command is NSFW
*/
public setNSFW(nsfw = true) {
// Assert the value matches the conditions
validateNSFW(nsfw);
Reflect.set(this, 'nsfw', nsfw);
return this;
}

/**
* Adds a new subcommand group to this command.
*
* @param input - A function that returns a subcommand group builder or an already built builder
*/
public addSubcommandGroup(
input:
| SlashCommandSubcommandGroupBuilder
| ((subcommandGroup: SlashCommandSubcommandGroupBuilder) => SlashCommandSubcommandGroupBuilder),
): SlashCommandSubcommandsOnlyBuilder {
const { options } = this;

// First, assert options conditions - we cannot have more than 25 options
validateMaxOptionsLength(options);

// Get the final result
const result = typeof input === 'function' ? input(new SlashCommandSubcommandGroupBuilder()) : input;

assertReturnOfBuilder(result, SlashCommandSubcommandGroupBuilder);

// Push it
options.push(result);

return this;
}

/**
* Adds a new subcommand to this command.
*
* @param input - A function that returns a subcommand builder or an already built builder
*/
public addSubcommand(
input:
| SlashCommandSubcommandBuilder
| ((subcommandGroup: SlashCommandSubcommandBuilder) => SlashCommandSubcommandBuilder),
): SlashCommandSubcommandsOnlyBuilder {
const { options } = this;

// First, assert options conditions - we cannot have more than 25 options
validateMaxOptionsLength(options);

// Get the final result
const result = typeof input === 'function' ? input(new SlashCommandSubcommandBuilder()) : input;

assertReturnOfBuilder(result, SlashCommandSubcommandBuilder);

// Push it
options.push(result);

return this;
}

/**
* Serializes this builder to API-compatible JSON data.
*
* @remarks
* This method runs validations on the data before serializing it.
* As such, it may throw an error if the data is invalid.
*/
public toJSON(): RESTPostAPIChatInputApplicationCommandsJSONBody {
validateRequiredParameters(this.name, this.description, this.options);

validateLocalizationMap(this.name_localizations);
validateLocalizationMap(this.description_localizations);

return {
...this,
options: this.options.map((option) => option.toJSON()),
};
}
}

export interface SlashCommandBuilder extends SharedNameAndDescription, SharedSlashCommandOptions {}
export interface SlashCommandBuilder
extends SharedNameAndDescription,
SharedSlashCommandOptions<SlashCommandOptionsOnlyBuilder>,
SharedSlashCommandSubcommands<SlashCommandSubcommandsOnlyBuilder> {}

/**
* An interface specifically for slash command subcommands.
*/
export interface SlashCommandSubcommandsOnlyBuilder
extends Omit<SlashCommandBuilder, Exclude<keyof SharedSlashCommandOptions, 'options'>> {}
extends SharedNameAndDescription,
SharedSlashCommandSubcommands<SlashCommandSubcommandsOnlyBuilder> {}

/**
* An interface specifically for slash command options.
*/
export interface SlashCommandOptionsOnlyBuilder
extends SharedNameAndDescription,
SharedSlashCommandOptions,
Pick<SlashCommandBuilder, 'toJSON'> {}
SharedSlashCommandOptions<SlashCommandOptionsOnlyBuilder>,
ToAPIApplicationCommandOptions {}

/**
* An interface that ensures the `toJSON()` call will return something
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,6 @@ export class SlashCommandSubcommandBuilder implements ToAPIApplicationCommandOpt
}
}

export interface SlashCommandSubcommandBuilder extends SharedNameAndDescription, SharedSlashCommandOptions<false> {}
export interface SlashCommandSubcommandBuilder
extends SharedNameAndDescription,
SharedSlashCommandOptions<SlashCommandSubcommandBuilder> {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { s } from '@sapphire/shapeshift';
import type { ApplicationCommandOptionType } from 'discord-api-types/v10';

const booleanPredicate = s.boolean;

/**
* This mixin holds choices and autocomplete symbols used for options.
*/
export class ApplicationCommandOptionWithAutocompleteMixin {
/**
* Whether this option utilizes autocomplete.
*/
public readonly autocomplete?: boolean;

/**
* The type of this option.
*
* @privateRemarks Since this is present and this is a mixin, this is needed.
*/
public readonly type!: ApplicationCommandOptionType;

/**
* Whether this option uses autocomplete.
*
* @param autocomplete - Whether this option should use autocomplete
*/
public setAutocomplete(autocomplete: boolean): this {
// Assert that you actually passed a boolean
booleanPredicate.parse(autocomplete);

if (autocomplete && 'choices' in this && Array.isArray(this.choices) && this.choices.length > 0) {
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
}

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

return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,16 @@ const choicesPredicate = s.object({
name_localizations: localizationMapPredicate,
value: s.union(stringPredicate, numberPredicate),
}).array;
const booleanPredicate = s.boolean;

/**
* This mixin holds choices and autocomplete symbols used for options.
*/
export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin<ChoiceType extends number | string> {
export class ApplicationCommandOptionWithChoicesMixin<ChoiceType extends number | string> {
/**
* The choices of this option.
*/
public readonly choices?: APIApplicationCommandOptionChoice<ChoiceType>[];

/**
* Whether this option utilizes autocomplete.
*/
public readonly autocomplete?: boolean;

/**
* The type of this option.
*
Expand All @@ -38,7 +32,7 @@ export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin<ChoiceType
* @param choices - The choices to add
*/
public addChoices(...choices: APIApplicationCommandOptionChoice<ChoiceType>[]): this {
if (choices.length > 0 && this.autocomplete) {
if (choices.length > 0 && 'autocomplete' in this && this.autocomplete) {
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
}

Expand Down Expand Up @@ -70,7 +64,7 @@ export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin<ChoiceType
* @param choices - The choices to set
*/
public setChoices<Input extends APIApplicationCommandOptionChoice<ChoiceType>[]>(...choices: Input): this {
if (choices.length > 0 && this.autocomplete) {
if (choices.length > 0 && 'autocomplete' in this && this.autocomplete) {
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
}

Expand All @@ -81,22 +75,4 @@ export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin<ChoiceType

return this;
}

/**
* Whether this option uses autocomplete.
*
* @param autocomplete - Whether this option should use autocomplete
*/
public setAutocomplete(autocomplete: boolean): this {
// Assert that you actually passed a boolean
booleanPredicate.parse(autocomplete);

if (autocomplete && Array.isArray(this.choices) && this.choices.length > 0) {
throw new RangeError('Autocomplete and choices are mutually exclusive to each other.');
}

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

return this;
}
}
Loading

0 comments on commit a0c83a2

Please sign in to comment.