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: replace zod with shapeshift #7547

Merged
merged 21 commits into from
Apr 9, 2022
Merged
Show file tree
Hide file tree
Changes from 11 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
8 changes: 4 additions & 4 deletions packages/builders/__tests__/messages/embed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,10 @@ describe('Embed', () => {
expect(embed.toJSON()).toStrictEqual({ url: undefined });
});

test('GIVEN an embed with an invalid URL THEN throws error', () => {
test.each(['owo', 'discord://user'])('GIVEN an embed with an invalid URL THEN throws error', (input) => {
const embed = new EmbedBuilder();

expect(() => embed.setURL('owo')).toThrowError();
expect(() => embed.setURL(input)).toThrowError();
});
});

Expand Down Expand Up @@ -325,7 +325,7 @@ describe('Embed', () => {
embed.addFields({ name: 'foo', value: 'bar' });

expect(embed.toJSON()).toStrictEqual({
fields: [{ name: 'foo', value: 'bar' }],
fields: [{ name: 'foo', value: 'bar', inline: undefined }],
});
});

Expand All @@ -334,7 +334,7 @@ describe('Embed', () => {
embed.addFields({ name: 'foo', value: 'bar' }, { name: 'foo', value: 'baz' });

expect(embed.spliceFields(0, 1).toJSON()).toStrictEqual({
fields: [{ name: 'foo', value: 'baz' }],
fields: [{ name: 'foo', value: 'baz', inline: undefined }],
});
});

Expand Down
4 changes: 2 additions & 2 deletions packages/builders/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@
},
"homepage": "https://discord.js.org",
"dependencies": {
"@sapphire/shapeshift": "^2.0.0",
"@sindresorhus/is": "^4.4.0",
"discord-api-types": "^0.27.3",
"fast-deep-equal": "^3.1.3",
"ts-mixer": "^6.0.0",
"tslib": "^2.3.1",
"zod": "^3.11.6"
"tslib": "^2.3.1"
},
"devDependencies": {
"@babel/core": "^7.17.2",
Expand Down
37 changes: 18 additions & 19 deletions packages/builders/src/components/Assertions.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,42 @@
import { APIMessageComponentEmoji, ButtonStyle } from 'discord-api-types/v9';
import { z } from 'zod';
import { s } from '@sapphire/shapeshift';
import type { SelectMenuOptionBuilder } from './selectMenu/SelectMenuOption';

export const customIdValidator = z.string().min(1).max(100);
export const customIdValidator = s.string.lengthGe(1).lengthLe(100);

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

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

export const buttonLabelValidator = z.string().nonempty().max(80);
export const buttonLabelValidator = s.string.lengthGe(1).lengthLe(80);

export const buttonStyleValidator = z.number().int().min(ButtonStyle.Primary).max(ButtonStyle.Link);
export const buttonStyleValidator = s.nativeEnum(ButtonStyle);

export const placeholderValidator = z.string().max(150);
export const minMaxValidator = z.number().int().min(0).max(25);
export const placeholderValidator = s.string.lengthLe(150);
export const minMaxValidator = s.number.int.ge(0).le(25);

export const optionsValidator = z.object({}).array().nonempty();
export const optionsValidator = s.object({}).array.lengthGe(1);
imranbarbhuiya marked this conversation as resolved.
Show resolved Hide resolved

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

export const labelValueValidator = z.string().min(1).max(100);
export const defaultValidator = z.boolean();
export const labelValueValidator = s.string.lengthGe(1).lengthLe(100);
export const defaultValidator = s.boolean;

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

export const urlValidator = z.string().url();
export const urlValidator = s.string.url({
allowedProtocols: ['http:', 'https:', 'discord:'],
});

export function validateRequiredButtonParameters(
style?: ButtonStyle,
Expand Down
16 changes: 8 additions & 8 deletions packages/builders/src/components/textInput/Assertions.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { TextInputStyle } from 'discord-api-types/v9';
import { z } from 'zod';
import { s } from '@sapphire/shapeshift';
import { customIdValidator } from '../Assertions';

export const textInputStyleValidator = z.nativeEnum(TextInputStyle);
export const minLengthValidator = z.number().int().min(0).max(4000);
export const maxLengthValidator = z.number().int().min(1).max(4000);
export const requiredValidator = z.boolean();
export const valueValidator = z.string().max(4000);
export const placeholderValidator = z.string().max(100);
export const labelValidator = z.string().min(1).max(45);
export const textInputStyleValidator = s.nativeEnum(TextInputStyle);
export const minLengthValidator = s.number.int.ge(0).le(4000);
export const maxLengthValidator = s.number.int.ge(1).le(4000);
export const requiredValidator = s.boolean;
export const valueValidator = s.string.lengthLe(4000);
export const placeholderValidator = s.string.lengthLe(100);
export const labelValidator = s.string.lengthGe(1).lengthLe(45);

export function validateRequiredParameters(customId?: string, style?: TextInputStyle, label?: string) {
customIdValidator.parse(customId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { z } from 'zod';
import { s } from '@sapphire/shapeshift';
import { ApplicationCommandType } from 'discord-api-types/v9';
import type { ContextMenuCommandType } from './ContextMenuCommandBuilder';

const namePredicate = z
.string()
.min(1)
.max(32)
const namePredicate = s.string
.lengthGe(1)
.lengthLe(32)
.regex(/^( *[\p{L}\p{N}_-]+ *)+$/u);

const typePredicate = z.union([z.literal(ApplicationCommandType.User), z.literal(ApplicationCommandType.Message)]);
const typePredicate = s.union(s.literal(ApplicationCommandType.User), s.literal(ApplicationCommandType.Message));

const booleanPredicate = z.boolean();
const booleanPredicate = s.boolean;

export function validateDefaultPermission(value: unknown): asserts value is boolean {
booleanPredicate.parse(value);
Expand Down
6 changes: 3 additions & 3 deletions packages/builders/src/interactions/modals/Assertions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { z } from 'zod';
import { s } from '@sapphire/shapeshift';
import { ActionRowBuilder, type ModalActionRowComponentBuilder } from '../..';
import { customIdValidator } from '../../components/Assertions';

export const titleValidator = z.string().min(1).max(45);
export const componentsValidator = z.array(z.instanceof(ActionRowBuilder)).min(1);
export const titleValidator = s.string.lengthGe(1).lengthLe(45);
export const componentsValidator = s.instance(ActionRowBuilder).array.lengthGe(1);

export function validateRequiredParameters(
customId?: string,
Expand Down
17 changes: 8 additions & 9 deletions packages/builders/src/interactions/slashCommands/Assertions.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
import is from '@sindresorhus/is';
import type { APIApplicationCommandOptionChoice } from 'discord-api-types/v9';
import { z } from 'zod';
import { s } from '@sapphire/shapeshift';
import type { ApplicationCommandOptionBase } from './mixins/ApplicationCommandOptionBase';
import type { ToAPIApplicationCommandOptions } from './SlashCommandBuilder';
import type { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from './SlashCommandSubcommands';

const namePredicate = z
.string()
.min(1)
.max(32)
const namePredicate = s.string
.lengthGe(1)
.lengthLe(32)
.regex(/^[\P{Lu}\p{N}_-]+$/u);

export function validateName(name: unknown): asserts name is string {
namePredicate.parse(name);
}

const descriptionPredicate = z.string().min(1).max(100);
const descriptionPredicate = s.string.lengthGe(1).lengthLe(100);

export function validateDescription(description: unknown): asserts description is string {
descriptionPredicate.parse(description);
}

const maxArrayLengthPredicate = z.unknown().array().max(25);
const maxArrayLengthPredicate = s.unknown.array.lengthLe(25);

export function validateMaxOptionsLength(options: unknown): asserts options is ToAPIApplicationCommandOptions[] {
maxArrayLengthPredicate.parse(options);
Expand All @@ -42,7 +41,7 @@ export function validateRequiredParameters(
validateMaxOptionsLength(options);
}

const booleanPredicate = z.boolean();
const booleanPredicate = s.boolean;

export function validateDefaultPermission(value: unknown): asserts value is boolean {
booleanPredicate.parse(value);
Expand All @@ -52,7 +51,7 @@ export function validateRequired(required: unknown): asserts required is boolean
booleanPredicate.parse(required);
}

const choicesLengthPredicate = z.number().lte(25);
const choicesLengthPredicate = s.number.le(25);

export function validateChoicesLength(amountAdding: number, choices?: APIApplicationCommandOptionChoice[]): void {
choicesLengthPredicate.parse((choices?.length ?? 0) + amountAdding);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ChannelType } from 'discord-api-types/v9';
import { z, ZodLiteral } from 'zod';
import { s } from '@sapphire/shapeshift';

// Only allow valid channel types to be used. (This can't be dynamic because const enums are erased at runtime)
const allowedChannelTypes = [
Expand All @@ -16,15 +16,7 @@ const allowedChannelTypes = [

export type ApplicationCommandOptionAllowedChannelTypes = typeof allowedChannelTypes[number];

const channelTypesPredicate = z.array(
z.union(
allowedChannelTypes.map((type) => z.literal(type)) as [
ZodLiteral<ChannelType>,
ZodLiteral<ChannelType>,
...ZodLiteral<ChannelType>[]
],
),
);
const channelTypesPredicate = s.array(s.union(...allowedChannelTypes.map((type) => s.literal(type))));
imranbarbhuiya marked this conversation as resolved.
Show resolved Hide resolved

export class ApplicationCommandOptionChannelTypesMixin {
public readonly channel_types?: ApplicationCommandOptionAllowedChannelTypes[];
Expand All @@ -39,9 +31,7 @@ export class ApplicationCommandOptionChannelTypesMixin {
Reflect.set(this, 'channel_types', []);
}

channelTypesPredicate.parse(channelTypes);

this.channel_types!.push(...channelTypes);
this.channel_types!.push(...channelTypesPredicate.parse(channelTypes));

return this;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { APIApplicationCommandOptionChoice, ApplicationCommandOptionType } from 'discord-api-types/v9';
import { z } from 'zod';
import { s } from '@sapphire/shapeshift';
import { validateChoicesLength } from '../Assertions';

const stringPredicate = z.string().min(1).max(100);
const numberPredicate = z.number().gt(-Infinity).lt(Infinity);
const choicesPredicate = z
.object({ name: stringPredicate, value: z.union([stringPredicate, numberPredicate]) })
.array();
const booleanPredicate = z.boolean();
const stringPredicate = s.string.lengthGe(1).lengthLe(100);
const numberPredicate = s.number.gt(-Infinity).lt(Infinity);
const choicesPredicate = s.object({ name: stringPredicate, value: s.union(stringPredicate, numberPredicate) }).array;
const booleanPredicate = s.boolean;

export class ApplicationCommandOptionWithChoicesAndAutocompleteMixin<T extends string | number> {
public readonly choices?: APIApplicationCommandOptionChoice<T>[];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { APIApplicationCommandIntegerOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
import { mix } from 'ts-mixer';
import { z } from 'zod';
import { s } from '@sapphire/shapeshift';
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin';
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin';

const numberValidator = z.number().int();
const numberValidator = s.number.int;

@mix(ApplicationCommandNumericOptionMinMaxValueMixin, ApplicationCommandOptionWithChoicesAndAutocompleteMixin)
export class SlashCommandIntegerOption
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { APIApplicationCommandNumberOption, ApplicationCommandOptionType } from 'discord-api-types/v9';
import { mix } from 'ts-mixer';
import { z } from 'zod';
import { s } from '@sapphire/shapeshift';
import { ApplicationCommandNumericOptionMinMaxValueMixin } from '../mixins/ApplicationCommandNumericOptionMinMaxValueMixin';
import { ApplicationCommandOptionBase } from '../mixins/ApplicationCommandOptionBase';
import { ApplicationCommandOptionWithChoicesAndAutocompleteMixin } from '../mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin';

const numberValidator = z.number();
const numberValidator = s.number;

@mix(ApplicationCommandNumericOptionMinMaxValueMixin, ApplicationCommandOptionWithChoicesAndAutocompleteMixin)
export class SlashCommandNumberOption
Expand Down
41 changes: 20 additions & 21 deletions packages/builders/src/messages/embed/Assertions.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,42 @@
import type { APIEmbedField } from 'discord-api-types/v9';
import { z } from 'zod';
import { s } from '@sapphire/shapeshift';

export const fieldNamePredicate = z.string().min(1).max(256);
export const fieldNamePredicate = s.string.lengthGe(1).lengthLe(256);

export const fieldValuePredicate = z.string().min(1).max(1024);
export const fieldValuePredicate = s.string.lengthGe(1).lengthLe(1024);

export const fieldInlinePredicate = z.boolean().optional();
export const fieldInlinePredicate = s.boolean.optional;

export const embedFieldPredicate = z.object({
export const embedFieldPredicate = s.object({
name: fieldNamePredicate,
value: fieldValuePredicate,
inline: fieldInlinePredicate,
});

export const embedFieldsArrayPredicate = embedFieldPredicate.array();
export const embedFieldsArrayPredicate = embedFieldPredicate.array;

export const fieldLengthPredicate = z.number().lte(25);
export const fieldLengthPredicate = s.number.le(25);

export function validateFieldLength(amountAdding: number, fields?: APIEmbedField[]): void {
fieldLengthPredicate.parse((fields?.length ?? 0) + amountAdding);
}

export const authorNamePredicate = fieldNamePredicate.nullable();
export const authorNamePredicate = fieldNamePredicate.nullable;

export const urlPredicate = z.string().url().nullish();
export const urlPredicate = s.string.url({
allowedProtocols: ['http:', 'https:'],
}).nullish;

export const RGBPredicate = z.number().int().gte(0).lte(255);
export const colorPredicate = z
.number()
.int()
.gte(0)
.lte(0xffffff)
.nullable()
.or(z.tuple([RGBPredicate, RGBPredicate, RGBPredicate]));
export const RGBPredicate = s.number.int.ge(0).le(255);
export const colorPredicate = s.number.int
.ge(0)
.le(0xffffff)
.or(s.tuple([RGBPredicate, RGBPredicate, RGBPredicate])).nullable;

export const descriptionPredicate = z.string().min(1).max(4096).nullable();
export const descriptionPredicate = s.string.lengthGe(1).lengthLe(4096).nullable;

export const footerTextPredicate = z.string().min(1).max(2048).nullable();
export const footerTextPredicate = s.string.lengthGe(1).lengthLe(2048).nullable;

export const timestampPredicate = z.union([z.number(), z.date()]).nullable();
export const timestampPredicate = s.union(s.number, s.date).nullable;

export const titlePredicate = fieldNamePredicate.nullable();
export const titlePredicate = fieldNamePredicate.nullable;