Skip to content

Commit 0c35eeb

Browse files
committed
Address review comments
1 parent c1663d4 commit 0c35eeb

File tree

2 files changed

+102
-15
lines changed

2 files changed

+102
-15
lines changed

src/json.test.ts

+77-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { validate, assert as superstructAssert, is, string } from 'superstruct';
1+
import {
2+
validate,
3+
assert as superstructAssert,
4+
is,
5+
string,
6+
union,
7+
literal,
8+
max,
9+
number,
10+
} from 'superstruct';
211

312
import {
413
assert,
@@ -111,6 +120,73 @@ describe('object', () => {
111120
});
112121
});
113122

123+
describe('exactOptional', () => {
124+
const simpleStruct = object({
125+
foo: exactOptional(string()),
126+
});
127+
128+
it.each([
129+
{ struct: simpleStruct, obj: {}, expected: true },
130+
{ struct: simpleStruct, obj: { foo: undefined }, expected: false },
131+
{ struct: simpleStruct, obj: { foo: 'hi' }, expected: true },
132+
{ struct: simpleStruct, obj: { bar: 'hi' }, expected: false },
133+
{ struct: simpleStruct, obj: { foo: 1 }, expected: false },
134+
])(
135+
'returns $expected for is($obj, <struct>)',
136+
({ struct, obj, expected }) => {
137+
expect(is(obj, struct)).toBe(expected);
138+
},
139+
);
140+
141+
const nestedStruct = object({
142+
foo: object({
143+
bar: exactOptional(string()),
144+
}),
145+
});
146+
147+
it.each([
148+
{ struct: nestedStruct, obj: { foo: {} }, expected: true },
149+
{ struct: nestedStruct, obj: { foo: { bar: 'hi' } }, expected: true },
150+
{
151+
struct: nestedStruct,
152+
obj: { foo: { bar: undefined } },
153+
expected: false,
154+
},
155+
])(
156+
'returns $expected for is($obj, <struct>)',
157+
({ struct, obj, expected }) => {
158+
expect(is(obj, struct)).toBe(expected);
159+
},
160+
);
161+
162+
const structWithUndefined = object({
163+
foo: exactOptional(union([string(), literal(undefined)])),
164+
});
165+
166+
it.each([
167+
{ struct: structWithUndefined, obj: {}, expected: true },
168+
{ struct: structWithUndefined, obj: { foo: undefined }, expected: true },
169+
{ struct: structWithUndefined, obj: { foo: 'hi' }, expected: true },
170+
{ struct: structWithUndefined, obj: { bar: 'hi' }, expected: false },
171+
{ struct: structWithUndefined, obj: { foo: 1 }, expected: false },
172+
])(
173+
'returns $expected for is($obj, <struct>)',
174+
({ struct, obj, expected }) => {
175+
expect(is(obj, struct)).toBe(expected);
176+
},
177+
);
178+
179+
it('supports refinements', () => {
180+
const struct = object({
181+
foo: exactOptional(max(number(), 0)),
182+
});
183+
184+
expect(is({ foo: 0 }, struct)).toBe(true);
185+
expect(is({ foo: -1 }, struct)).toBe(true);
186+
expect(is({ foo: 1 }, struct)).toBe(false);
187+
});
188+
});
189+
114190
describe('json', () => {
115191
beforeEach(() => {
116192
const actual = jest.requireActual('superstruct');

src/json.ts

+25-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Infer, Struct } from 'superstruct';
1+
import type { Context, Infer } from 'superstruct';
22
import {
33
any,
44
array,
@@ -18,6 +18,7 @@ import {
1818
string,
1919
union,
2020
unknown,
21+
Struct,
2122
} from 'superstruct';
2223
import type {
2324
ObjectSchema,
@@ -95,6 +96,19 @@ type ExactOptionalGuard = {
9596
_exactOptionalGuard?: typeof exactOptionalSymbol;
9697
};
9798

99+
/**
100+
* Check the last field of a path is present.
101+
*
102+
* @param context - The context to check.
103+
* @param context.path - The path to check.
104+
* @param context.branch - The branch to check.
105+
* @returns Whether the last field of a path is present.
106+
*/
107+
function hasOptional({ path, branch }: Context): boolean {
108+
const field = path[path.length - 1];
109+
return hasProperty(branch[branch.length - 2], field);
110+
}
111+
98112
/**
99113
* A struct to check if the given value is valid, or not present. This means
100114
* that it allows an object which does not have the property, or an object which
@@ -125,23 +139,20 @@ type ExactOptionalGuard = {
125139
* // }
126140
* ```
127141
*/
128-
export const exactOptional = <Type, Schema>(
142+
export function exactOptional<Type, Schema>(
129143
struct: Struct<Type, Schema>,
130-
): Struct<Type & ExactOptionalGuard, null> =>
131-
define('optional', (value, context) => {
132-
const parent = context.branch[context.branch.length - 2];
133-
const key = context.path[context.path.length - 1];
134-
135-
if (!hasProperty(parent, key)) {
136-
return true;
137-
}
144+
): Struct<Type & ExactOptionalGuard, Schema> {
145+
return new Struct<Type & ExactOptionalGuard, Schema>({
146+
...struct,
138147

139-
if (value === undefined) {
140-
return 'Expected a value, but received: undefined.';
141-
}
148+
type: `optional ${struct.type}`,
149+
validator: (value, context) =>
150+
!hasOptional(context) || struct.validator(value, context),
142151

143-
return struct.validator(value, context);
152+
refiner: (value, context) =>
153+
!hasOptional(context) || struct.refiner(value as Type, context),
144154
});
155+
}
145156

146157
/**
147158
* A struct to check if the given value is finite number. Superstruct's

0 commit comments

Comments
 (0)