Skip to content

Commit 3e36644

Browse files
committed
feat(renderer): add independent validate function
1 parent 197f85d commit 3e36644

File tree

5 files changed

+211
-0
lines changed

5 files changed

+211
-0
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import validation from '../../validation';
2+
3+
describe('validation', () => {
4+
let schema;
5+
let options;
6+
7+
beforeEach(() => {
8+
schema = undefined;
9+
options = undefined;
10+
});
11+
12+
it('no schema', async () => {
13+
expect.assertions(1);
14+
15+
try {
16+
await validation(schema, options);
17+
} catch (e) {
18+
expect(e.toString()).toEqual('Error: validation requires a schema as the first argument.');
19+
}
20+
});
21+
22+
it('options is not object', async () => {
23+
expect.assertions(1);
24+
25+
schema = {};
26+
27+
try {
28+
await validation(schema, options);
29+
} catch (e) {
30+
expect(e.toString()).toEqual('Error: options argument has to be type of object, provided: undefined');
31+
}
32+
});
33+
34+
it('wrong schema', async () => {
35+
expect.assertions(1);
36+
37+
schema = { fields: [{ name: 'field-1', component: 'checkbox', validate: [{ type: 'custom-type' }] }] };
38+
options = {};
39+
40+
try {
41+
await validation(schema, options);
42+
} catch (e) {
43+
expect(e.toString().trim()).toEqual(`DefaultSchemaError:
44+
Error occured in field definition with name: "field-1".
45+
Field validator at index: 0 does not have correct "type" property!
46+
Received "custom-type", expected one of: [required,min-length,max-length,exact-length,min-items,pattern,max-number-value,min-number-value,url].`);
47+
}
48+
});
49+
50+
it('returns correct an array errors', async () => {
51+
const asyncValidate = jest.fn().mockImplementation((pass) => {
52+
const method = pass ? 'resolve' : 'reject';
53+
54+
return Promise[method]().catch(() => {
55+
// eslint-disable-next-line no-throw-literal
56+
throw 'some async validation error';
57+
});
58+
});
59+
60+
schema = {
61+
fields: [
62+
{ name: 'no-validation', component: 'checkbox' },
63+
{ name: 'pass', component: 'select', validate: [{ type: 'required' }] },
64+
{ name: 'invisible', component: 'dual-list', condition: { when: 'x', is: 'abc' }, validate: [{ type: 'required' }] },
65+
{ name: 'fail', component: 'select', validate: [{ type: 'required' }] },
66+
{ name: 'subform', component: 'subform', fields: [{ name: 'fail-in-nest', component: 'select', validate: [{ type: 'required' }] }] },
67+
{ name: 'fail.nested', component: 'select', validate: [{ type: 'required' }] },
68+
{ name: 'passes.nested', component: 'select', validate: [{ type: 'required' }] },
69+
{ name: 'fail-function', component: 'select', validate: [() => 'error-message-from-function'] },
70+
{ name: 'fail-custom', component: 'select', validate: [{ type: 'custom' }] },
71+
{ name: 'async-fail', component: 'select', validate: [asyncValidate] },
72+
{ name: 'async-pass', component: 'select', validate: [asyncValidate] },
73+
{ name: 'datatype-fail', component: 'select', dataType: 'number' },
74+
{ name: 'fail-multiple', component: 'select', validate: [{ type: 'required' }, { type: 'pattern', pattern: /abc/ }] },
75+
{ name: 'double-fail', component: 'select', validate: [{ type: 'required' }] },
76+
{ name: 'double-fail', component: 'select', validate: [{ type: 'pattern', pattern: /abc/ }] },
77+
{ name: 'double-fail-1', component: 'select', validate: [{ type: 'required' }] },
78+
{ name: 'double-fail-1', component: 'select', validate: [{ type: 'pattern', pattern: /abc/ }] },
79+
{ name: 'double-fail-2', component: 'select', validate: [{ type: 'required' }] },
80+
{ name: 'double-fail-2', component: 'select', dataType: 'number' },
81+
{ name: 'combined-fail', component: 'select', dataType: 'number', validate: [{ type: 'required' }] },
82+
{ name: 'combined-fail-1', component: 'select', dataType: 'number', validate: [{ type: 'required' }] },
83+
{ name: 'combined-pass', component: 'select', dataType: 'number', validate: [{ type: 'required' }] }
84+
]
85+
};
86+
options = {
87+
values: {
88+
pass: 'some-value',
89+
'async-pass': 'some-value',
90+
passes: { nested: 'some-value' },
91+
'datatype-fail': 'abc',
92+
'fail-multiple': 'ccc',
93+
'double-fail': 'ccc',
94+
'double-fail-2': 'ccc',
95+
'combined-fail-1': 'string',
96+
'combined-pass': 123
97+
},
98+
validatorMapper: {
99+
custom: () => () => 'custom error validator'
100+
}
101+
};
102+
103+
const results = await validation(schema, options);
104+
105+
expect(results).toEqual({
106+
'async-fail': 'some async validation error',
107+
'datatype-fail': 'Values must be number',
108+
fail: 'Required',
109+
'fail-custom': 'custom error validator',
110+
'fail-function': 'error-message-from-function',
111+
'fail-in-nest': 'Required',
112+
'fail.nested': 'Required',
113+
'fail-multiple': 'Value does not match pattern: /abc/.',
114+
'double-fail': 'Value does not match pattern: /abc/.',
115+
'double-fail-1': 'Required',
116+
'double-fail-2': 'Values must be number',
117+
'combined-fail': 'Required',
118+
'combined-fail-1': 'Values must be number'
119+
});
120+
expect(asyncValidate.mock.calls).toEqual([
121+
[undefined, options.values, {}],
122+
['some-value', options.values, {}]
123+
]);
124+
});
125+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './validation';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './validation';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Schema, AnyObject } from "../common-types";
2+
3+
export interface ValidationOptions {
4+
values: AnyObject;
5+
}
6+
7+
declare function validation(schema: Schema, options: ValidationOptions): AnyObject;
8+
9+
export default validation;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import get from 'lodash/get';
2+
3+
import composeValidators from '../compose-validators';
4+
import defaultSchemaValidator from '../default-schema-validator';
5+
import getValidates from '../get-validates';
6+
import getVisibleFields from '../get-visible-fields';
7+
import defaultValidatorMapper from '../validator-mapper';
8+
import { getValidate } from '../use-field-api/validator-helpers';
9+
10+
const DEFAULT_COMPONENT = 'default-component';
11+
12+
const noop = () => {};
13+
14+
const changeToDefaultComponent = (schema) => {
15+
if (Array.isArray(schema)) {
16+
return schema.map(changeToDefaultComponent);
17+
}
18+
19+
return {
20+
...schema,
21+
...(schema.component && { component: DEFAULT_COMPONENT }),
22+
...(schema.fields && { fields: changeToDefaultComponent(schema.fields) })
23+
};
24+
};
25+
26+
const validation = async (schema, options) => {
27+
if (!schema) {
28+
throw new Error('validation requires a schema as the first argument.');
29+
}
30+
31+
if (typeof options !== 'object') {
32+
throw new Error(`options argument has to be type of object, provided: ${typeof options}`);
33+
}
34+
35+
const { values, componentMapper, validatorMapper, actionMapper, schemaValidatorMapper } = options;
36+
37+
const validatorMapperMerged = { ...defaultValidatorMapper, ...validatorMapper };
38+
39+
const validatorTypes = Object.keys(validatorMapperMerged);
40+
const actionTypes = Object.keys(actionMapper || {});
41+
42+
let finalComponentMapper = componentMapper;
43+
let finalSchema = schema;
44+
45+
if (!finalComponentMapper) {
46+
finalComponentMapper = { [DEFAULT_COMPONENT]: noop };
47+
finalSchema = changeToDefaultComponent(schema);
48+
}
49+
50+
defaultSchemaValidator(finalSchema, finalComponentMapper, validatorTypes, actionTypes, schemaValidatorMapper);
51+
52+
finalSchema = getVisibleFields(finalSchema, values);
53+
54+
const validates = getValidates(finalSchema, { componentMapper: finalComponentMapper, actionMapper, values });
55+
56+
return await Object.keys(validates).reduce(async (accP, name) => {
57+
const acc = await accP;
58+
let error;
59+
let index = 0;
60+
61+
while (!error && index + 1 <= validates[name].length) {
62+
const validateFn = composeValidators(getValidate(validates[name][index], schema.dataType, validatorMapperMerged));
63+
error = await validateFn(get(values, name), values, {});
64+
index = index + 1;
65+
}
66+
67+
if (error) {
68+
return { ...acc, [name]: error };
69+
}
70+
71+
return acc;
72+
}, {});
73+
};
74+
75+
export default validation;

0 commit comments

Comments
 (0)