Skip to content

Commit

Permalink
refactor: replace zod with shapeshift (#7547)
Browse files Browse the repository at this point in the history
  • Loading branch information
imranbarbhuiya committed Apr 9, 2022
1 parent 3f3e432 commit 3c0bbac
Show file tree
Hide file tree
Showing 14 changed files with 105 additions and 116 deletions.
8 changes: 4 additions & 4 deletions packages/builders/__tests__/messages/embed.test.ts
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
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.29.0",
"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
55 changes: 27 additions & 28 deletions packages/builders/src/components/Assertions.ts
@@ -1,56 +1,55 @@
import { APIMessageComponentEmoji, ButtonStyle } from 'discord-api-types/v10';
import { z } from 'zod';
import { s } from '@sapphire/shapeshift';
import type { SelectMenuOptionBuilder } from './selectMenu/SelectMenuOption';
import { UnsafeSelectMenuOptionBuilder } from './selectMenu/UnsafeSelectMenuOption';

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 labelValueDescriptionValidator = z.string().min(1).max(100);
export const optionValidator = z.union([
z.object({
export const labelValueDescriptionValidator = s.string.lengthGe(1).lengthLe(100);
export const optionValidator = s.union(
s.object({
label: labelValueDescriptionValidator,
value: labelValueDescriptionValidator,
description: labelValueDescriptionValidator.optional(),
emoji: emojiValidator.optional(),
default: z.boolean().optional(),
description: labelValueDescriptionValidator.optional,
emoji: emojiValidator.optional,
default: s.boolean.optional,
}),
z.instanceof(UnsafeSelectMenuOptionBuilder),
]);
export const optionsValidator = optionValidator.array().nonempty();
export const optionsLengthValidator = z.number().int().min(0).max(25);
s.instance(UnsafeSelectMenuOptionBuilder),
);
export const optionsValidator = optionValidator.array.lengthGe(0);
export const optionsLengthValidator = s.number.int.ge(0).le(25);

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
@@ -1,14 +1,14 @@
import { TextInputStyle } from 'discord-api-types/v10';
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
@@ -1,16 +1,15 @@
import { z } from 'zod';
import { s } from '@sapphire/shapeshift';
import { ApplicationCommandType } from 'discord-api-types/v10';
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
@@ -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
@@ -1,27 +1,26 @@
import is from '@sindresorhus/is';
import type { APIApplicationCommandOptionChoice } from 'discord-api-types/v10';
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
@@ -1,5 +1,5 @@
import { ChannelType } from 'discord-api-types/v10';
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 @@ -15,15 +15,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))));

export class ApplicationCommandOptionChannelTypesMixin {
public readonly channel_types?: ApplicationCommandOptionAllowedChannelTypes[];
Expand All @@ -38,9 +30,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
@@ -1,13 +1,11 @@
import { APIApplicationCommandOptionChoice, ApplicationCommandOptionType } from 'discord-api-types/v10';
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
@@ -1,11 +1,11 @@
import { APIApplicationCommandIntegerOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
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
@@ -1,11 +1,11 @@
import { APIApplicationCommandNumberOption, ApplicationCommandOptionType } from 'discord-api-types/v10';
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
45 changes: 24 additions & 21 deletions packages/builders/src/messages/embed/Assertions.ts
@@ -1,43 +1,46 @@
import type { APIEmbedField } from 'discord-api-types/v10';
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 imageURLPredicate = s.string.url({
allowedProtocols: ['http:', 'https:', 'attachment:'],
}).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 urlPredicate = s.string.url({
allowedProtocols: ['http:', 'https:'],
}).nullish;

export const descriptionPredicate = z.string().min(1).max(4096).nullable();
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 footerTextPredicate = z.string().min(1).max(2048).nullable();
export const descriptionPredicate = s.string.lengthGe(1).lengthLe(4096).nullable;

export const timestampPredicate = z.union([z.number(), z.date()]).nullable();
export const footerTextPredicate = s.string.lengthGe(1).lengthLe(2048).nullable;

export const titlePredicate = fieldNamePredicate.nullable();
export const timestampPredicate = s.union(s.number, s.date).nullable;

export const titlePredicate = fieldNamePredicate.nullable;

0 comments on commit 3c0bbac

Please sign in to comment.