Skip to content

Commit 48dc64f

Browse files
authored
Merge pull request #8 from andnp/ValidationErrors
feat: make validation errors available
2 parents 6fa752c + 9a5093b commit 48dc64f

File tree

4 files changed

+101
-6
lines changed

4 files changed

+101
-6
lines changed

src/index.ts

+37-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Schema } from 'type-level-schema/schema';
2-
import { objectKeys, Nominal, AnyFunc, AllRequired, Optional } from 'simplytyped';
1+
import { Schema, SchemaMetaData } from 'type-level-schema/schema';
2+
import { objectKeys, Nominal, AnyFunc, AllRequired, Optional, Unknown } from 'simplytyped';
33
import * as Ajv from 'ajv';
44

55
type ObjectValidator<O extends Record<string, Validator<any>>, OptionalKeys extends keyof O> = Optional<{
@@ -10,6 +10,16 @@ export type ObjectOptions<OptionalKeys> = Partial<{
1010
optional: OptionalKeys[];
1111
}>;
1212

13+
export interface ValidResult<T> {
14+
data: T;
15+
valid: true;
16+
}
17+
18+
export interface InvalidResult {
19+
errors: Ajv.ErrorObject[];
20+
valid: false;
21+
}
22+
1323
const once = <F extends AnyFunc>(f: F): F => {
1424
let ret: any;
1525
let called = false;
@@ -89,16 +99,37 @@ export default class Validator<T> {
8999
private constructor(private schema: Schema) {}
90100

91101
private getAjv = once(() => new Ajv());
102+
private getCompiledSchema = once(() => {
103+
const ajv = this.getAjv();
104+
const schema = this.getSchema();
105+
return ajv.compile(schema);
106+
});
92107

93108
getSchema(): Schema {
94109
return this.schema;
95110
}
96111

97-
isValid(thing: any): thing is T {
98-
const schema = this.getSchema();
99-
const ajv = this.getAjv();
112+
isValid(thing: Unknown): thing is T {
113+
const ajvValidator = this.getCompiledSchema();
100114

101-
return ajv.validate(schema, thing) as boolean;
115+
return ajvValidator(thing) as boolean;
116+
}
117+
118+
validate(data: Unknown): ValidResult<T> | InvalidResult {
119+
if (this.isValid(data)) {
120+
return { data, valid: true };
121+
}
122+
123+
const ajvValidator = this.getCompiledSchema();
124+
return { errors: ajvValidator.errors || [], valid: false };
125+
}
126+
127+
setSchemaMetaData(meta: Partial<SchemaMetaData>): this {
128+
this.schema = {
129+
...this.schema,
130+
...meta,
131+
};
132+
return this;
102133
}
103134
}
104135

tests/integration/api.test.ts

+23
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,26 @@ test('Validates a deep object', () => {
9595
fail();
9696
}
9797
});
98+
99+
test('Can add json schema metadata', () => {
100+
const validator = v.object({
101+
thing: v.string(),
102+
}).setSchemaMetaData({
103+
title: 'ThingSchema',
104+
description: 'This is a thing schema',
105+
});
106+
107+
const obj: any = { thing: 'hi' };
108+
const result = validator.validate(obj);
109+
if (result.valid) {
110+
expect(result.data).toEqual({ thing: 'hi' });
111+
} else {
112+
type Got = typeof result;
113+
interface Expected {
114+
valid: false;
115+
errors: Ajv.ErrorObject[];
116+
}
117+
assertTypesEqual<Got, Expected>();
118+
fail();
119+
}
120+
});

tests/integration/errors.test.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import v from 'index';
2+
3+
test('Will get back array of errors on invalid data', () => {
4+
const validator = v.object({
5+
thing: v.string(),
6+
});
7+
8+
const obj: any = { thing: 22 };
9+
10+
const result = validator.validate(obj);
11+
if (result.valid) fail();
12+
else {
13+
expect(result.errors.length).toBe(1);
14+
}
15+
});

tests/integration/scale.test.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import v from 'index';
2+
import { pass } from '../helpers/assert';
3+
4+
test('Ajv compiles only one instance of schema', () => {
5+
const validator = v.object({
6+
a: v.string([ 'a', 'b', 'c' ]),
7+
b: v.number(),
8+
c: v.boolean(),
9+
});
10+
11+
const t = Date.now();
12+
for (let i = 0; i < 2e4; ++i) {
13+
const o = {
14+
a: 'a',
15+
b: Math.random(),
16+
c: false,
17+
};
18+
19+
if (validator.isValid(o)) pass();
20+
else fail();
21+
}
22+
23+
const took = Date.now() - t;
24+
25+
expect(took).toBeLessThan(2000);
26+
});

0 commit comments

Comments
 (0)