From 59129a1cb734ca87d12e32b7250c9c13538f85e2 Mon Sep 17 00:00:00 2001 From: OnurGvnc Date: Tue, 10 Jan 2023 11:50:17 +0300 Subject: [PATCH] feat: custom error messages for helpers #27 --- src/index.ts | 23 +++++++++--------- src/parsers.test.ts | 40 +++++++++++++++--------------- src/schemas.test.ts | 51 +++++++++++++++++++++------------------ src/schemas.ts | 59 ++++++++++++++++++++++++++++++--------------- 4 files changed, 98 insertions(+), 75 deletions(-) diff --git a/src/index.ts b/src/index.ts index a9e2189..706cd7f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,10 +7,10 @@ import { parseFormSafe, } from './parsers'; import { - BoolAsString, - CheckboxAsString, - IntAsString, - NumAsString, + boolAsString, + checkboxAsString, + intAsString, + numAsString, } from './schemas'; export { @@ -20,10 +20,9 @@ export { parseQuerySafe, parseForm, parseFormSafe, - BoolAsString, - CheckboxAsString, - IntAsString, - NumAsString, + boolAsString, + intAsString, + numAsString, }; export const zx = { @@ -33,8 +32,8 @@ export const zx = { parseQuerySafe, parseForm, parseFormSafe, - BoolAsString, - CheckboxAsString, - IntAsString, - NumAsString, + boolAsString, + checkboxAsString, + intAsString, + numAsString, }; diff --git a/src/parsers.test.ts b/src/parsers.test.ts index 81a01d0..525884b 100644 --- a/src/parsers.test.ts +++ b/src/parsers.test.ts @@ -11,7 +11,7 @@ describe('parseParams', () => { type Result = { id: string; age: number }; const params: Params = { id: 'id1', age: '10' }; const paramsResult = { id: 'id1', age: 10 }; - const objectSchema = { id: z.string(), age: zx.IntAsString }; + const objectSchema = { id: z.string(), age: zx.intAsString() }; const zodSchema = z.object(objectSchema); test('parses params using an object', () => { @@ -41,13 +41,13 @@ describe('parseParamsSafe', () => { type Result = { id: string; age: number }; const params: Params = { id: 'id1', age: '10' }; const paramsResult = { id: 'id1', age: 10 }; - const objectSchema = { id: z.string(), age: zx.IntAsString }; + const objectSchema = { id: z.string(), age: zx.intAsString() }; const zodSchema = z.object(objectSchema); test('parses params using an object', () => { const result = zx.parseParamsSafe(params, { id: z.string(), - age: zx.IntAsString, + age: zx.intAsString(), }); expect(result.success).toBe(true); if (result.success !== true) throw new Error('Parsing failed'); @@ -88,7 +88,7 @@ describe('parseQuery', () => { const queryResult = { id: 'id1', age: 10 }; const objectSchema = { id: z.string(), - age: zx.IntAsString, + age: zx.intAsString(), friends: z.array(z.string()).optional(), }; const zodSchema = z.object(objectSchema); @@ -96,7 +96,7 @@ describe('parseQuery', () => { test('parses URLSearchParams using an object', () => { const result = zx.parseQuery(search, { id: z.string(), - age: zx.IntAsString, + age: zx.intAsString(), friends: z.array(z.string()).optional(), }); expect(result).toStrictEqual(queryResult); @@ -115,7 +115,7 @@ describe('parseQuery', () => { search.append('friends', 'friend2'); const result = zx.parseQuery(search, { id: z.string(), - age: zx.IntAsString, + age: zx.intAsString(), friends: z.array(z.string()).optional(), }); expect(result).toStrictEqual({ @@ -141,7 +141,7 @@ describe('parseQuery', () => { const request = new Request(`http://example.com?${search.toString()}`); const result = zx.parseQuery(request, { id: z.string(), - age: zx.IntAsString, + age: zx.intAsString(), friends: z.array(z.string()).optional(), }); expect(result).toStrictEqual(queryResult); @@ -173,7 +173,7 @@ describe('parseQuery', () => { search, { id: z.string(), - age: zx.IntAsString, + age: zx.intAsString(), friends: z.array(z.string()).optional(), }, { parser: customArrayParser } @@ -205,7 +205,7 @@ describe('parseQuerySafe', () => { const queryResult = { id: 'id1', age: 10 }; const zodSchema = z.object({ id: z.string(), - age: zx.IntAsString, + age: zx.intAsString(), friends: z.array(z.string()).optional(), }); @@ -213,7 +213,7 @@ describe('parseQuerySafe', () => { const search = new URLSearchParams({ id: 'id1', age: '10' }); const result = zx.parseQuerySafe(search, { id: z.string(), - age: zx.IntAsString, + age: zx.intAsString(), friends: z.array(z.string()).optional(), }); expect(result.success).toBe(true); @@ -237,7 +237,7 @@ describe('parseQuerySafe', () => { search.append('friends', 'friend2'); const result = zx.parseQuerySafe(search, { id: z.string(), - age: zx.IntAsString, + age: zx.intAsString(), friends: z.array(z.string()).optional(), }); expect(result.success).toBe(true); @@ -268,7 +268,7 @@ describe('parseQuerySafe', () => { const request = new Request(`http://example.com?${search.toString()}`); const result = zx.parseQuerySafe(request, { id: z.string(), - age: zx.IntAsString, + age: zx.intAsString(), friends: z.array(z.string()).optional(), }); expect(result.success).toBe(true); @@ -316,8 +316,8 @@ describe('parseForm', () => { const formResult = { id: 'id1', age: 10, consent: true }; const objectSchema = { id: z.string(), - age: zx.IntAsString, - consent: zx.CheckboxAsString, + age: zx.intAsString(), + consent: zx.checkboxAsString(), friends: z.array(z.string()).optional(), image: z.instanceof(NodeOnDiskFile).optional(), }; @@ -379,8 +379,8 @@ describe('parseForm', () => { }); const result = await zx.parseForm(request, { id: z.string(), - age: zx.IntAsString, - consent: zx.CheckboxAsString, + age: zx.intAsString(), + consent: zx.checkboxAsString(), friends: z.array(z.string()).optional(), image: z.instanceof(NodeOnDiskFile).optional(), }); @@ -454,8 +454,8 @@ describe('parseFormSafe', () => { const formResult = { id: 'id1', age: 10, consent: true }; const zodSchema = z.object({ id: z.string(), - age: zx.IntAsString, - consent: zx.CheckboxAsString, + age: zx.intAsString(), + consent: zx.checkboxAsString(), friends: z.array(z.string()).optional(), image: z.instanceof(NodeOnDiskFile).optional(), }); @@ -465,8 +465,8 @@ describe('parseFormSafe', () => { const request = createFormRequest(); const result = await zx.parseFormSafe(request, { id: z.string(), - age: zx.IntAsString, - consent: zx.CheckboxAsString, + age: zx.intAsString(), + consent: zx.checkboxAsString(), friends: z.array(z.string()).optional(), image: z.instanceof(NodeOnDiskFile).optional(), }); diff --git a/src/schemas.test.ts b/src/schemas.test.ts index 35fff22..780bb79 100644 --- a/src/schemas.test.ts +++ b/src/schemas.test.ts @@ -1,73 +1,76 @@ import { zx } from './'; -describe('BoolAsString', () => { +describe('boolAsString', () => { test('parses true as string', () => { - expect(zx.BoolAsString.parse('true')).toBe(true); + expect(zx.boolAsString().parse('true')).toBe(true); }); test('parses false as string', () => { - expect(zx.BoolAsString.parse('false')).toBe(false); + expect(zx.boolAsString().parse('false')).toBe(false); }); test('throws on non-boolean string', () => { - expect(() => zx.BoolAsString.parse('hello')).toThrowError(); + expect(() => zx.boolAsString().parse('hello')).toThrowError(); }); }); -describe('CheckboxAsString', () => { +describe('checkboxAsString', () => { test('parses "on" as boolean', () => { - expect(zx.CheckboxAsString.parse('on')).toBe(true); + expect(zx.checkboxAsString().parse('on')).toBe(true); + }); + test('parses "true" as boolean', () => { + expect(zx.checkboxAsString({ trueValue: 'true' }).parse('true')).toBe(true); }); test('parses undefined as boolean', () => { - expect(zx.CheckboxAsString.parse(undefined)).toBe(false); + expect(zx.checkboxAsString().parse(undefined)).toBe(false); }); test('throws on non-"on" string', () => { - expect(() => zx.CheckboxAsString.parse('hello')).toThrowError(); + expect(() => zx.checkboxAsString().parse('hello')).toThrowError(); }); }); -describe('IntAsString', () => { +describe('intAsString', () => { test('parses int as string', () => { - expect(zx.IntAsString.parse('3')).toBe(3); + expect(zx.intAsString().parse('3')).toBe(3); }); test('parses int as string with leading 0', () => { - expect(zx.IntAsString.parse('03')).toBe(3); + expect(zx.intAsString().parse('03')).toBe(3); }); test('parses negative int as string', () => { - expect(zx.IntAsString.parse('-3')).toBe(-3); + expect(zx.intAsString().parse('-3')).toBe(-3); }); test('throws on int as number', () => { - expect(() => zx.IntAsString.parse(3)).toThrowError(); + expect(() => zx.intAsString().parse(3)).toThrowError(); }); test('throws on float', () => { - expect(() => zx.IntAsString.parse(3.14)).toThrowError(); + expect(() => zx.intAsString().parse(3.14)).toThrowError(); }); test('throws on string float', () => { - expect(() => zx.IntAsString.parse('3.14')).toThrowError(); + expect(() => zx.intAsString().parse('3.14')).toThrowError(); }); test('throws on non-int string', () => { - expect(() => zx.IntAsString.parse('a3')).toThrowError(); + expect(() => zx.intAsString().parse('a3')).toThrowError(); }); }); -describe('NumAsString', () => { +describe('numAsString', () => { test('parses number with decimal as string', () => { - expect(zx.NumAsString.parse('3.14')).toBe(3.14); + expect(zx.numAsString().parse('3.14')).toBe(3.14); }); test('parses number with decimal as string with leading 0', () => { - expect(zx.NumAsString.parse('03.14')).toBe(3.14); + expect(zx.numAsString().parse('03.14')).toBe(3.14); }); test('parses negative number with decimal as string', () => { - expect(zx.NumAsString.parse('-3.14')).toBe(-3.14); + expect(zx.numAsString().parse('-3.14')).toBe(-3.14); }); test('parses int as string', () => { - expect(zx.NumAsString.parse('3')).toBe(3); + expect(zx.numAsString().parse('3')).toBe(3); }); test('parses int as string with leading 0', () => { - expect(zx.NumAsString.parse('03')).toBe(3); + expect(zx.numAsString().parse('03')).toBe(3); }); test('parses negative int as string', () => { - expect(zx.NumAsString.parse('-3')).toBe(-3); + expect(zx.numAsString().parse('-3')).toBe(-3); }); test('throws on non-number string', () => { - expect(() => zx.NumAsString.parse('a3')).toThrowError(); + expect(() => zx.numAsString().parse('a3')).toThrowError(); }); }); diff --git a/src/schemas.ts b/src/schemas.ts index 3c66848..aaf31dd 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import type { errorUtil } from 'zod/lib/helpers/errorUtil'; /** * Zod schema to parse strings that are booleans. @@ -8,37 +9,54 @@ import { z } from 'zod'; * BoolAsString.parse('true') -> true * ``` */ -export const BoolAsString = z - .string() - .regex(/^(true|false)$/, 'Must be a boolean string ("true" or "false")') - .transform((value) => value === 'true'); +export const boolAsString = ( + message: + | errorUtil.ErrMessage + | undefined = 'Must be a boolean string ("true" or "false")' +) => + z + .string() + .regex(/^(true|false)$/, message) + .transform((value) => value === 'true'); /** * Zod schema to parse checkbox formdata. * Use to parse values. * @example * ```ts - * CheckboxAsString.parse('on') -> true - * CheckboxAsString.parse(undefined) -> false + * checkboxAsString().parse('on') -> true + * checkboxAsString().parse(undefined) -> false * ``` */ -export const CheckboxAsString = z - .literal('on') - .optional() - .transform((value) => value === 'on'); +export const checkboxAsString = ({ + trueValue = 'on', + ...params +}: { + trueValue?: string; +} & Parameters[1] = {}) => + z.union( + [ + z.literal(trueValue).transform(() => true), + z.literal(undefined).transform(() => false), + ], + params + ); /** * Zod schema to parse strings that are integers. * Use to parse values. * @example * ```ts - * IntAsString.parse('3') -> 3 + * intAsString.parse('3') -> 3 * ``` */ -export const IntAsString = z - .string() - .regex(/^-?\d+$/, 'Must be an integer string') - .transform((val) => parseInt(val, 10)); +export const intAsString = ( + message: errorUtil.ErrMessage | undefined = 'Must be an integer string' +) => + z + .string() + .regex(/^-?\d+$/, message) + .transform((val) => parseInt(val, 10)); /** * Zod schema to parse strings that are numbers. @@ -48,7 +66,10 @@ export const IntAsString = z * NumAsString.parse('3.14') -> 3.14 * ``` */ -export const NumAsString = z - .string() - .regex(/^-?\d*\.?\d+$/, 'Must be a number string') - .transform(Number); +export const numAsString = ( + message: errorUtil.ErrMessage | undefined = 'Must be a number string' +) => + z + .string() + .regex(/^-?\d*\.?\d+$/, message) + .transform(Number);