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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/builders/package.json
Expand Up @@ -66,11 +66,12 @@
"dependencies": {
"@discordjs/formatters": "workspace:^",
"@discordjs/util": "workspace:^",
"@sapphire/shapeshift": "^3.9.6",
"discord-api-types": "0.37.61",
"fast-deep-equal": "^3.1.3",
"ts-mixer": "^6.0.3",
"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
89 changes: 35 additions & 54 deletions packages/builders/src/components/Assertions.ts
@@ -1,80 +1,61 @@
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.nativeEnum(ButtonStyle);

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 channelTypesValidator = z.nativeEnum(ChannelType).array();

export const urlValidator = s.string
.url({
allowedProtocols: ['http:', 'https:', 'discord:'],
})
.setValidationEnabled(isValidationEnabled);
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
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
@@ -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
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
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.slice();
return this;
}
Expand Down
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