Skip to content

Commit

Permalink
feat: custom error messages for helpers rileytomasek#27
Browse files Browse the repository at this point in the history
  • Loading branch information
OnurGvnc committed Jan 10, 2023
1 parent 77dd7ce commit 8b0db2c
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 75 deletions.
23 changes: 11 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import {
parseFormSafe,
} from './parsers';
import {
BoolAsString,
CheckboxAsString,
IntAsString,
NumAsString,
boolAsString,
checkboxAsString,
intAsString,
numAsString,
} from './schemas';

export {
Expand All @@ -20,10 +20,9 @@ export {
parseQuerySafe,
parseForm,
parseFormSafe,
BoolAsString,
CheckboxAsString,
IntAsString,
NumAsString,
boolAsString,
intAsString,
numAsString,
};

export const zx = {
Expand All @@ -33,8 +32,8 @@ export const zx = {
parseQuerySafe,
parseForm,
parseFormSafe,
BoolAsString,
CheckboxAsString,
IntAsString,
NumAsString,
boolAsString,
checkboxAsString,
intAsString,
numAsString,
};
40 changes: 20 additions & 20 deletions src/parsers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -88,15 +88,15 @@ 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);

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);
Expand All @@ -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({
Expand All @@ -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);
Expand Down Expand Up @@ -173,7 +173,7 @@ describe('parseQuery', () => {
search,
{
id: z.string(),
age: zx.IntAsString,
age: zx.intAsString(),
friends: z.array(z.string()).optional(),
},
{ parser: customArrayParser }
Expand Down Expand Up @@ -205,15 +205,15 @@ 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(),
});

test('parses URLSearchParams using an object', () => {
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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(),
};
Expand Down Expand Up @@ -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(),
});
Expand Down Expand Up @@ -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(),
});
Expand All @@ -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(),
});
Expand Down
51 changes: 27 additions & 24 deletions src/schemas.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
59 changes: 40 additions & 19 deletions src/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from 'zod';
import type { errorUtil } from 'zod/lib/helpers/errorUtil';

/**
* Zod schema to parse strings that are booleans.
Expand All @@ -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 <input type="checkbox" /> 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<typeof z.union>[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 <input type="number" /> 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.
Expand All @@ -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);

0 comments on commit 8b0db2c

Please sign in to comment.