diff --git a/lib/util/schema-utils.spec.ts b/lib/util/schema-utils.spec.ts index 10af9db538eb20c4e0a2c1f546c2052b41068b82..f8ae991b26b53addf3406735f166e1ab9d578ae7 100644 --- a/lib/util/schema-utils.spec.ts +++ b/lib/util/schema-utils.spec.ts @@ -1,26 +1,32 @@ import { z } from 'zod'; -import * as schema from './schema-utils'; +import { + looseArray, + looseRecord, + looseValue, + parseJson, + safeParseJson, +} from './schema-utils'; describe('util/schema-utils', () => { describe('looseArray', () => { it('parses array', () => { - const s = schema.looseArray(z.string()); + const s = looseArray(z.string()); expect(s.parse(['foo', 'bar'])).toEqual(['foo', 'bar']); }); it('handles non-array', () => { - const s = schema.looseArray(z.string()); + const s = looseArray(z.string()); expect(s.parse({ foo: 'bar' })).toEqual([]); }); it('drops wrong items', () => { - const s = schema.looseArray(z.string()); + const s = looseArray(z.string()); expect(s.parse(['foo', 123, null, undefined, []])).toEqual(['foo']); }); it('runs callback for wrong elements', () => { let called = false; - const s = schema.looseArray(z.string(), () => { + const s = looseArray(z.string(), () => { called = true; }); expect(s.parse(['foo', 123, 'bar'])).toEqual(['foo', 'bar']); @@ -29,7 +35,7 @@ describe('util/schema-utils', () => { it('runs callback for non-array', () => { let called = false; - const s = schema.looseArray(z.string(), () => { + const s = looseArray(z.string(), () => { called = true; }); expect(s.parse('foobar')).toEqual([]); @@ -39,23 +45,23 @@ describe('util/schema-utils', () => { describe('looseRecord', () => { it('parses record', () => { - const s = schema.looseRecord(z.string()); + const s = looseRecord(z.string()); expect(s.parse({ foo: 'bar' })).toEqual({ foo: 'bar' }); }); it('handles non-record', () => { - const s = schema.looseRecord(z.string()); + const s = looseRecord(z.string()); expect(s.parse(['foo', 'bar'])).toEqual({}); }); it('drops wrong items', () => { - const s = schema.looseRecord(z.string()); + const s = looseRecord(z.string()); expect(s.parse({ foo: 'foo', bar: 123 })).toEqual({ foo: 'foo' }); }); it('runs callback for wrong elements', () => { let called = false; - const s = schema.looseRecord(z.string(), () => { + const s = looseRecord(z.string(), () => { called = true; }); expect(s.parse({ foo: 'foo', bar: 123 })).toEqual({ foo: 'foo' }); @@ -64,7 +70,7 @@ describe('util/schema-utils', () => { it('runs callback for non-record', () => { let called = false; - const s = schema.looseRecord(z.string(), () => { + const s = looseRecord(z.string(), () => { called = true; }); expect(s.parse('foobar')).toEqual({}); @@ -74,22 +80,76 @@ describe('util/schema-utils', () => { describe('looseValue', () => { it('parses value', () => { - const s = schema.looseValue(z.string()); + const s = looseValue(z.string()); expect(s.parse('foobar')).toBe('foobar'); }); it('falls back to null wrong value', () => { - const s = schema.looseValue(z.string()); + const s = looseValue(z.string()); expect(s.parse(123)).toBeNull(); }); it('runs callback for wrong elements', () => { let called = false; - const s = schema.looseValue(z.string(), () => { + const s = looseValue(z.string(), () => { called = true; }); expect(s.parse(123)).toBeNull(); expect(called).toBeTrue(); }); }); + + describe('parseJson', () => { + it('parses json', () => { + const res = parseJson('{"foo": "bar"}', z.object({ foo: z.string() })); + expect(res).toEqual({ foo: 'bar' }); + }); + + it('throws on invalid json', () => { + expect(() => + parseJson('{"foo": "bar"', z.object({ foo: z.string() })) + ).toThrow(SyntaxError); + }); + + it('throws on invalid schema', () => { + expect(() => + parseJson('{"foo": "bar"}', z.object({ foo: z.number() })) + ).toThrow(z.ZodError); + }); + }); + + describe('safeParseJson', () => { + it('parses json', () => { + const res = safeParseJson( + '{"foo": "bar"}', + z.object({ foo: z.string() }) + ); + expect(res).toEqual({ foo: 'bar' }); + }); + + it('returns null on invalid json', () => { + const res = safeParseJson('{"foo": "bar"', z.object({ foo: z.string() })); + expect(res).toBeNull(); + }); + + it('returns null on invalid schema', () => { + const res = safeParseJson( + '{"foo": "bar"}', + z.object({ foo: z.number() }) + ); + expect(res).toBeNull(); + }); + + it('runs callback on invalid json', () => { + const callback = jest.fn(); + safeParseJson('{"foo": "bar"', z.object({ foo: z.string() }), callback); + expect(callback).toHaveBeenCalledWith(expect.any(SyntaxError)); + }); + + it('runs callback on invalid schema', () => { + const callback = jest.fn(); + safeParseJson('{"foo": "bar"}', z.object({ foo: z.number() }), callback); + expect(callback).toHaveBeenCalledWith(expect.any(z.ZodError)); + }); + }); }); diff --git a/lib/util/schema-utils.ts b/lib/util/schema-utils.ts index 781cca9d8ea98162870c1c44c4438ab40e44b596..d8996cb2a9e7ff4d5f45d1ed0c1af75580f89587 100644 --- a/lib/util/schema-utils.ts +++ b/lib/util/schema-utils.ts @@ -90,3 +90,27 @@ export function looseValue<T, U extends z.ZodTypeDef, V>( : nullableSchema.catch(null); return schemaWithFallback; } + +export function parseJson< + T = unknown, + Schema extends z.ZodType<T> = z.ZodType<T> +>(input: string, schema: Schema): z.infer<Schema> { + const parsed = JSON.parse(input); + return schema.parse(parsed); +} + +export function safeParseJson< + T = unknown, + Schema extends z.ZodType<T> = z.ZodType<T> +>( + input: string, + schema: Schema, + catchCallback?: (e: SyntaxError | z.ZodError) => void +): z.infer<Schema> | null { + try { + return parseJson(input, schema); + } catch (err) { + catchCallback?.(err); + return null; + } +}