Skip to content

Commit 91068f1

Browse files
committed
feat: make validating async
Running validate now will return a promise with the validation result. This allows us to support constraints that return promises. BREAKING CHANGE: validate() now returns a promise.
1 parent 62062e0 commit 91068f1

20 files changed

+471
-419
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@
7474
"dependencies": {
7575
"date-fns": "^2.0.0-0",
7676
"lodash-es": "^4.17.11",
77+
"p-map": "^2.0.0",
78+
"p-map-series": "^1.0.0",
7779
"tslib": "^1.8.3"
7880
},
7981
"devDependencies": {
@@ -83,6 +85,7 @@
8385
"@semantic-release/git": "^7.0.4",
8486
"@types/jest": "^23.3.2",
8587
"@types/lodash-es": "^4.17.1",
88+
"@types/p-map-series": "^1.0.1",
8689
"babel-plugin-lodash": "^3.3.4",
8790
"commitizen": "^2.10.1",
8891
"jest": "^23.6.0",

rollup.config.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,15 @@ export default ({ 'config-visualize': visualize }) => {
3232
plugins: [
3333
resolve(),
3434
commonjs(),
35-
typescript({ target, tsconfig: 'tsconfig.build.json' }),
35+
typescript({
36+
target,
37+
tsconfig: 'tsconfig.build.json',
38+
include: [
39+
'src/**',
40+
'node_modules/lodash-es',
41+
'node_modules/p-*/**',
42+
],
43+
}),
3644
babel({ extensions: ['.ts'] }),
3745
].concat(
3846
minify ? uglify() : [],

src/groups/has-elements.spec.ts

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,69 @@
11
import { maxLength, string } from '../rules';
22
import { validate } from '../validate';
33
import { hasElements } from './has-elements';
4+
import pMapSeries = require('p-map-series');
45

56
const rules = [string(), maxLength(10)];
67

8+
function validateEach(values: any, constraints = rules) {
9+
return pMapSeries(values, async value => (await validate(value, constraints)).all());
10+
}
11+
712
describe('hasElements', () => {
8-
it('should pass null and undefined', () => {
9-
expect(validate(null, hasElements(rules)).passed()).toBe(true);
10-
expect(validate(undefined, hasElements(rules)).passed()).toBe(true);
13+
it('should pass null and undefined', async () => {
14+
expect((await validate(null, hasElements(rules))).passed()).toBe(true);
15+
expect((await validate(undefined, hasElements(rules))).passed()).toBe(true);
1116
});
1217

13-
it('should an array of results', () => {
18+
it('should return an array of results', async () => {
1419
const values = [1, 2];
15-
const messages = validate(values, hasElements(rules)).all();
16-
expect(messages).toEqual(values.map(value => validate(value, rules).all()));
20+
const result = await validate(values, hasElements(rules));
21+
const expectedMessages = await validateEach(values);
22+
expect(result.all()).toEqual(expectedMessages);
1723
});
1824

19-
it('should validate each object individually', () => {
25+
it('should validate each object individually', async () => {
2026
const values = ['hello', 5, 'world'];
21-
const messages = validate(values, hasElements(rules)).all();
22-
expect(messages).toEqual(values.map(value => validate(value, rules).all()));
27+
const result = await validate(values, hasElements(rules));
28+
const expectedMessages = await validateEach(values);
29+
expect(result.all()).toEqual(expectedMessages);
2330
});
2431

25-
it('should work on all array like objects', () => {
32+
it('should work on all array like objects', async () => {
2633
const string = 'hello';
27-
let messages = validate(string, hasElements(rules)).all();
28-
expect(messages).toEqual(Array.from(string).map(value => validate(value, rules).all()));
34+
let result = await validate(string, hasElements(rules));
35+
expect(result.all()).toEqual(await validateEach(Array.from(string)));
2936

3037
const object = { 1: 'h', 2: 'i', length: 2 };
31-
messages = validate(object, hasElements(rules)).all();
32-
expect(messages).toEqual(Array.from(object).map(value => validate(value, rules).all()));
38+
result = await validate(object, hasElements(rules));
39+
expect(result.all()).toEqual(await validateEach(Array.from(object)));
3340
});
3441

35-
it('should correctly pass the key option', () => {
42+
it('should correctly pass the key option', async () => {
3643
const constraint = jest.fn(() => undefined);
3744
const parent = ['a', 'b', 'c'];
38-
validate(parent, hasElements([constraint]));
45+
await validate(parent, hasElements([constraint]));
3946
parent.forEach((value, index) => {
4047
const options = expect.objectContaining({ key: `${index}` });
4148
expect(constraint).nthCalledWith(index + 1, value, options);
4249
});
4350
});
4451

45-
it('should correctly pass the key path option', () => {
52+
it('should correctly pass the key path option', async () => {
4653
const constraint = jest.fn(() => undefined);
4754
const parent = ['a', 'b', 'c'];
48-
validate(parent, hasElements([constraint]));
55+
await validate(parent, hasElements([constraint]));
4956
parent.forEach((value, index) => {
5057
const options = expect.objectContaining({ keyPath: [`${index}`] });
5158
expect(constraint).nthCalledWith(index + 1, value, options);
5259
});
5360
});
5461

55-
it('should correctly pass the key path option when nested', () => {
62+
it('should correctly pass the key path option when nested', async () => {
5663
const constraint = jest.fn(() => undefined);
5764
const child = ['a', 'b', 'c'];
5865
const parent = [child, child, child];
59-
validate(parent, hasElements(hasElements([constraint])));
66+
await validate(parent, hasElements(hasElements([constraint])));
6067
parent.forEach((_, parentIndex) => {
6168
child.forEach((value, childIndex) => {
6269
const index = parentIndex * child.length + childIndex;
@@ -66,10 +73,10 @@ describe('hasElements', () => {
6673
});
6774
});
6875

69-
it('should correctly pass the parent option', () => {
76+
it('should correctly pass the parent option', async () => {
7077
const constraint = jest.fn(() => undefined);
7178
const parent = ['a', 'b', 'c'];
72-
validate(parent, hasElements([constraint]));
79+
await validate(parent, hasElements([constraint]));
7380
parent.forEach((value, index) => {
7481
const options = expect.objectContaining({ parent });
7582
expect(constraint).nthCalledWith(index + 1, value, options);

src/groups/has-elements.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
1+
import pMap from 'p-map';
2+
import { isArrayLike } from 'lodash-es';
13
import { InternalConstraint, GroupConstraint, ConstraintOptions } from '../make-constraint';
24
import { runConstraints } from '../validate';
3-
import { isArrayLike } from 'lodash-es';
45

56
export function hasElements<T = string[]>(
67
constraints: InternalConstraint[] | GroupConstraint<T>,
78
): GroupConstraint<(T | undefined)[]> {
8-
return (value, options) => {
9-
const childOptions: ConstraintOptions = {
10-
...options,
11-
parent: value,
12-
};
13-
if (isArrayLike(value)) {
14-
// Need to use a normal for loop to support all array like objects
15-
const messages: (T | undefined)[] = Array(value.length);
16-
for (let i = 0; i < messages.length; i += 1) {
17-
const key = `${i}`;
18-
messages[i] = runConstraints(value[i], constraints, {
19-
...childOptions,
20-
key,
21-
keyPath: [...childOptions.keyPath, key],
22-
});
23-
}
24-
return messages;
9+
return (values, options) => {
10+
if (!isArrayLike(values)) {
11+
return undefined;
2512
}
13+
14+
const childOptions: ConstraintOptions = { ...options, parent: values };
15+
// Need to use Array.from to support all array like objects
16+
return pMap(Array.from(values), (value, index) => {
17+
const key = index.toString();
18+
return runConstraints(value, constraints, {
19+
...childOptions,
20+
key,
21+
keyPath: [...childOptions.keyPath, key],
22+
});
23+
});
2624
};
2725
}

src/groups/has-properties.spec.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,37 @@
11
import { validate } from '../validate';
22
import { hasProperties } from './has-properties';
33
import { greaterThan, required, string } from '../rules';
4-
import { assertType, Dictionary, returns } from '../type-assertions';
4+
import { assertType, Dictionary, returns } from '../utils';
55
import { mapValues } from 'lodash-es';
66

77
describe('object', () => {
88
const requiredString = [required(), string()];
99

10-
it('should return messages in order', () => {
10+
it('should return messages in order', async () => {
1111
const test = { firstName: 1 };
1212
const rules = { firstName: [string(), greaterThan(2)] };
13-
const result = validate(test, hasProperties(rules));
13+
const result = await validate(test, hasProperties(rules));
1414
const messages = result.flattened();
1515
expect(messages).toHaveLength(2);
1616
expect(messages[0]).toMatch(/must be a string/);
1717
expect(messages[1]).toMatch(/must be greater than/);
1818
});
1919

20-
it('should not return properties that have passed validation', () => {
20+
it('should not return properties that have passed validation', async () => {
2121
const test = { firstName: 'tim' };
2222
const rules = { firstName: requiredString, lastName: requiredString };
23-
const result = validate(test, hasProperties(rules)).all();
23+
const result = (await validate(test, hasProperties(rules))).all();
2424
if (result) {
2525
expect(result.firstName).toBe(undefined);
2626
expect(result.hasOwnProperty('firstName')).toBe(false);
2727
expect('firstName' in result).toBe(false);
2828
}
2929
});
3030

31-
it('should retain the shape of the input in the message object', () => {
31+
it('should retain the shape of the input in the message object', async () => {
3232
const test = { firstName: 'tim' };
3333
const rules = { firstName: requiredString, lastName: requiredString };
34-
const result = validate(test, hasProperties(rules)).all();
34+
const result = (await validate(test, hasProperties(rules))).all();
3535
if (result) {
3636
assertType<string[] | undefined>(result.firstName);
3737
assertType<typeof result.firstName>(returns<string[] | undefined>());
@@ -41,11 +41,11 @@ describe('object', () => {
4141
}
4242
});
4343

44-
it('should retain the shape of child objects of the input in the message object', () => {
44+
it('should retain the shape of child objects of the input in the message object', async () => {
4545
const test = { parent: { firstName: 'tim' } };
46-
const result = validate(test, hasProperties({
46+
const result = (await validate(test, hasProperties({
4747
parent: hasProperties({ firstName: requiredString }),
48-
})).all();
48+
}))).all();
4949

5050
if (result) {
5151
assertType<{ parent?: { firstName?: string[] } }>(result);
@@ -63,8 +63,8 @@ describe('object', () => {
6363
}
6464
});
6565

66-
it('should work', () => {
67-
const result = validate(
66+
it('should work', async () => {
67+
const result = await validate(
6868
{ firstName: 'tim' },
6969
hasProperties({
7070
firstName: [required()],
@@ -78,32 +78,32 @@ describe('object', () => {
7878
expect(messages[0]).toMatch(/required/);
7979
});
8080

81-
it('should correctly pass the key option', () => {
81+
it('should correctly pass the key option', async () => {
8282
const constraint = jest.fn(() => undefined);
8383
const parent: Dictionary<number> = { a: 1, b: 2 };
84-
validate(parent, hasProperties(mapValues(parent, () => constraint)));
84+
await validate(parent, hasProperties(mapValues(parent, () => constraint)));
8585
Object.keys(parent).forEach((key, index) => {
8686
const options = expect.objectContaining({ key });
8787
expect(constraint).nthCalledWith(index + 1, parent[key], options);
8888
});
8989
});
9090

91-
it('should correctly pass the key path option', () => {
91+
it('should correctly pass the key path option', async () => {
9292
const constraint = jest.fn(() => undefined);
9393
const parent: Dictionary<number> = { a: 1, b: 2 };
94-
validate(parent, hasProperties(mapValues(parent, () => constraint)));
94+
await validate(parent, hasProperties(mapValues(parent, () => constraint)));
9595
Object.keys(parent).forEach((key, index) => {
9696
const options = expect.objectContaining({ keyPath: [key] });
9797
expect(constraint).nthCalledWith(index + 1, parent[key], options);
9898
});
9999
});
100100

101-
it('should correctly pass the key path option when nested', () => {
101+
it('should correctly pass the key path option when nested', async () => {
102102
const constraint = jest.fn(() => undefined);
103103
const child: Dictionary<number> = { a: 1, b: 2 };
104104
const parent: Dictionary<Dictionary<number>> = { c: child, d: child };
105105
const childConstraints = hasProperties(mapValues(child, () => constraint));
106-
validate(parent, hasProperties(mapValues(parent, () => childConstraints)));
106+
await validate(parent, hasProperties(mapValues(parent, () => childConstraints)));
107107
Object.keys(parent).forEach((parentKey, parentIndex) => {
108108
Object.keys(child).forEach((childKey, childIndex) => {
109109
const index = parentIndex * Object.keys(child).length + childIndex;
@@ -113,10 +113,10 @@ describe('object', () => {
113113
});
114114
});
115115

116-
it('should correctly pass the parent option', () => {
116+
it('should correctly pass the parent option', async () => {
117117
const constraint = jest.fn(() => undefined);
118118
const parent: Dictionary<number> = { a: 1, b: 2 };
119-
validate(parent, hasProperties(mapValues(parent, () => constraint)));
119+
await validate(parent, hasProperties(mapValues(parent, () => constraint)));
120120
Object.keys(parent).forEach((key, index) => {
121121
const options = expect.objectContaining({ parent });
122122
expect(constraint).nthCalledWith(index + 1, parent[key], options);

src/groups/has-properties.ts

Lines changed: 29 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,46 @@
1+
import pMap from 'p-map';
2+
import { isUndefined, omitBy, zipObject } from 'lodash-es';
13
import { ObjectMessageMap, ObjectValidatorMap, runConstraints } from '../validate';
2-
import { ConstraintOptions } from '../make-constraint';
3-
import { Dictionary } from '../type-assertions';
4+
import { ConstraintOptions, GroupConstraint } from '../make-constraint';
5+
import { Dictionary } from '../utils';
46

5-
function runPropertyConstraints<T extends ObjectValidatorMap<any>>(
6-
resultMap: ObjectMessageMap<T>,
7-
name: string,
7+
function runPropertyConstraint<T extends ObjectValidatorMap<any>>(
88
value: any,
9-
constraints: any,
9+
constraints: T,
1010
options: ConstraintOptions,
11-
): boolean {
12-
const result = runConstraints(value, constraints, options);
13-
if (result !== undefined) {
14-
resultMap[name] = result as any;
15-
return false;
16-
}
17-
return true;
11+
): (key: string) => Promise<any> {
12+
return (key) => {
13+
const propertyOptions = {
14+
...options,
15+
key,
16+
keyPath: [...options.keyPath, key],
17+
parent: value,
18+
};
19+
return runConstraints(value[key], constraints[key], propertyOptions);
20+
};
1821
}
1922

2023
function runObjectConstraints<T extends ObjectValidatorMap<any>>(
21-
constraints: T,
2224
value: Dictionary<any>,
25+
constraints: T,
2326
options: ConstraintOptions,
24-
): ObjectMessageMap<T> | undefined {
25-
let allConstraintsPassed = true;
26-
const resultMap = Object.keys(constraints).reduce<ObjectMessageMap<T>>(
27-
(resultMap, key) => {
28-
const propertyOptions = {
29-
...options,
30-
key,
31-
keyPath: [...options.keyPath, key],
32-
parent: value,
33-
};
34-
35-
const result = runConstraints(value[key], constraints[key], propertyOptions);
36-
if (result !== undefined) {
37-
resultMap[key] = result as any;
38-
allConstraintsPassed = false;
39-
}
40-
41-
return resultMap;
42-
},
43-
{},
44-
);
45-
return allConstraintsPassed ? undefined : resultMap;
27+
): Promise<ObjectMessageMap<T> | undefined> {
28+
const keys = Object.keys(constraints);
29+
return pMap(keys, runPropertyConstraint(value, constraints, options))
30+
.then(results => (
31+
results.every(isUndefined)
32+
? undefined
33+
: omitBy(zipObject(keys, results), isUndefined) as ObjectMessageMap<T>
34+
));
4635
}
4736

4837
export function hasProperties<T extends ObjectValidatorMap<any>>(
4938
constraints: T,
50-
): (...args: any[]) => ObjectMessageMap<T> | undefined {
51-
return (value: any, options: ConstraintOptions): ObjectMessageMap<T> | undefined => {
39+
): GroupConstraint<ObjectMessageMap<T> | undefined> {
40+
return (value: any, options: ConstraintOptions) => {
5241
if (value === null || typeof value !== 'object') {
53-
return undefined;
42+
return Promise.resolve(undefined);
5443
}
55-
return runObjectConstraints(constraints, value, options);
44+
return runObjectConstraints(value, constraints, options);
5645
};
5746
}

0 commit comments

Comments
 (0)