From 035998d2daf2366dd72a2a63f6eb09692f677b02 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Tue, 5 Sep 2023 17:53:56 +0200 Subject: [PATCH] Address review comments --- src/json.test.ts | 78 +++++++++++++++++++++++++++++++++++++++++++++++- src/json.ts | 39 +++++++++++++++--------- 2 files changed, 102 insertions(+), 15 deletions(-) diff --git a/src/json.test.ts b/src/json.test.ts index f6076590..56e812ca 100644 --- a/src/json.test.ts +++ b/src/json.test.ts @@ -1,4 +1,13 @@ -import { validate, assert as superstructAssert, is, string } from 'superstruct'; +import { + validate, + assert as superstructAssert, + is, + string, + union, + literal, + max, + number, +} from 'superstruct'; import { assert, @@ -111,6 +120,73 @@ describe('object', () => { }); }); +describe('exactOptional', () => { + const simpleStruct = object({ + foo: exactOptional(string()), + }); + + it.each([ + { struct: simpleStruct, obj: {}, expected: true }, + { struct: simpleStruct, obj: { foo: undefined }, expected: false }, + { struct: simpleStruct, obj: { foo: 'hi' }, expected: true }, + { struct: simpleStruct, obj: { bar: 'hi' }, expected: false }, + { struct: simpleStruct, obj: { foo: 1 }, expected: false }, + ])( + 'returns $expected for is($obj, )', + ({ struct, obj, expected }) => { + expect(is(obj, struct)).toBe(expected); + }, + ); + + const nestedStruct = object({ + foo: object({ + bar: exactOptional(string()), + }), + }); + + it.each([ + { struct: nestedStruct, obj: { foo: {} }, expected: true }, + { struct: nestedStruct, obj: { foo: { bar: 'hi' } }, expected: true }, + { + struct: nestedStruct, + obj: { foo: { bar: undefined } }, + expected: false, + }, + ])( + 'returns $expected for is($obj, )', + ({ struct, obj, expected }) => { + expect(is(obj, struct)).toBe(expected); + }, + ); + + const structWithUndefined = object({ + foo: exactOptional(union([string(), literal(undefined)])), + }); + + it.each([ + { struct: structWithUndefined, obj: {}, expected: true }, + { struct: structWithUndefined, obj: { foo: undefined }, expected: true }, + { struct: structWithUndefined, obj: { foo: 'hi' }, expected: true }, + { struct: structWithUndefined, obj: { bar: 'hi' }, expected: false }, + { struct: structWithUndefined, obj: { foo: 1 }, expected: false }, + ])( + 'returns $expected for is($obj, )', + ({ struct, obj, expected }) => { + expect(is(obj, struct)).toBe(expected); + }, + ); + + it('supports refinements', () => { + const struct = object({ + foo: exactOptional(max(number(), 0)), + }); + + expect(is({ foo: 0 }, struct)).toBe(true); + expect(is({ foo: -1 }, struct)).toBe(true); + expect(is({ foo: 1 }, struct)).toBe(false); + }); +}); + describe('json', () => { beforeEach(() => { const actual = jest.requireActual('superstruct'); diff --git a/src/json.ts b/src/json.ts index b56a4769..6235b3b6 100644 --- a/src/json.ts +++ b/src/json.ts @@ -1,4 +1,4 @@ -import type { Infer, Struct } from 'superstruct'; +import type { Context, Infer } from 'superstruct'; import { any, array, @@ -18,6 +18,7 @@ import { string, union, unknown, + Struct, } from 'superstruct'; import type { ObjectSchema, @@ -95,6 +96,19 @@ type ExactOptionalGuard = { _exactOptionalGuard?: typeof exactOptionalSymbol; }; +/** + * Check the last field of a path is present. + * + * @param context - The context to check. + * @param context.path - The path to check. + * @param context.branch - The branch to check. + * @returns Whether the last field of a path is present. + */ +function hasOptional({ path, branch }: Context): boolean { + const field = path[path.length - 1]; + return hasProperty(branch[branch.length - 2], field); +} + /** * A struct to check if the given value is valid, or not present. This means * that it allows an object which does not have the property, or an object which @@ -125,23 +139,20 @@ type ExactOptionalGuard = { * // } * ``` */ -export const exactOptional = ( +export function exactOptional( struct: Struct, -): Struct => - define('optional', (value, context) => { - const parent = context.branch[context.branch.length - 2]; - const key = context.path[context.path.length - 1]; - - if (!hasProperty(parent, key)) { - return true; - } +): Struct { + return new Struct({ + ...struct, - if (value === undefined) { - return 'Expected a value, but received: undefined.'; - } + type: `optional ${struct.type}`, + validator: (value, context) => + !hasOptional(context) || struct.validator(value, context), - return struct.validator(value, context); + refiner: (value, context) => + !hasOptional(context) || struct.refiner(value as Type, context), }); +} /** * A struct to check if the given value is finite number. Superstruct's