Skip to content

Commit

Permalink
Address review comments
Browse files Browse the repository at this point in the history
  • Loading branch information
Mrtenz committed Sep 5, 2023
1 parent 48cf8d0 commit 035998d
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 15 deletions.
78 changes: 77 additions & 1 deletion src/json.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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>)',
({ 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>)',
({ 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>)',
({ 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');
Expand Down
39 changes: 25 additions & 14 deletions src/json.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Infer, Struct } from 'superstruct';
import type { Context, Infer } from 'superstruct';
import {
any,
array,
Expand All @@ -18,6 +18,7 @@ import {
string,
union,
unknown,
Struct,
} from 'superstruct';
import type {
ObjectSchema,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -125,23 +139,20 @@ type ExactOptionalGuard = {
* // }
* ```
*/
export const exactOptional = <Type, Schema>(
export function exactOptional<Type, Schema>(
struct: Struct<Type, Schema>,
): Struct<Type & ExactOptionalGuard, null> =>
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<Type & ExactOptionalGuard, Schema> {
return new Struct<Type & ExactOptionalGuard, Schema>({
...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
Expand Down

0 comments on commit 035998d

Please sign in to comment.