Skip to content
This repository has been archived by the owner on Apr 24, 2023. It is now read-only.

Improve validation with asynchronous pipeline #175

Open
wants to merge 22 commits into
base: canary
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1aefc98
Fix: check whether validator is a function to avoid type error
Mosoc Apr 27, 2019
db82657
Fix: typo `ComponentWithValidation`
Mosoc Apr 27, 2019
8c03926
Only pass schema property to avj.compile()
Mosoc Apr 29, 2019
ae5d0ce
Fix some coding style
Mosoc Apr 29, 2019
991c04f
Add async keyword to validate function
Mosoc May 9, 2019
0d51466
Update validation test file because of object structure change
Mosoc May 14, 2019
ff1312c
Update validator test
Mosoc May 14, 2019
2f642e0
Implement asynchronous pipline of validation
Mosoc May 9, 2019
763fde4
Fix typo: withValidation
Mosoc May 14, 2019
dfa6ce1
Reform validation's test file with async/await, and remove unused props
Mosoc May 15, 2019
c5711d0
Change the test label of customized validator
Mosoc May 15, 2019
9e5748a
Create test cases of sync validator function
Mosoc May 15, 2019
1c731c9
Correct fault in validation HoC
Mosoc May 15, 2019
e10c57f
Create test cases with async-wait functions
Mosoc May 15, 2019
67ffc9a
Create test cases with promise operations
Mosoc May 15, 2019
a493e5a
Add try-catch in validate to handle uncaught error
Mosoc May 15, 2019
096a86a
Workaround for when validator is not a function
Mosoc May 15, 2019
91038ba
Reove `_ ` prefix of promise creator
Mosoc May 16, 2019
831fd24
Update type definition of validation
Mosoc May 16, 2019
471a875
Update the usage of customized validator in docs
Mosoc May 18, 2019
0993e84
Use keep (verb) as prefix for promise creator
Mosoc May 19, 2019
9157f2c
Change `keep`(verb) with `promise`(verb)
Mosoc May 29, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
137 changes: 100 additions & 37 deletions packages/canner/src/hocs/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,72 @@
import * as React from 'react';
import RefId from 'canner-ref-id';
import Ajv from 'ajv';
import {isEmpty, isArray, isPlainObject, get} from 'lodash';
import {isEmpty, isObject, isArray, isPlainObject, isFunction, get} from 'lodash';
import type {HOCProps} from './types';

type State = {
error: boolean,
errorInfo: Array<any>
}

const _isRequiredValidation = async (value) => {
const valid = Boolean(value)
return {
error: !valid,
errorInfo: !valid ? [{message: 'should be required'}] :[]
}
}

const checkValidation = (validation) => {
return (isObject(validation) && !isEmpty(validation))
}

const checkSchema = (schema) => {
return (isObject(schema) && !isEmpty(schema) )
}
const checkValidator = (validator) => {
return (isFunction(validator))
}

const _schemaValidation = (schema, errorMessage) => {
const ajv = new Ajv();
const validate = ajv.compile(schema);
return async (value) => {
try {
const error = !validate(value)
const errorInfo = error ? [].concat( errorMessage ? {message: errorMessage} : validate.errors ) : []
return {
error,
errorInfo
}
}
catch(err){
return {
error: true,
errorInfo: [{message: err}]
}
}

}
}
const _customizedValidator = (validator) => async (value) => {
try {
const errorMessage = await validator(value)
return {
error: Boolean(errorMessage),
errorInfo: [{message: errorMessage}]
}
}
catch(err) {
return {
error: true,
errorInfo: [{message: err}]
}
}
}

export default function withValidation(Com: React.ComponentType<*>) {
return class ComponentWithValition extends React.Component<HOCProps, State> {
return class ComponentWithValidation extends React.Component<HOCProps, State> {
key: string;
id: ?string;
callbackId: ?string;
Expand All @@ -35,48 +91,55 @@ export default function withValidation(Com: React.ComponentType<*>) {
this.removeOnDeploy();
}

validate = (result: any) => {
const {refId, validation = {}, required = false} = this.props;
handleValidationResult = (results: any) => {

let error = false;
let errorInfo = [];

for(let index = 0; index < results.length; index++) {
error = error || results[index].error
errorInfo = errorInfo.concat(results[index].errorInfo);
}

this.setState({
error,
errorInfo
});

return {
error,
errorInfo
}
}

validate = async (result: any) => {
const {refId, required = false, validation} = this.props;
// required
const paths = refId.getPathArr().slice(1);
const {value} = getValueAndPaths(result.data, paths);
const isRequiredValid = required ? Boolean(value) : true;

// Ajv validation
const ajv = new Ajv();
const validate = ajv.compile(validation);

// custom validator
const {validator, errorMessage} = validation;
const reject = message => ({error: true, message});
const validatorResult = validator && validator(value, reject);

let customValid = !(validatorResult && validatorResult.error);
// if value is empty, should not validate with ajv
if (customValid && isRequiredValid && (!value || validate(value))) {
this.setState({
error: false,
errorInfo: []
});
return result;
const promiseQueue = [];

// check whether value is required in first step
if(required) {
promiseQueue.push(_isRequiredValidation(value));
}


const errorInfo = []
.concat(isRequiredValid ? [] : {
message: 'should be required'
})
.concat(validate.errors ? (errorMessage ? {message: errorMessage} : validate.errors) : [])
.concat(customValid ? [] : validatorResult);

this.setState({
error: true,
errorInfo: errorInfo
});
// skip validation if object validation is undefined or empty
if(checkValidation(validation)) {
const {schema, errorMessage, validator} = validation;
if(value && checkSchema(schema)) {
promiseQueue.push(_schemaValidation(schema, errorMessage)(value));
}
if(checkValidator(validator)) {
promiseQueue.push(_customizedValidator(validator)(value));
}
}

const ValidationResult = await Promise.all(promiseQueue);

return {
...result,
error: true,
errorInfo: errorInfo
...this.handleValidationResult(ValidationResult)
}
}

Expand Down
14 changes: 7 additions & 7 deletions packages/canner/test/hocs/validation.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import Enzyme, { mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import withValidationn from '../../src/hocs/validation';
import withValidation from '../../src/hocs/validation';
import RefId from 'canner-ref-id';


Expand All @@ -21,7 +21,7 @@ describe('withValidation', () => {
onDeploy,
removeOnDeploy
}
WrapperComponent = withValidationn(MockComponent);
WrapperComponent = withValidation(MockComponent);
});

it('should error state = false', () => {
Mosoc marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -88,7 +88,7 @@ describe('withValidation', () => {
{...props}
onDeploy={jest.fn().mockImplementation((_, fn) => (fn(result)))}
Mosoc marked this conversation as resolved.
Show resolved Hide resolved
required
validation={{pattern: '^http://[.]+'}}
validation={{schema: {pattern: '^http://[.]+'}}}
/>);
expect(wrapper.state()).toMatchObject({
Mosoc marked this conversation as resolved.
Show resolved Hide resolved
error: true,
Expand All @@ -109,7 +109,7 @@ describe('withValidation', () => {
{...props}
onDeploy={jest.fn().mockImplementation((_, fn) => (fn(result)))}
required
validation={{pattern: '^http://[.]+', errorMessage}}
validation={{schema: {pattern: '^http://[.]+'}, errorMessage}}
/>);
expect(wrapper.state()).toMatchObject({
error: true,
Expand All @@ -128,7 +128,7 @@ describe('withValidation', () => {
const wrapper = mount(<WrapperComponent
{...props}
onDeploy={jest.fn().mockImplementation((_, fn) => (fn(result)))}
validation={{pattern: '^http://[.]+'}}
validation={{schema: {pattern: '^http://[.]+'}}}
/>);
expect(wrapper.state()).toMatchObject({
error: false,
Expand All @@ -147,9 +147,9 @@ describe('withValidation', () => {
onDeploy={jest.fn().mockImplementation((_, fn) => (fn(result)))}
validation={
{
validator: (content, reject) => {
validator: (content) => {
if (!content) {
return reject('should be required');
return 'should be required';
}
}
}
Expand Down