From 57c70aba750dcf703ee376be44059faf34c4c5cd Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Sun, 4 Aug 2024 18:59:26 +0200 Subject: [PATCH 1/2] Wrapping errors in emitter. --- coverage.svg | 2 +- src/emission.ts | 38 ++++++++++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/coverage.svg b/coverage.svg index 5bb55be2..55fe6149 100644 --- a/coverage.svg +++ b/coverage.svg @@ -1 +1 @@ -Coverage: 100%Coverage100% \ No newline at end of file +Coverage: 99.64%Coverage99.64% \ No newline at end of file diff --git a/src/emission.ts b/src/emission.ts index 385392e2..a8d8e579 100644 --- a/src/emission.ts +++ b/src/emission.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import type { RemoteSocket, Server, Socket } from "socket.io"; import { z } from "zod"; +import { InputValidationError, OutputValidationError } from "./errors"; import { RemoteClient, SomeRemoteSocket, @@ -67,22 +68,47 @@ export function makeEmitter({ }: { subject: Socket | SomeRemoteSocket | Socket["broadcast"] | Server; } & EmitterConfig) { + const getSchemas = (event: string) => { + const { schema, ack } = emission[event]; + return { schema, ack: ack && ("id" in subject ? ack : ack.array()) }; + }; + const parseOutput = (schema: z.AnyZodTuple, payload: unknown[]) => { + try { + return schema.parse(payload); + } catch (error) { + throw error instanceof z.ZodError + ? new OutputValidationError(error) + : error; + } + }; + const parseAck = ( + schema: z.AnyZodTuple | z.ZodArray, + payload: unknown[], + ) => { + try { + return schema.parse(payload); + } catch (error) { + throw error instanceof z.ZodError + ? new InputValidationError(error) + : error; + } + }; /** - * @throws z.ZodError on validation - * @throws Error on ack timeout + * @throws OutputValidationError on validating emission + * @throws InputValidationError on validating acknowledgment + * @throws Error on ack timeout or unsupported event * */ return async (event: string, ...args: unknown[]) => { - const isSocket = "id" in subject; assert(event in emission, new Error(`Unsupported event ${event}`)); - const { schema, ack } = emission[event]; - const payload = schema.parse(args); + const { schema, ack } = getSchemas(event); + const payload = parseOutput(schema, args); if (!ack) { return subject.emit(String(event), ...payload) || true; } const response = await subject .timeout(timeout) .emitWithAck(String(event), ...payload); - return (isSocket ? ack : ack.array()).parse(response); + return parseAck(ack, response); }; } From 99d2411190a94ce6274711a6a786571c640070e9 Mon Sep 17 00:00:00 2001 From: Robin Tail Date: Sun, 4 Aug 2024 21:26:01 +0200 Subject: [PATCH 2/2] REF: introducing parseWrapped(). --- coverage.svg | 2 +- src/action.ts | 35 +++++++++++++++++------------------ src/emission.ts | 31 +++++++------------------------ src/errors.ts | 19 ++++++++++++++++--- 4 files changed, 41 insertions(+), 46 deletions(-) diff --git a/coverage.svg b/coverage.svg index 55fe6149..5bb55be2 100644 --- a/coverage.svg +++ b/coverage.svg @@ -1 +1 @@ -Coverage: 99.64%Coverage99.64% \ No newline at end of file +Coverage: 100%Coverage100% \ No newline at end of file diff --git a/src/action.ts b/src/action.ts index 376b52b7..b58710e0 100644 --- a/src/action.ts +++ b/src/action.ts @@ -2,7 +2,11 @@ import { init, last } from "ramda"; import { z } from "zod"; import { ActionNoAckDef, ActionWithAckDef } from "./actions-factory"; import { EmissionMap } from "./emission"; -import { OutputValidationError, InputValidationError } from "./errors"; +import { + OutputValidationError, + InputValidationError, + parseWrapped, +} from "./errors"; import { ActionContext, ClientContext, Handler } from "./handler"; import { Namespaces, rootNS } from "./namespace"; @@ -77,11 +81,7 @@ export class Action< /** @throws InputValidationError */ #parseInput(params: unknown[]) { const payload = this.#outputSchema ? init(params) : params; - try { - return this.#inputSchema.parse(payload); - } catch (e) { - throw e instanceof z.ZodError ? new InputValidationError(e) : e; - } + return parseWrapped(this.#inputSchema, payload, InputValidationError); } /** @throws InputValidationError */ @@ -89,13 +89,12 @@ export class Action< if (!this.#outputSchema) { return undefined; } - try { - return z - .function(this.#outputSchema, z.void()) - .parse(last(params), { path: [Math.max(0, params.length - 1)] }); - } catch (e) { - throw e instanceof z.ZodError ? new InputValidationError(e) : e; - } + return parseWrapped( + z.function(this.#outputSchema, z.void()), + last(params), + InputValidationError, + { path: [Math.max(0, params.length - 1)] }, + ); } /** @throws OutputValidationError */ @@ -103,11 +102,11 @@ export class Action< if (!this.#outputSchema) { return; } - try { - return this.#outputSchema.parse(output) as z.output>; - } catch (e) { - throw e instanceof z.ZodError ? new OutputValidationError(e) : e; - } + return parseWrapped( + this.#outputSchema, + output, + OutputValidationError, + ) as z.output>; } public override async execute({ diff --git a/src/emission.ts b/src/emission.ts index a8d8e579..b42bd5a7 100644 --- a/src/emission.ts +++ b/src/emission.ts @@ -1,7 +1,11 @@ import assert from "node:assert/strict"; import type { RemoteSocket, Server, Socket } from "socket.io"; import { z } from "zod"; -import { InputValidationError, OutputValidationError } from "./errors"; +import { + InputValidationError, + OutputValidationError, + parseWrapped, +} from "./errors"; import { RemoteClient, SomeRemoteSocket, @@ -72,27 +76,6 @@ export function makeEmitter({ const { schema, ack } = emission[event]; return { schema, ack: ack && ("id" in subject ? ack : ack.array()) }; }; - const parseOutput = (schema: z.AnyZodTuple, payload: unknown[]) => { - try { - return schema.parse(payload); - } catch (error) { - throw error instanceof z.ZodError - ? new OutputValidationError(error) - : error; - } - }; - const parseAck = ( - schema: z.AnyZodTuple | z.ZodArray, - payload: unknown[], - ) => { - try { - return schema.parse(payload); - } catch (error) { - throw error instanceof z.ZodError - ? new InputValidationError(error) - : error; - } - }; /** * @throws OutputValidationError on validating emission * @throws InputValidationError on validating acknowledgment @@ -101,14 +84,14 @@ export function makeEmitter({ return async (event: string, ...args: unknown[]) => { assert(event in emission, new Error(`Unsupported event ${event}`)); const { schema, ack } = getSchemas(event); - const payload = parseOutput(schema, args); + const payload = parseWrapped(schema, args, OutputValidationError); if (!ack) { return subject.emit(String(event), ...payload) || true; } const response = await subject .timeout(timeout) .emitWithAck(String(event), ...payload); - return parseAck(ack, response); + return parseWrapped(ack, response, InputValidationError); }; } diff --git a/src/errors.ts b/src/errors.ts index b04b063e..927cc855 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,4 +1,4 @@ -import { ZodError } from "zod"; +import { z } from "zod"; import { getMessageFromError } from "./common-helpers"; /** @desc An error related to the input and output schemas declaration */ @@ -10,7 +10,7 @@ export class IOSchemaError extends Error { export class InputValidationError extends IOSchemaError { public override name = "InputValidationError"; - constructor(public readonly originalError: ZodError) { + constructor(public readonly originalError: z.ZodError) { super(getMessageFromError(originalError)); } } @@ -19,7 +19,20 @@ export class InputValidationError extends IOSchemaError { export class OutputValidationError extends IOSchemaError { public override name = "OutputValidationError"; - constructor(public readonly originalError: ZodError) { + constructor(public readonly originalError: z.ZodError) { super(getMessageFromError(originalError)); } } + +export const parseWrapped = ( + schema: T, + data: unknown, + Class: { new (error: z.ZodError): IOSchemaError }, + params?: Partial, +): z.output => { + try { + return schema.parse(data, params); + } catch (error) { + throw error instanceof z.ZodError ? new Class(error) : error; + } +};