Skip to content

Commit

Permalink
Verbose validation error message (#296)
Browse files Browse the repository at this point in the history
* improve validation error logging

* update tests

* style: format

* fix: indent details message properly

* test: use `outdent` for testing multiline messages

* refactor: remove unnecessary escaping

Co-authored-by: Yu Shimura <[email protected]>
  • Loading branch information
HzrdIRL and yuhr authored Aug 11, 2022
1 parent 60156b0 commit dcd4fe0
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 24 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"coveralls": "^3.1.0",
"jest": "26.6.3",
"jest-github-actions-reporter": "^1.0.3",
"outdent": "^0.8.0",
"prettier": "^2.2.1",
"ts-jest": "^26.5.4",
"typescript": "4.5.2"
Expand Down
174 changes: 155 additions & 19 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import { Constructor } from './types/instanceof';
import { ValidationError } from './errors';
import { Details, Failcode } from './result';

import outdent from 'outdent';

const boolTuple = Tuple(Boolean, Boolean, Boolean);
const record1 = Record({ Boolean, Number });
const union1 = Union(Literal(3), String, boolTuple, record1);
Expand Down Expand Up @@ -384,7 +386,13 @@ describe('check errors', () => {
[false, '0', true],
Tuple(Number, String, Boolean),
Failcode.CONTENT_INCORRECT,
'Expected [number, string, boolean], but was incompatible',
outdent`
Validation failed:
[
"Expected number, but was boolean"
].
Object should match [number, string, boolean]
`,
{ 0: 'Expected number, but was boolean' },
);
});
Expand All @@ -403,7 +411,15 @@ describe('check errors', () => {
[0, { name: 0 }],
Tuple(Number, Record({ name: String })),
Failcode.CONTENT_INCORRECT,
'Expected [number, { name: string; }], but was incompatible',
outdent`
Validation failed:
[
{
"name": "Expected string, but was number"
}
].
Object should match [number, { name: string; }]
`,
{ 1: { name: 'Expected string, but was number' } },
);
});
Expand All @@ -417,7 +433,13 @@ describe('check errors', () => {
[0, 2, 'test'],
Array(Number),
Failcode.CONTENT_INCORRECT,
'Expected number[], but was incompatible',
outdent`
Validation failed:
[
"Expected number, but was string"
].
Object should match number[]
`,
{ 2: 'Expected number, but was string' },
);
});
Expand All @@ -427,7 +449,15 @@ describe('check errors', () => {
[{ name: 'Foo' }, { name: false }],
Array(Record({ name: String })),
Failcode.CONTENT_INCORRECT,
'Expected { name: string; }[], but was incompatible',
outdent`
Validation failed:
[
{
"name": "Expected string, but was boolean"
}
].
Object should match { name: string; }[]
`,
{ 1: { name: 'Expected string, but was boolean' } },
);
});
Expand All @@ -437,7 +467,13 @@ describe('check errors', () => {
[{ name: 'Foo' }, null],
Array(Record({ name: String })),
Failcode.CONTENT_INCORRECT,
'Expected { name: string; }[], but was incompatible',
outdent`
Validation failed:
[
"Expected { name: string; }, but was null"
].
Object should match { name: string; }[]
`,
{ 1: 'Expected { name: string; }, but was null' },
);
});
Expand All @@ -447,7 +483,13 @@ describe('check errors', () => {
[0, 2, 'test'],
Array(Number).asReadonly(),
Failcode.CONTENT_INCORRECT,
'Expected readonly number[], but was incompatible',
outdent`
Validation failed:
[
"Expected number, but was string"
].
Object should match readonly number[]
`,
{ 2: 'Expected number, but was string' },
);
});
Expand All @@ -457,7 +499,15 @@ describe('check errors', () => {
[{ name: 'Foo' }, { name: false }],
Array(Record({ name: String })).asReadonly(),
Failcode.CONTENT_INCORRECT,
'Expected readonly { name: string; }[], but was incompatible',
outdent`
Validation failed:
[
{
"name": "Expected string, but was boolean"
}
].
Object should match readonly { name: string; }[]
`,
{ 1: { name: 'Expected string, but was boolean' } },
);
});
Expand All @@ -467,7 +517,13 @@ describe('check errors', () => {
[{ name: 'Foo' }, null],
Array(Record({ name: String })).asReadonly(),
Failcode.CONTENT_INCORRECT,
'Expected readonly { name: string; }[], but was incompatible',
outdent`
Validation failed:
[
"Expected { name: string; }, but was null"
].
Object should match readonly { name: string; }[]
`,
{ 1: 'Expected { name: string; }, but was null' },
);
});
Expand Down Expand Up @@ -501,7 +557,15 @@ describe('check errors', () => {
{ foo: { name: false } },
Dictionary(Record({ name: String })),
Failcode.CONTENT_INCORRECT,
'Expected { [_: string]: { name: string; } }, but was incompatible',
outdent`
Validation failed:
{
"foo": {
"name": "Expected string, but was boolean"
}
}.
Object should match { [_: string]: { name: string; } }
`,
{ foo: { name: 'Expected string, but was boolean' } },
);
});
Expand All @@ -511,7 +575,13 @@ describe('check errors', () => {
{ foo: 'bar', test: true },
Dictionary(String),
Failcode.CONTENT_INCORRECT,
'Expected { [_: string]: string }, but was incompatible',
outdent`
Validation failed:
{
"test": "Expected string, but was boolean"
}.
Object should match { [_: string]: string }
`,
{ test: 'Expected string, but was boolean' },
);
});
Expand All @@ -521,7 +591,13 @@ describe('check errors', () => {
{ 1: 'bar', 2: 20 },
Dictionary(String, 'number'),
Failcode.CONTENT_INCORRECT,
'Expected { [_: number]: string }, but was incompatible',
outdent`
Validation failed:
{
"2": "Expected string, but was number"
}.
Object should match { [_: number]: string }
`,
{ 2: 'Expected string, but was number' },
);
});
Expand All @@ -534,7 +610,13 @@ describe('check errors', () => {
age: Number,
}),
Failcode.CONTENT_INCORRECT,
'Expected { name: string; age: number; }, but was incompatible',
outdent`
Validation failed:
{
"age": "Expected number, but was string"
}.
Object should match { name: string; age: number; }
`,
{ age: 'Expected number, but was string' },
);
});
Expand All @@ -559,7 +641,13 @@ describe('check errors', () => {
age: Number,
}),
Failcode.CONTENT_INCORRECT,
'Expected { name: string; age: number; }, but was incompatible',
outdent`
Validation failed:
{
"age": "Expected number, but was missing"
}.
Object should match { name: string; age: number; }
`,
{ age: 'Expected number, but was missing' },
);
});
Expand All @@ -573,7 +661,17 @@ describe('check errors', () => {
likes: Array(Record({ title: String })),
}),
Failcode.CONTENT_INCORRECT,
'Expected { name: string; age: number; likes: { title: string; }[]; }, but was incompatible',
outdent`
Validation failed:
{
"likes": [
{
"title": "Expected string, but was boolean"
}
]
}.
Object should match { name: string; age: number; likes: { title: string; }[]; }
`,
{ likes: { 0: { title: 'Expected string, but was boolean' } } },
);
});
Expand All @@ -586,7 +684,13 @@ describe('check errors', () => {
age: Number,
}).asReadonly(),
Failcode.CONTENT_INCORRECT,
'Expected { readonly name: string; readonly age: number; }, but was incompatible',
outdent`
Validation failed:
{
"age": "Expected number, but was string"
}.
Object should match { readonly name: string; readonly age: number; }
`,
{ age: 'Expected number, but was string' },
);
});
Expand All @@ -599,7 +703,13 @@ describe('check errors', () => {
age: Number,
}).asReadonly(),
Failcode.CONTENT_INCORRECT,
'Expected { readonly name: string; readonly age: number; }, but was incompatible',
outdent`
Validation failed:
{
"age": "Expected number, but was missing"
}.
Object should match { readonly name: string; readonly age: number; }
`,
{ age: 'Expected number, but was missing' },
);
});
Expand All @@ -613,7 +723,17 @@ describe('check errors', () => {
likes: Array(Record({ title: String }).asReadonly()),
}).asReadonly(),
Failcode.CONTENT_INCORRECT,
'Expected { readonly name: string; readonly age: number; readonly likes: { readonly title: string; }[]; }, but was incompatible',
outdent`
Validation failed:
{
"likes": [
{
"title": "Expected string, but was boolean"
}
]
}.
Object should match { readonly name: string; readonly age: number; readonly likes: { readonly title: string; }[]; }
`,
{ likes: { 0: { title: 'Expected string, but was boolean' } } },
);
});
Expand All @@ -626,7 +746,13 @@ describe('check errors', () => {
age: Number,
}),
Failcode.CONTENT_INCORRECT,
'Expected { name?: string; age?: number; }, but was incompatible',
outdent`
Validation failed:
{
"age": "Expected number, but was null"
}.
Object should match { name?: string; age?: number; }
`,
{ age: 'Expected number, but was null' },
);
});
Expand All @@ -640,7 +766,17 @@ describe('check errors', () => {
likes: Array(Record({ title: String })),
}),
Failcode.CONTENT_INCORRECT,
'Expected { name?: string; age?: number; likes?: { title: string; }[]; }, but was incompatible',
outdent`
Validation failed:
{
"likes": [
{
"title": "Expected string, but was number"
}
]
}.
Object should match { name?: string; age?: number; likes?: { title: string; }[]; }
`,
{ likes: { 0: { title: 'Expected string, but was number' } } },
);
});
Expand Down
27 changes: 23 additions & 4 deletions src/types/union.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Union, String, Literal, Record, Number, InstanceOf } from '..';
import { Failcode } from '../result';
import { Static } from '../runtype';
import { LiteralBase } from './literal';
import outdent from 'outdent';

const ThreeOrString = Union(Literal(3), String);

Expand Down Expand Up @@ -48,15 +49,27 @@ describe('union', () => {
expect(Shape.validate({ kind: 'square', size: new Date() })).toMatchObject({
success: false,
code: Failcode.CONTENT_INCORRECT,
message: 'Expected { kind: "square"; size: number; }, but was incompatible',
message: outdent`
Validation failed:
{
"size": "Expected number, but was Date"
}.
Object should match { kind: "square"; size: number; }
`,
details: { size: 'Expected number, but was Date' },
});

expect(Shape.validate({ kind: 'rectangle', size: new Date() })).toMatchObject({
success: false,
code: Failcode.CONTENT_INCORRECT,
message:
'Expected { kind: "rectangle"; width: number; height: number; }, but was incompatible',
message: outdent`
Validation failed:
{
"width": "Expected number, but was missing",
"height": "Expected number, but was missing"
}.
Object should match { kind: "rectangle"; width: number; height: number; }
`,
details: {
width: 'Expected number, but was missing',
height: 'Expected number, but was missing',
Expand All @@ -66,7 +79,13 @@ describe('union', () => {
expect(Shape.validate({ kind: 'circle', size: new Date() })).toMatchObject({
success: false,
code: Failcode.CONTENT_INCORRECT,
message: 'Expected { kind: "circle"; radius: number; }, but was incompatible',
message: outdent`
Validation failed:
{
"radius": "Expected number, but was missing"
}.
Object should match { kind: "circle"; radius: number; }
`,
details: { radius: 'Expected number, but was missing' },
});

Expand Down
3 changes: 2 additions & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ export const FAILURE = Object.assign(
);
},
CONTENT_INCORRECT: (self: Reflect, details: Details) => {
const message = `Expected ${show(self)}, but was incompatible`;
const formattedDetails = JSON.stringify(details, null, 2).replace(/^ *null,\n/gm, '');
const message = `Validation failed:\n${formattedDetails}.\nObject should match ${show(self)}`;
return FAILURE(Failcode.CONTENT_INCORRECT, message, details);
},
ARGUMENT_INCORRECT: (message: string) => {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2696,6 +2696,11 @@ optionator@^0.8.1:
type-check "~0.3.2"
word-wrap "~1.2.3"

outdent@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/outdent/-/outdent-0.8.0.tgz#2ebc3e77bf49912543f1008100ff8e7f44428eb0"
integrity sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==

p-each-series@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a"
Expand Down

0 comments on commit dcd4fe0

Please sign in to comment.