Skip to content

Commit 64a0957

Browse files
author
andy.patterson
committed
feat: allow combining multiple validators
1 parent 9a5093b commit 64a0957

File tree

6 files changed

+137
-3
lines changed

6 files changed

+137
-3
lines changed

src/index.ts

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

5-
type ObjectValidator<O extends Record<string, Validator<any>>, OptionalKeys extends keyof O> = Optional<{
5+
type ObjectValidator<O extends Record<string, Validator<any>>> = {
66
[S in keyof O]: ValidType<O[S]>;
7-
}, OptionalKeys>;
7+
};
8+
9+
type OptionalObjectValidator<O extends Record<string, Validator<any>>, OptionalKeys extends keyof O> = Optional<ObjectValidator<O>, OptionalKeys>;
810

911
export type ObjectOptions<OptionalKeys> = Partial<{
1012
optional: OptionalKeys[];
@@ -50,10 +52,16 @@ export default class Validator<T> {
5052
return new Validator({ type: 'boolean' });
5153
}
5254

55+
static any(): Validator<any> {
56+
return new Validator({});
57+
}
58+
5359
static nominal<T, S extends string>(v: Validator<T>, s: S): Validator<Nominal<T, S>> {
5460
return new Validator(v.getSchema());
5561
}
5662

63+
static object<O extends Record<string, Validator<any>>>(o: O): Validator<ObjectValidator<O>>;
64+
static object<O extends Record<string, Validator<any>>, OptionalKeys extends keyof O = never>(o: O, opts?: ObjectOptions<OptionalKeys>): Validator<OptionalObjectValidator<O, OptionalKeys>>;
5765
static object<O extends Record<string, Validator<any>>, OptionalKeys extends keyof O = never>(o: O, opts?: ObjectOptions<OptionalKeys>) {
5866
const options: AllRequired<ObjectOptions<OptionalKeys>> = {
5967
optional: [] as OptionalKeys[],
@@ -68,7 +76,7 @@ export default class Validator<T> {
6876

6977
const required = Object.keys(o).filter(key => !options.optional.includes(key as OptionalKeys));
7078

71-
return new Validator<ObjectValidator<O, OptionalKeys>>({
79+
return new Validator({
7280
type: 'object',
7381
properties,
7482
required,
@@ -82,6 +90,16 @@ export default class Validator<T> {
8290
});
8391
}
8492

93+
static partial<T extends Record<string, any>>(v: Validator<T>): Validator<Partial<T>> {
94+
const schema = v.getSchema();
95+
if (!('type' in schema)) throw new Error('Must apply partial only to a type definition');
96+
if (schema.type !== 'object') throw new Error('Must only apply partial to an object schema');
97+
return new Validator({
98+
...schema,
99+
required: [],
100+
});
101+
}
102+
85103
static array<V extends Validator<any>>(v: V[]) {
86104
return new Validator<Array<ValidType<V>>>({
87105
type: 'array',
@@ -96,6 +114,10 @@ export default class Validator<T> {
96114
});
97115
}
98116

117+
static intersect<T1, T2>(v1: Validator<T1>, v2: Validator<T2>) {
118+
return new Validator<T1 & T2>({ allOf: [v1.getSchema(), v2.getSchema()] });
119+
}
120+
99121
private constructor(private schema: Schema) {}
100122

101123
private getAjv = once(() => new Ajv());
@@ -131,6 +153,14 @@ export default class Validator<T> {
131153
};
132154
return this;
133155
}
156+
157+
or<V extends Validator<any>>(v: V): Validator<T | ValidType<V>> {
158+
return Validator.union([this, v]);
159+
}
160+
161+
and<V extends Validator<any>>(v: V): Validator<T & ValidType<V>> {
162+
return Validator.intersect(this, v);
163+
}
134164
}
135165

136166
export type ValidType<V extends Validator<any>> = V extends Validator<infer T> ? T : never;

tests/unit/and.test.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import v from 'index';
2+
import { fail, pass, assertTypesEqual } from '../helpers/assert';
3+
4+
test('Can combine two schema', () => {
5+
const x: any = { a: '', b: 22 };
6+
7+
const validator = v.object({
8+
a: v.string(),
9+
}).and(v.object({
10+
b: v.number(),
11+
}));
12+
13+
if (validator.isValid(x)) {
14+
type got = typeof x;
15+
type expected = { a: string, b: number };
16+
assertTypesEqual<got, expected>();
17+
assertTypesEqual<expected, got>();
18+
pass();
19+
} else {
20+
fail();
21+
}
22+
});

tests/unit/any.test.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import v from 'index';
2+
import { pass } from '../helpers/assert';
3+
4+
test('Can validate an any type', () => {
5+
const things: any[] = [
6+
22,
7+
'yellow',
8+
false,
9+
{},
10+
undefined,
11+
null,
12+
() => ({}),
13+
];
14+
15+
const validator = v.any();
16+
17+
things.forEach(thing => {
18+
if (validator.isValid(thing)) pass();
19+
else fail();
20+
});
21+
22+
});

tests/unit/intersect.test.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import v from 'index';
2+
import { fail, pass, assertTypesEqual } from '../helpers/assert';
3+
4+
test('Can validate an intersection of objects', () => {
5+
const x: any = { a: '', b: 22 };
6+
7+
const v1 = v.object({ a: v.string() });
8+
const v2 = v.object({ b: v.number() });
9+
10+
const validator = v.intersect(v1, v2);
11+
12+
if (validator.isValid(x)) {
13+
type got = typeof x;
14+
type expected = { a: string, b: number };
15+
assertTypesEqual<got, expected>();
16+
assertTypesEqual<expected, got>();
17+
pass();
18+
} else {
19+
fail();
20+
}
21+
});

tests/unit/or.test.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import v from 'index';
2+
import { fail, pass, assertTypesEqual } from '../helpers/assert';
3+
4+
test('Can combine two schema', () => {
5+
const x: any = 'hi';
6+
7+
const validator = v.string().or(v.number());
8+
9+
if (validator.isValid(x)) {
10+
type got = typeof x;
11+
type expected = string | number;
12+
assertTypesEqual<got, expected>();
13+
assertTypesEqual<expected, got>();
14+
pass();
15+
} else {
16+
fail();
17+
}
18+
});

tests/unit/partial.test.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import v from 'index';
2+
import { fail, pass, assertTypesEqual } from '../helpers/assert';
3+
4+
test('Can validate a partial object', () => {
5+
const x: any = { a: '' };
6+
7+
const validator = v.partial(v.object({
8+
a: v.string(),
9+
b: v.number(),
10+
}));
11+
12+
if (validator.isValid(x)) {
13+
type got = typeof x;
14+
type expected = { a?: string, b?: number };
15+
assertTypesEqual<got, expected>();
16+
assertTypesEqual<expected, got>();
17+
pass();
18+
} else {
19+
fail();
20+
}
21+
});

0 commit comments

Comments
 (0)