Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 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
5 changes: 3 additions & 2 deletions packages/builders/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,12 @@
"dependencies": {
"@discordjs/formatters": "workspace:^",
"@discordjs/util": "workspace:^",
"@sapphire/shapeshift": "^3.9.7",
"discord-api-types": "0.37.83",
"fast-deep-equal": "^3.1.3",
"ts-mixer": "^6.0.4",
"tslib": "^2.6.2"
"tslib": "^2.6.2",
"zod": "^3.22.4",
"zod-validation-error": "^3.0.0"
},
"devDependencies": {
"@discordjs/api-extractor": "workspace:^",
Expand Down
113 changes: 58 additions & 55 deletions packages/builders/src/components/Assertions.ts
Original file line number Diff line number Diff line change
@@ -1,80 +1,83 @@
import { s } from '@sapphire/shapeshift';
import { ButtonStyle, ChannelType, type APIMessageComponentEmoji } from 'discord-api-types/v10';
import { isValidationEnabled } from '../util/validation.js';
import { z } from 'zod';
import { parse } from '../util/validation.js';
import { StringSelectMenuOptionBuilder } from './selectMenu/StringSelectMenuOption.js';

export const customIdValidator = s.string
.lengthGreaterThanOrEqual(1)
.lengthLessThanOrEqual(100)
.setValidationEnabled(isValidationEnabled);
export const customIdValidator = z.string().min(1).max(100);

export const emojiValidator = s
export const emojiValidator = z
.object({
id: s.string,
name: s.string,
animated: s.boolean,
id: z.string(),
name: z.string(),
animated: z.boolean(),
})
.partial.strict.setValidationEnabled(isValidationEnabled);
.partial()
.strict();

export const disabledValidator = s.boolean;
export const disabledValidator = z.boolean();

export const buttonLabelValidator = s.string
.lengthGreaterThanOrEqual(1)
.lengthLessThanOrEqual(80)
.setValidationEnabled(isValidationEnabled);
export const buttonLabelValidator = z.string().min(1).max(80);

export const buttonStyleValidator = s.nativeEnum(ButtonStyle);
export const buttonStyleValidator = z.union([
z.nativeEnum(ButtonStyle),
z
.enum(
Object.values(ButtonStyle).filter((value) => typeof value === 'string') as [
keyof typeof ButtonStyle,
...(keyof typeof ButtonStyle)[],
],
)
.transform((key) => ButtonStyle[key]),
]);

export const placeholderValidator = s.string.lengthLessThanOrEqual(150).setValidationEnabled(isValidationEnabled);
export const minMaxValidator = s.number.int
.greaterThanOrEqual(0)
.lessThanOrEqual(25)
.setValidationEnabled(isValidationEnabled);
export const placeholderValidator = z.string().max(150);
export const minMaxValidator = z.number().int().gte(0).lte(25);

export const labelValueDescriptionValidator = s.string
.lengthGreaterThanOrEqual(1)
.lengthLessThanOrEqual(100)
.setValidationEnabled(isValidationEnabled);
export const labelValueDescriptionValidator = z.string().min(1).max(100);

export const jsonOptionValidator = s
.object({
label: labelValueDescriptionValidator,
value: labelValueDescriptionValidator,
description: labelValueDescriptionValidator.optional,
emoji: emojiValidator.optional,
default: s.boolean.optional,
})
.setValidationEnabled(isValidationEnabled);
export const jsonOptionValidator = z.object({
label: labelValueDescriptionValidator,
value: labelValueDescriptionValidator,
description: labelValueDescriptionValidator.optional(),
emoji: emojiValidator.optional(),
default: z.boolean().optional(),
});

export const optionValidator = s.instance(StringSelectMenuOptionBuilder).setValidationEnabled(isValidationEnabled);
export const optionValidator = z.instanceof(StringSelectMenuOptionBuilder);

export const optionsValidator = optionValidator.array
.lengthGreaterThanOrEqual(0)
.setValidationEnabled(isValidationEnabled);
export const optionsLengthValidator = s.number.int
.greaterThanOrEqual(0)
.lessThanOrEqual(25)
.setValidationEnabled(isValidationEnabled);
export const optionsValidator = optionValidator.array().min(0);
export const optionsLengthValidator = z.number().int().gte(0).lte(25);

export function validateRequiredSelectMenuParameters(options: StringSelectMenuOptionBuilder[], customId?: string) {
customIdValidator.parse(customId);
optionsValidator.parse(options);
parse(customIdValidator, customId);
parse(optionsValidator, options);
}

export const defaultValidator = s.boolean;
export const defaultValidator = z.boolean();

export function validateRequiredSelectMenuOptionParameters(label?: string, value?: string) {
labelValueDescriptionValidator.parse(label);
labelValueDescriptionValidator.parse(value);
parse(labelValueDescriptionValidator, label);
parse(labelValueDescriptionValidator, value);
}

export const channelTypesValidator = s.nativeEnum(ChannelType).array.setValidationEnabled(isValidationEnabled);

export const urlValidator = s.string
.url({
allowedProtocols: ['http:', 'https:', 'discord:'],
})
.setValidationEnabled(isValidationEnabled);
export const channelTypesValidator = z
.union([
z.nativeEnum(ChannelType),
z
.enum(
Object.values(ChannelType).filter((value) => typeof value === 'string') as [
keyof typeof ChannelType,
...(keyof typeof ChannelType)[],
],
)
.transform((key) => ChannelType[key]),
])
.array();

export const urlValidator = z
.string()
.url()
.regex(/^(?<proto>https?|discord):\/\//);

export function validateRequiredButtonParameters(
style?: ButtonStyle,
Expand Down
13 changes: 7 additions & 6 deletions packages/builders/src/components/button/Button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type APIButtonComponentWithCustomId,
type ButtonStyle,
} from 'discord-api-types/v10';
import { parse } from '../../util/validation.js';
import {
buttonLabelValidator,
buttonStyleValidator,
Expand Down Expand Up @@ -59,7 +60,7 @@ export class ButtonBuilder extends ComponentBuilder<APIButtonComponent> {
* @param style - The style to use
*/
public setStyle(style: ButtonStyle) {
this.data.style = buttonStyleValidator.parse(style);
this.data.style = parse(buttonStyleValidator, style);
return this;
}

Expand All @@ -72,7 +73,7 @@ export class ButtonBuilder extends ComponentBuilder<APIButtonComponent> {
* @param url - The URL to use
*/
public setURL(url: string) {
(this.data as APIButtonComponentWithURL).url = urlValidator.parse(url);
(this.data as APIButtonComponentWithURL).url = parse(urlValidator, url);
return this;
}

Expand All @@ -84,7 +85,7 @@ export class ButtonBuilder extends ComponentBuilder<APIButtonComponent> {
* @param customId - The custom id to use
*/
public setCustomId(customId: string) {
(this.data as APIButtonComponentWithCustomId).custom_id = customIdValidator.parse(customId);
(this.data as APIButtonComponentWithCustomId).custom_id = parse(customIdValidator, customId);
return this;
}

Expand All @@ -94,7 +95,7 @@ export class ButtonBuilder extends ComponentBuilder<APIButtonComponent> {
* @param emoji - The emoji to use
*/
public setEmoji(emoji: APIMessageComponentEmoji) {
this.data.emoji = emojiValidator.parse(emoji);
this.data.emoji = parse(emojiValidator, emoji);
return this;
}

Expand All @@ -104,7 +105,7 @@ export class ButtonBuilder extends ComponentBuilder<APIButtonComponent> {
* @param disabled - Whether to disable this button
*/
public setDisabled(disabled = true) {
this.data.disabled = disabledValidator.parse(disabled);
this.data.disabled = parse(disabledValidator, disabled);
return this;
}

Expand All @@ -114,7 +115,7 @@ export class ButtonBuilder extends ComponentBuilder<APIButtonComponent> {
* @param label - The label to use
*/
public setLabel(label: string) {
this.data.label = buttonLabelValidator.parse(label);
this.data.label = parse(buttonLabelValidator, label);
return this;
}

Expand Down
13 changes: 7 additions & 6 deletions packages/builders/src/components/selectMenu/BaseSelectMenu.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { APISelectMenuComponent } from 'discord-api-types/v10';
import { parse } from '../../util/validation.js';
import { customIdValidator, disabledValidator, minMaxValidator, placeholderValidator } from '../Assertions.js';
import { ComponentBuilder } from '../Component.js';

Expand All @@ -16,7 +17,7 @@ export abstract class BaseSelectMenuBuilder<
* @param placeholder - The placeholder to use
*/
public setPlaceholder(placeholder: string) {
this.data.placeholder = placeholderValidator.parse(placeholder);
this.data.placeholder = parse(placeholderValidator, placeholder);
return this;
}

Expand All @@ -26,7 +27,7 @@ export abstract class BaseSelectMenuBuilder<
* @param minValues - The minimum values that must be selected
*/
public setMinValues(minValues: number) {
this.data.min_values = minMaxValidator.parse(minValues);
this.data.min_values = parse(minMaxValidator, minValues);
return this;
}

Expand All @@ -36,7 +37,7 @@ export abstract class BaseSelectMenuBuilder<
* @param maxValues - The maximum values that must be selected
*/
public setMaxValues(maxValues: number) {
this.data.max_values = minMaxValidator.parse(maxValues);
this.data.max_values = parse(minMaxValidator, maxValues);
return this;
}

Expand All @@ -46,7 +47,7 @@ export abstract class BaseSelectMenuBuilder<
* @param customId - The custom id to use
*/
public setCustomId(customId: string) {
this.data.custom_id = customIdValidator.parse(customId);
this.data.custom_id = parse(customIdValidator, customId);
return this;
}

Expand All @@ -56,15 +57,15 @@ export abstract class BaseSelectMenuBuilder<
* @param disabled - Whether this select menu is disabled
*/
public setDisabled(disabled = true) {
this.data.disabled = disabledValidator.parse(disabled);
this.data.disabled = parse(disabledValidator, disabled);
return this;
}

/**
* {@inheritDoc ComponentBuilder.toJSON}
*/
public toJSON(): SelectMenuType {
customIdValidator.parse(this.data.custom_id);
parse(customIdValidator, this.data.custom_id);
return {
...this.data,
} as SelectMenuType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SelectMenuDefaultValueType,
} from 'discord-api-types/v10';
import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js';
import { parse } from '../../util/validation.js';
import { channelTypesValidator, customIdValidator, optionsLengthValidator } from '../Assertions.js';
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';

Expand Down Expand Up @@ -48,7 +49,7 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
public addChannelTypes(...types: RestOrArray<ChannelType>) {
const normalizedTypes = normalizeArray(types);
this.data.channel_types ??= [];
this.data.channel_types.push(...channelTypesValidator.parse(normalizedTypes));
this.data.channel_types.push(...parse(channelTypesValidator, normalizedTypes));
return this;
}

Expand All @@ -60,7 +61,7 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
public setChannelTypes(...types: RestOrArray<ChannelType>) {
const normalizedTypes = normalizeArray(types);
this.data.channel_types ??= [];
this.data.channel_types.splice(0, this.data.channel_types.length, ...channelTypesValidator.parse(normalizedTypes));
this.data.channel_types.splice(0, this.data.channel_types.length, ...parse(channelTypesValidator, normalizedTypes));
return this;
}

Expand All @@ -71,7 +72,7 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
*/
public addDefaultChannels(...channels: RestOrArray<Snowflake>) {
const normalizedValues = normalizeArray(channels);
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
parse(optionsLengthValidator, (this.data.default_values?.length ?? 0) + normalizedValues.length);
this.data.default_values ??= [];

this.data.default_values.push(
Expand All @@ -91,7 +92,7 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
*/
public setDefaultChannels(...channels: RestOrArray<Snowflake>) {
const normalizedValues = normalizeArray(channels);
optionsLengthValidator.parse(normalizedValues.length);
parse(optionsLengthValidator, normalizedValues.length);

this.data.default_values = normalizedValues.map((id) => ({
id,
Expand All @@ -105,7 +106,7 @@ export class ChannelSelectMenuBuilder extends BaseSelectMenuBuilder<APIChannelSe
* {@inheritDoc BaseSelectMenuBuilder.toJSON}
*/
public override toJSON(): APIChannelSelectComponent {
customIdValidator.parse(this.data.custom_id);
parse(customIdValidator, this.data.custom_id);

return {
...this.data,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SelectMenuDefaultValueType,
} from 'discord-api-types/v10';
import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js';
import { parse } from '../../util/validation.js';
import { optionsLengthValidator } from '../Assertions.js';
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';

Expand Down Expand Up @@ -46,7 +47,7 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMenti
*/
public addDefaultRoles(...roles: RestOrArray<Snowflake>) {
const normalizedValues = normalizeArray(roles);
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
parse(optionsLengthValidator, (this.data.default_values?.length ?? 0) + normalizedValues.length);
this.data.default_values ??= [];

this.data.default_values.push(
Expand All @@ -66,7 +67,7 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMenti
*/
public addDefaultUsers(...users: RestOrArray<Snowflake>) {
const normalizedValues = normalizeArray(users);
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
parse(optionsLengthValidator, (this.data.default_values?.length ?? 0) + normalizedValues.length);
this.data.default_values ??= [];

this.data.default_values.push(
Expand All @@ -91,7 +92,7 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMenti
>
) {
const normalizedValues = normalizeArray(values);
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
parse(optionsLengthValidator, (this.data.default_values?.length ?? 0) + normalizedValues.length);
this.data.default_values ??= [];
this.data.default_values.push(...normalizedValues);
return this;
Expand All @@ -109,7 +110,7 @@ export class MentionableSelectMenuBuilder extends BaseSelectMenuBuilder<APIMenti
>
) {
const normalizedValues = normalizeArray(values);
optionsLengthValidator.parse(normalizedValues.length);
parse(optionsLengthValidator, normalizedValues.length);
this.data.default_values = normalizedValues;
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
SelectMenuDefaultValueType,
} from 'discord-api-types/v10';
import { type RestOrArray, normalizeArray } from '../../util/normalizeArray.js';
import { parse } from '../../util/validation.js';
import { optionsLengthValidator } from '../Assertions.js';
import { BaseSelectMenuBuilder } from './BaseSelectMenu.js';

Expand Down Expand Up @@ -45,7 +46,7 @@ export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder<APIRoleSelectCo
*/
public addDefaultRoles(...roles: RestOrArray<Snowflake>) {
const normalizedValues = normalizeArray(roles);
optionsLengthValidator.parse((this.data.default_values?.length ?? 0) + normalizedValues.length);
parse(optionsLengthValidator, (this.data.default_values?.length ?? 0) + normalizedValues.length);
this.data.default_values ??= [];

this.data.default_values.push(
Expand All @@ -65,7 +66,7 @@ export class RoleSelectMenuBuilder extends BaseSelectMenuBuilder<APIRoleSelectCo
*/
public setDefaultRoles(...roles: RestOrArray<Snowflake>) {
const normalizedValues = normalizeArray(roles);
optionsLengthValidator.parse(normalizedValues.length);
parse(optionsLengthValidator, normalizedValues.length);

this.data.default_values = normalizedValues.map((id) => ({
id,
Expand Down
Loading