From 3c0bbac82fa9988af4a62ff00c66d149fbe6b921 Mon Sep 17 00:00:00 2001 From: Parbez Date: Sat, 9 Apr 2022 15:07:16 +0530 Subject: [PATCH] refactor: replace zod with shapeshift (#7547) --- .../builders/__tests__/messages/embed.test.ts | 8 +-- packages/builders/package.json | 4 +- .../builders/src/components/Assertions.ts | 55 +++++++++---------- .../src/components/textInput/Assertions.ts | 16 +++--- .../contextMenuCommands/Assertions.ts | 13 ++--- .../src/interactions/modals/Assertions.ts | 6 +- .../interactions/slashCommands/Assertions.ts | 17 +++--- ...plicationCommandOptionChannelTypesMixin.ts | 16 +----- ...ndOptionWithChoicesAndAutocompleteMixin.ts | 12 ++-- .../slashCommands/options/integer.ts | 4 +- .../slashCommands/options/number.ts | 4 +- .../builders/src/messages/embed/Assertions.ts | 45 ++++++++------- packages/builders/src/messages/embed/Embed.ts | 5 +- yarn.lock | 16 +++--- 14 files changed, 105 insertions(+), 116 deletions(-) diff --git a/packages/builders/__tests__/messages/embed.test.ts b/packages/builders/__tests__/messages/embed.test.ts index 332b9fe07..5bf2fd277 100644 --- a/packages/builders/__tests__/messages/embed.test.ts +++ b/packages/builders/__tests__/messages/embed.test.ts @@ -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(); }); }); @@ -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 }], }); }); @@ -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 }], }); }); diff --git a/packages/builders/package.json b/packages/builders/package.json index 519c5de99..2a27d3a87 100644 --- a/packages/builders/package.json +++ b/packages/builders/package.json @@ -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", diff --git a/packages/builders/src/components/Assertions.ts b/packages/builders/src/components/Assertions.ts index 4e1fd7b8e..c3407c448 100644 --- a/packages/builders/src/components/Assertions.ts +++ b/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, diff --git a/packages/builders/src/components/textInput/Assertions.ts b/packages/builders/src/components/textInput/Assertions.ts index 58b2d5ed4..1c9bff6fb 100644 --- a/packages/builders/src/components/textInput/Assertions.ts +++ b/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); diff --git a/packages/builders/src/interactions/contextMenuCommands/Assertions.ts b/packages/builders/src/interactions/contextMenuCommands/Assertions.ts index eba1a65f1..1935f9021 100644 --- a/packages/builders/src/interactions/contextMenuCommands/Assertions.ts +++ b/packages/builders/src/interactions/contextMenuCommands/Assertions.ts @@ -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); diff --git a/packages/builders/src/interactions/modals/Assertions.ts b/packages/builders/src/interactions/modals/Assertions.ts index ecf59b9c3..85668c688 100644 --- a/packages/builders/src/interactions/modals/Assertions.ts +++ b/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, diff --git a/packages/builders/src/interactions/slashCommands/Assertions.ts b/packages/builders/src/interactions/slashCommands/Assertions.ts index 9ce5467e1..84cf46df8 100644 --- a/packages/builders/src/interactions/slashCommands/Assertions.ts +++ b/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); @@ -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); @@ -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); diff --git a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionChannelTypesMixin.ts b/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionChannelTypesMixin.ts index 316da0cb4..96fe76f28 100644 --- a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionChannelTypesMixin.ts +++ b/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionChannelTypesMixin.ts @@ -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 = [ @@ -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, - ZodLiteral, - ...ZodLiteral[] - ], - ), -); +const channelTypesPredicate = s.array(s.union(...allowedChannelTypes.map((type) => s.literal(type)))); export class ApplicationCommandOptionChannelTypesMixin { public readonly channel_types?: ApplicationCommandOptionAllowedChannelTypes[]; @@ -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; } diff --git a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin.ts b/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin.ts index d6bc14a2e..9562c9643 100644 --- a/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin.ts +++ b/packages/builders/src/interactions/slashCommands/mixins/ApplicationCommandOptionWithChoicesAndAutocompleteMixin.ts @@ -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 { public readonly choices?: APIApplicationCommandOptionChoice[]; diff --git a/packages/builders/src/interactions/slashCommands/options/integer.ts b/packages/builders/src/interactions/slashCommands/options/integer.ts index ec8bf52df..1795b3c40 100644 --- a/packages/builders/src/interactions/slashCommands/options/integer.ts +++ b/packages/builders/src/interactions/slashCommands/options/integer.ts @@ -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 diff --git a/packages/builders/src/interactions/slashCommands/options/number.ts b/packages/builders/src/interactions/slashCommands/options/number.ts index ff843e384..3a42f43ae 100644 --- a/packages/builders/src/interactions/slashCommands/options/number.ts +++ b/packages/builders/src/interactions/slashCommands/options/number.ts @@ -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 diff --git a/packages/builders/src/messages/embed/Assertions.ts b/packages/builders/src/messages/embed/Assertions.ts index 442ad10c0..0af2cb4e6 100644 --- a/packages/builders/src/messages/embed/Assertions.ts +++ b/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; diff --git a/packages/builders/src/messages/embed/Embed.ts b/packages/builders/src/messages/embed/Embed.ts index 896d200b9..980fc8ff9 100644 --- a/packages/builders/src/messages/embed/Embed.ts +++ b/packages/builders/src/messages/embed/Embed.ts @@ -5,6 +5,7 @@ import { descriptionPredicate, embedFieldsArrayPredicate, footerTextPredicate, + imageURLPredicate, timestampPredicate, titlePredicate, urlPredicate, @@ -69,12 +70,12 @@ export class EmbedBuilder extends UnsafeEmbedBuilder { public override setImage(url: string | null): this { // Data assertions - return super.setImage(urlPredicate.parse(url)!); + return super.setImage(imageURLPredicate.parse(url)!); } public override setThumbnail(url: string | null): this { // Data assertions - return super.setThumbnail(urlPredicate.parse(url)!); + return super.setThumbnail(imageURLPredicate.parse(url)!); } public override setTimestamp(timestamp: number | Date | null = Date.now()): this { diff --git a/yarn.lock b/yarn.lock index 9caddb7ad..3f416cb79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1758,6 +1758,7 @@ __metadata: "@babel/preset-env": ^7.16.11 "@babel/preset-typescript": ^7.16.7 "@discordjs/ts-docgen": ^0.3.4 + "@sapphire/shapeshift": ^2.0.0 "@sindresorhus/is": ^4.4.0 "@types/jest": ^27.4.0 "@types/node": ^16.11.24 @@ -1777,7 +1778,6 @@ __metadata: tsup: ^5.11.13 typedoc: ^0.22.11 typescript: ^4.5.5 - zod: ^3.11.6 languageName: unknown linkType: soft @@ -2317,6 +2317,13 @@ __metadata: languageName: node linkType: hard +"@sapphire/shapeshift@npm:^2.0.0": + version: 2.0.0 + resolution: "@sapphire/shapeshift@npm:2.0.0" + checksum: e866aa714d70b0c0d607ed9ad831c039b396cca30611691c59a6e43cb369cf23d69404560105101231f56128022b0030a5201c074193da324ad4345c6d06992c + languageName: node + linkType: hard + "@sapphire/snowflake@npm:^3.1.0": version: 3.1.0 resolution: "@sapphire/snowflake@npm:3.1.0" @@ -11308,10 +11315,3 @@ dts-critic@latest: checksum: f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700 languageName: node linkType: hard - -"zod@npm:^3.11.6": - version: 3.11.6 - resolution: "zod@npm:3.11.6" - checksum: 044ac416450f179a0c88240f27849d2886c777cebade42df10e5f04125b0265cec82d9bd741a7dcb11796b2ea88b32c86be7d36932a4bed6af57002560359db1 - languageName: node - linkType: hard