Skip to content

Commit

Permalink
Add struct utils for validating JSON objects with optional values (#136)
Browse files Browse the repository at this point in the history
* Add struct utils for validating JSON objects with optional values

* Add type tests

* Update existing structs to use jsonObject struct

* Add test for coverage

* Fix type of InferWithParams

* Rename to object and exactOptional

* Fix type test

* Remove unused imports

* Rename test names

* Address review comments

* Update src/json.test.ts

Co-authored-by: Elliot Winkler <[email protected]>

* Update src/json.ts

Co-authored-by: Elliot Winkler <[email protected]>

* Update snapshots

* Move some tests

---------

Co-authored-by: Elliot Winkler <[email protected]>
  • Loading branch information
Mrtenz and mcmire authored Oct 19, 2023
1 parent 12b5003 commit 0d68084
Show file tree
Hide file tree
Showing 5 changed files with 339 additions and 10 deletions.
2 changes: 2 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ describe('index', () => {
"createModuleLogger",
"createNumber",
"createProjectLogger",
"exactOptional",
"getChecksumAddress",
"getJsonRpcIdValidator",
"getJsonSize",
Expand Down Expand Up @@ -115,6 +116,7 @@ describe('index', () => {
"jsonrpc2",
"numberToBytes",
"numberToHex",
"object",
"parseCaipAccountId",
"parseCaipChainId",
"remove0x",
Expand Down
23 changes: 23 additions & 0 deletions src/json.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */

import type { Infer } from 'superstruct';
import { boolean, number, optional, string } from 'superstruct';
import { expectAssignable, expectNotAssignable } from 'tsd';

import type { Json } from '.';
import { exactOptional, object } from '.';

// Valid Json:

Expand Down Expand Up @@ -134,3 +137,23 @@ class Foo {
}
const foo = new Foo();
expectNotAssignable<Json>(foo);

// Object using `exactOptional`:

const exactOptionalObject = object({
a: number(),
b: optional(string()),
c: exactOptional(boolean()),
});

type ExactOptionalObject = Infer<typeof exactOptionalObject>;

expectAssignable<ExactOptionalObject>({ a: 0 });
expectAssignable<ExactOptionalObject>({ a: 0, b: 'test' });
expectAssignable<ExactOptionalObject>({ a: 0, b: 'test', c: true });
expectNotAssignable<ExactOptionalObject>({ a: 0, b: 'test', c: 0 });
expectNotAssignable<ExactOptionalObject>({
a: 0,
b: 'test',
c: undefined,
});
186 changes: 185 additions & 1 deletion src/json.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { validate, assert as superstructAssert } from 'superstruct';
import {
validate,
assert as superstructAssert,
is,
string,
union,
literal,
max,
number,
optional,
} from 'superstruct';

import {
assert,
Expand All @@ -20,6 +30,8 @@ import {
isJsonRpcSuccess,
isPendingJsonRpcResponse,
isValidJson,
object,
exactOptional,
JsonStruct,
} from '.';
import {
Expand All @@ -39,6 +51,178 @@ jest.mock('superstruct', () => ({
assert: jest.fn(),
}));

describe('object', () => {
it('validates an object', () => {
expect(
is(
{
foo: 'bar',
},
object({
foo: string(),
}),
),
).toBe(true);

expect(
is(
{
foo: 123,
},
object({
foo: string(),
}),
),
).toBe(false);
});

it('validates an object with exact optional values', () => {
expect(
is(
{
foo: 'bar',
},
object({
foo: exactOptional(string()),
}),
),
).toBe(true);

expect(
is(
{},
object({
foo: exactOptional(string()),
}),
),
).toBe(true);

expect(
is(
{
foo: undefined,
},
object({
foo: exactOptional(string()),
}),
),
).toBe(false);
});

it('validates an object with other values', () => {
expect(
is(
{
foo: 123,
},
object({
foo: number(),
}),
),
).toBe(true);

expect(
is(
{
foo: 123,
},
object({
foo: string(),
}),
),
).toBe(false);

expect(
is(
{
foo: undefined,
},
object({
foo: optional(string()),
}),
),
).toBe(true);

expect(
is(
{
foo: 'bar',
},
object({
foo: optional(string()),
}),
),
).toBe(true);
});
});

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
Loading

0 comments on commit 0d68084

Please sign in to comment.