Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: use zod in builders #10117

Closed
wants to merge 14 commits into from
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.90",
"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 @@ -8,6 +8,7 @@ import {
type ButtonStyle,
type Snowflake,
} from 'discord-api-types/v10';
import { parse } from '../../util/validation.js';
import {
buttonLabelValidator,
buttonStyleValidator,
Expand Down Expand Up @@ -61,7 +62,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 @@ -74,7 +75,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 @@ -86,7 +87,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 @@ -107,7 +108,7 @@ export class ButtonBuilder extends ComponentBuilder<APIButtonComponent> {
* @param emoji - The emoji to use
*/
public setEmoji(emoji: APIMessageComponentEmoji) {
(this.data as Exclude<APIButtonComponent, APIButtonComponentWithSKUId>).emoji = emojiValidator.parse(emoji);
(this.data as Exclude<APIButtonComponent, APIButtonComponentWithSKUId>).emoji = parse(emojiValidator, emoji);
return this;
}

Expand All @@ -117,7 +118,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 @@ -127,7 +128,7 @@ export class ButtonBuilder extends ComponentBuilder<APIButtonComponent> {
* @param label - The label to use
*/
public setLabel(label: string) {
(this.data as Exclude<APIButtonComponent, APIButtonComponentWithSKUId>).label = buttonLabelValidator.parse(label);
(this.data as Exclude<APIButtonComponent, APIButtonComponentWithSKUId>).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
Loading