Skip to content

Commit

Permalink
deps(refactor): switch to using zod
Browse files Browse the repository at this point in the history
  • Loading branch information
ghostdevv committed Apr 20, 2024
1 parent 2cc65ba commit 869a9ba
Show file tree
Hide file tree
Showing 19 changed files with 169 additions and 164 deletions.
5 changes: 5 additions & 0 deletions .changeset/tough-kangaroos-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"jellycommands": patch
---

deps: switch to parsing with zod
2 changes: 1 addition & 1 deletion packages/jellycommands/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"dependencies": {
"axios": "^1.6.8",
"discord-api-types": "^0.37.79",
"joi": "^17.12.3"
"zod": "^3.22.5"
},
"repository": {
"type": "git",
Expand Down
14 changes: 8 additions & 6 deletions packages/jellycommands/src/JellyCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { resolveCommands } from './commands/resolve';
import { getCommandIdMap } from './commands/cache';
import { registerEvents } from './events/register';
import { handleButton } from './buttons/handle.js';
import { JellyCommandsOptions } from './options';
import { JellyCommandsOptions, jellyCommandsOptionsSchema } from './options';
import { loadButtons } from './buttons/load.js';
import { respond } from './commands/respond';
import { Client } from 'discord.js';
import { schema } from './options';
import { parseSchema } from './utils/zod.js';

export class JellyCommands extends Client {
public readonly joptions: JellyCommandsOptions;
Expand All @@ -16,10 +16,12 @@ export class JellyCommands extends Client {
constructor(options: JellyCommandsOptions) {
super(options.clientOptions);

const { error, value } = schema.validate(options);

if (error) throw error.annotate();
else this.joptions = value;
// @ts-expect-error issue with intents
this.joptions = parseSchema(
'JellyCommands options',
jellyCommandsOptionsSchema,
options,
) as JellyCommandsOptions;

this.props = options.props || {};

Expand Down
8 changes: 3 additions & 5 deletions packages/jellycommands/src/buttons/buttons.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { type ButtonOptions, schema } from './options';
import { type ButtonOptions, buttonSchema } from './options';
import type { JellyCommands } from '../JellyCommands';
import type { ButtonInteraction } from 'discord.js';
import type { Awaitable } from '../utils/types';
import { parseSchema } from '../utils/zod';

export type ButtonCallback = (context: {
client: JellyCommands;
Expand All @@ -16,10 +17,7 @@ export class Button {
options: ButtonOptions,
public readonly run: ButtonCallback,
) {
const { error, value } = schema.validate(options);

if (error) throw error.annotate();
else this.options = value;
this.options = parseSchema('button', buttonSchema, options);
}
}

Expand Down
32 changes: 21 additions & 11 deletions packages/jellycommands/src/buttons/options.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { InteractionDeferReplyOptions } from 'discord.js';
import type { Awaitable } from '../utils/types';
import Joi from 'joi';
import { z } from 'zod';

export interface ButtonOptions {
/**
Expand All @@ -19,16 +19,26 @@ export interface ButtonOptions {
disabled?: boolean;
}

export const schema = Joi.object({
id: Joi.alternatives().try(Joi.string(), Joi.object().regex(), Joi.function()),
export const buttonSchema = z.object({
id: z.union([
z.string(),
z.instanceof(RegExp),
// todo test this
z
.function()
.args(z.string().optional())
.returns(z.union([z.boolean(), z.promise(z.boolean())])),
]),

defer: [
Joi.bool(),
Joi.object({
ephemeral: Joi.bool(),
fetchReply: Joi.bool(),
}),
],
defer: z
.union([
z.boolean().default(false),
z.object({
ephemeral: z.boolean().optional(),
fetchReply: z.boolean().optional(),
}),
])
.optional(),

disabled: Joi.bool().default(false),
disabled: z.boolean().default(false).optional(),
});
12 changes: 5 additions & 7 deletions packages/jellycommands/src/commands/types/BaseCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import type { RESTPostAPIApplicationCommandsJSONBody } from 'discord-api-types/v
import type { ApplicationCommandType } from 'discord-api-types/v10';
import type { JellyCommands } from '../../JellyCommands';
import type { BaseInteraction } from 'discord.js';
import type { AnyZodObject } from 'zod';

import { PermissionsBitField } from 'discord.js';
import { Awaitable } from '../../utils/types';
import { parseSchema } from '../../utils/zod';
import { BaseOptions } from './options';
import { createHash } from 'crypto';
import Joi from 'joi';

export interface RunOptions<InteractionType extends BaseInteraction> {
interaction: InteractionType;
Expand All @@ -33,17 +35,13 @@ export abstract class BaseCommand<
schema,
}: {
options: OptionsType;
schema: Joi.ObjectSchema<any>;
schema: AnyZodObject;
run: CommandCallback<InteractionType>;
}) {
if (!run || typeof run != 'function')
throw new TypeError(`Expected type function for run, received ${typeof run}`);

const { error, value } = schema.validate(options);

if (error) throw error.annotate();

this.options = value as typeof options;
this.options = parseSchema('command', schema, options) as OptionsType;
this.run = run;

if (!this.options.guilds?.length && !this.options.global && !this.options.dev)
Expand Down
4 changes: 2 additions & 2 deletions packages/jellycommands/src/commands/types/commands/Command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type { APIApplicationCommandOption } from 'discord-api-types/v10';
import { ApplicationCommandType } from 'discord-api-types/v10';
import type { JellyApplicationCommandOption } from './types';
import type { JellyCommands } from '../../../JellyCommands';
import { commandSchema, CommandOptions } from './options';
import type { CommandCallback } from '../BaseCommand';
import { schema, CommandOptions } from './options';
import { Awaitable } from '../../../utils/types';
import { ApplicationCommand } from 'discord.js';
import { BaseCommand } from '../BaseCommand';
Expand Down Expand Up @@ -34,7 +34,7 @@ export class Command extends BaseCommand<CommandOptions, ChatInputCommandInterac
options: CommandOptions;
autocomplete?: AutocompleteHandler;
}) {
super({ run, options, schema });
super({ run, options, schema: commandSchema });

if (autocomplete && typeof autocomplete !== 'function') {
throw new TypeError('Autocomplete handler must be a function');
Expand Down
36 changes: 18 additions & 18 deletions packages/jellycommands/src/commands/types/commands/options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from 'zod';
import { baseCommandSchema } from '../../../commands/types/options';
import type { BaseOptions } from '../../../commands/types/options';
import { baseSchema } from '../../../commands/types/options';
import type { JellyApplicationCommandOption } from './types';
import type { Locale } from 'discord-api-types/v10';

Expand All @@ -20,24 +21,23 @@ export interface CommandOptions extends BaseOptions {
options?: JellyApplicationCommandOption[];
}

import Joi from 'joi';
export const commandSchema = baseCommandSchema.extend({
name: z
.string()
.min(1, 'Slash command name must be at least 1 char long')
.max(32, 'Slash command name cannot exceed 32 chars')
.regex(
/^[a-z0-9]+$/,
'Slash command name must be all lowercase, alphanumeric, and at most 32 chars long',
)
.refine((str) => str.toLowerCase() == str, 'Slash command name must be lowercase'),

export const schema = baseSchema.append({
// Enforce good registration rule
name: Joi.string()
.required()
.prefs({ convert: false })
.ruleset.lowercase()
.min(1)
.max(32)
.pattern(/^[a-z0-9]+$/)
.rule({
message:
'Slash Command names must be 1 - 32 characters, all lowercase with no witespaces or special chars',
}),
description: z
.string({ required_error: 'Slash command description is required' })
.min(1, 'Slash command description must be at least 1 char long')
.max(100, 'Slash command description cannot exceed 100 chars'),

description: Joi.string().min(1).max(100).required(),
descriptionLocalizations: Joi.object(),
descriptionLocalizations: z.object({}).catchall(z.string()).optional(),

options: Joi.array(),
options: z.any().array().optional(),
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { CommandCallback } from '../../../commands/types/BaseCommand';
import { BaseCommand } from '../../../commands/types/BaseCommand';
import type { ContextMenuCommandInteraction } from 'discord.js';
import { ApplicationCommandType } from 'discord-api-types/v10';
import { schema, MessageCommandOptions } from './options';
import { messageCommandSchema, MessageCommandOptions } from './options';

export class MessageCommand extends BaseCommand<
MessageCommandOptions,
Expand All @@ -14,7 +14,7 @@ export class MessageCommand extends BaseCommand<
run: CommandCallback<ContextMenuCommandInteraction>,
options: MessageCommandOptions,
) {
super({ run, options, schema });
super({ run, options, schema: messageCommandSchema });
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { baseCommandSchema } from '../../../commands/types/options';
import type { BaseOptions } from '../../../commands/types/options';
import { baseSchema } from '../../../commands/types/options';

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface MessageCommandOptions extends BaseOptions {}

export const schema = baseSchema.append({});
export const messageCommandSchema = baseCommandSchema.extend({});
47 changes: 24 additions & 23 deletions packages/jellycommands/src/commands/types/options.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { InteractionDeferReplyOptions } from 'discord.js';
import type { PermissionResolvable } from 'discord.js';
import type { Locale } from 'discord-api-types/v10';
import { snowflakeArray } from '../../utils/joi';
import Joi from 'joi';
import { snowflakeSchema } from '../../utils/zod';
import { z } from 'zod';

export interface BaseOptions {
/**
Expand Down Expand Up @@ -57,26 +57,27 @@ export interface BaseOptions {
disabled?: boolean;
}

export const baseSchema = Joi.object({
name: Joi.string().required(),
nameLocalizations: Joi.object(),
export const baseCommandSchema = z.object({
name: z.string(),
nameLocalizations: z.object({}).catchall(z.string()).optional(),
dev: z.boolean().default(false).optional(),
defer: z
.union([
z.boolean().default(false),
z.object({
ephemeral: z.boolean().optional(),
fetchReply: z.boolean().optional(),
}),
])
.optional(),

dev: Joi.bool().default(false),

defer: [
Joi.bool(),
Joi.object({
ephemeral: Joi.bool(),
fetchReply: Joi.bool(),
}),
],

guards: Joi.object({
permissions: Joi.any(),
}),

guilds: snowflakeArray(),
global: Joi.bool().default(false),
dm: Joi.bool().default(true),
disabled: Joi.bool().default(false),
guards: z
.object({
permissions: z.any(),
})
.optional(),
guilds: snowflakeSchema.array().nonempty().optional(),
global: z.boolean().default(false).optional(),
dm: z.boolean().default(false).optional(),
disabled: z.boolean().default(false).optional(),
});
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { CommandCallback } from '../../../commands/types/BaseCommand';
import { userCommandSchema, UserCommandOptions } from './options';
import { BaseCommand } from '../../../commands/types/BaseCommand';
import type { ContextMenuCommandInteraction } from 'discord.js';
import { ApplicationCommandType } from 'discord-api-types/v10';
import { schema, UserCommandOptions } from './options';

export class UserCommand extends BaseCommand<UserCommandOptions, ContextMenuCommandInteraction> {
public readonly type = ApplicationCommandType.User;

constructor(run: CommandCallback<ContextMenuCommandInteraction>, options: UserCommandOptions) {
super({ run, options, schema });
super({ run, options, schema: userCommandSchema });
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { baseCommandSchema } from '../../../commands/types/options';
import type { BaseOptions } from '../../../commands/types/options';
import { baseSchema } from '../../../commands/types/options';

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface UserCommandOptions extends BaseOptions {}

export const schema = baseSchema.append({});
export const userCommandSchema = baseCommandSchema.extend({});
6 changes: 2 additions & 4 deletions packages/jellycommands/src/events/Event.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { JellyCommands } from '../JellyCommands';
import { schema, EventOptions } from './options';
import type { ClientEvents } from 'discord.js';
import { parseSchema } from '../utils/zod';
import { Awaitable } from '../utils/types';

export type EventCallback<EventName extends keyof ClientEvents> = (
Expand All @@ -22,10 +23,7 @@ export class Event<T extends keyof ClientEvents = keyof ClientEvents> {
if (!run || typeof run != 'function')
throw new TypeError(`Expected type function for run, received ${typeof run}`);

const { error, value } = schema.validate(options);

if (error) throw error.annotate();
else this.options = value;
this.options = parseSchema('event options', schema, options) as Required<EventOptions<T>>;
}
}

Expand Down
18 changes: 12 additions & 6 deletions packages/jellycommands/src/events/options.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ClientEvents } from 'discord.js';
import { type ClientEvents, Events } from 'discord.js';

export interface EventOptions<Event extends keyof ClientEvents> {
/**
Expand All @@ -8,19 +8,25 @@ export interface EventOptions<Event extends keyof ClientEvents> {

/**
* Whether or not the event should be loaded
* @default false
*/
disabled?: boolean;

/**
* Should the event be ran once or every time it's received
* @default false
*/
once?: boolean;
}

import Joi from 'joi';
import { z } from 'zod';

export const schema = Joi.object({
name: Joi.string().required(),
disabled: Joi.bool().default(false),
once: Joi.bool().default(false),
export const schema = z.object({
name: z
.string()
.refine((str): str is keyof ClientEvents => Object.values(Events).includes(str as any), {
message: 'Event name must be a valid client event',
}),
disabled: z.boolean().default(false),
once: z.boolean().default(false),
});
Loading

0 comments on commit 869a9ba

Please sign in to comment.