228 lines
6.1 KiB
JavaScript
228 lines
6.1 KiB
JavaScript
|
import RawAsyncValidator from 'async-validator';
|
||
|
import { cloneVNode } from 'vue';
|
||
|
import { warning } from '../../vc-util/warning';
|
||
|
|
||
|
import { setValues } from './valueUtil';
|
||
|
import { defaultValidateMessages } from './messages';
|
||
|
import { isValidElement } from '../../_util/props-util';
|
||
|
|
||
|
// Remove incorrect original ts define
|
||
|
const AsyncValidator = RawAsyncValidator;
|
||
|
|
||
|
/**
|
||
|
* Replace with template.
|
||
|
* `I'm ${name}` + { name: 'bamboo' } = I'm bamboo
|
||
|
*/
|
||
|
function replaceMessage(template, kv) {
|
||
|
return template.replace(/\$\{\w+\}/g, str => {
|
||
|
const key = str.slice(2, -1);
|
||
|
return kv[key];
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* We use `async-validator` to validate rules. So have to hot replace the message with validator.
|
||
|
* { required: '${name} is required' } => { required: () => 'field is required' }
|
||
|
*/
|
||
|
function convertMessages(messages, name, rule, messageVariables) {
|
||
|
const kv = {
|
||
|
...rule,
|
||
|
name,
|
||
|
enum: (rule.enum || []).join(', '),
|
||
|
};
|
||
|
|
||
|
const replaceFunc = (template, additionalKV) => () =>
|
||
|
replaceMessage(template, { ...kv, ...additionalKV });
|
||
|
|
||
|
/* eslint-disable no-param-reassign */
|
||
|
function fillTemplate(source, target = {}) {
|
||
|
Object.keys(source).forEach(ruleName => {
|
||
|
const value = source[ruleName];
|
||
|
if (typeof value === 'string') {
|
||
|
target[ruleName] = replaceFunc(value, messageVariables);
|
||
|
} else if (value && typeof value === 'object') {
|
||
|
target[ruleName] = {};
|
||
|
fillTemplate(value, target[ruleName]);
|
||
|
} else {
|
||
|
target[ruleName] = value;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
return target;
|
||
|
}
|
||
|
/* eslint-enable */
|
||
|
|
||
|
return fillTemplate(setValues({}, defaultValidateMessages, messages));
|
||
|
}
|
||
|
|
||
|
async function validateRule(name, value, rule, options, messageVariables) {
|
||
|
const cloneRule = { ...rule };
|
||
|
// We should special handle array validate
|
||
|
let subRuleField = null;
|
||
|
if (cloneRule && cloneRule.type === 'array' && cloneRule.defaultField) {
|
||
|
subRuleField = cloneRule.defaultField;
|
||
|
delete cloneRule.defaultField;
|
||
|
}
|
||
|
|
||
|
const validator = new AsyncValidator({
|
||
|
[name]: [cloneRule],
|
||
|
});
|
||
|
|
||
|
const messages = convertMessages(options.validateMessages, name, cloneRule, messageVariables);
|
||
|
validator.messages(messages);
|
||
|
|
||
|
let result = [];
|
||
|
|
||
|
try {
|
||
|
await Promise.resolve(validator.validate({ [name]: value }, { ...options }));
|
||
|
} catch (errObj) {
|
||
|
if (errObj.errors) {
|
||
|
result = errObj.errors.map(({ message }, index) =>
|
||
|
// Wrap VueNode with `key`
|
||
|
isValidElement(message) ? cloneVNode(message, { key: `error_${index}` }) : message,
|
||
|
);
|
||
|
} else {
|
||
|
console.error(errObj);
|
||
|
result = [messages.default()];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!result.length && subRuleField) {
|
||
|
const subResults = await Promise.all(
|
||
|
value.map((subValue, i) =>
|
||
|
validateRule(`${name}.${i}`, subValue, subRuleField, options, messageVariables),
|
||
|
),
|
||
|
);
|
||
|
|
||
|
return subResults.reduce((prev, errors) => [...prev, ...errors], []);
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* We use `async-validator` to validate the value.
|
||
|
* But only check one value in a time to avoid namePath validate issue.
|
||
|
*/
|
||
|
export function validateRules(namePath, value, rules, options, validateFirst, messageVariables) {
|
||
|
const name = namePath.join('.');
|
||
|
|
||
|
// Fill rule with context
|
||
|
const filledRules = rules.map(currentRule => {
|
||
|
const originValidatorFunc = currentRule.validator;
|
||
|
|
||
|
if (!originValidatorFunc) {
|
||
|
return currentRule;
|
||
|
}
|
||
|
return {
|
||
|
...currentRule,
|
||
|
validator(rule, val, callback) {
|
||
|
let hasPromise = false;
|
||
|
|
||
|
// Wrap callback only accept when promise not provided
|
||
|
const wrappedCallback = (...args) => {
|
||
|
// Wait a tick to make sure return type is a promise
|
||
|
Promise.resolve().then(() => {
|
||
|
warning(
|
||
|
!hasPromise,
|
||
|
'Your validator function has already return a promise. `callback` will be ignored.',
|
||
|
);
|
||
|
|
||
|
if (!hasPromise) {
|
||
|
callback(...args);
|
||
|
}
|
||
|
});
|
||
|
};
|
||
|
|
||
|
// Get promise
|
||
|
const promise = originValidatorFunc(rule, val, wrappedCallback);
|
||
|
hasPromise =
|
||
|
promise && typeof promise.then === 'function' && typeof promise.catch === 'function';
|
||
|
|
||
|
/**
|
||
|
* 1. Use promise as the first priority.
|
||
|
* 2. If promise not exist, use callback with warning instead
|
||
|
*/
|
||
|
warning(hasPromise, '`callback` is deprecated. Please return a promise instead.');
|
||
|
|
||
|
if (hasPromise) {
|
||
|
promise
|
||
|
.then(() => {
|
||
|
callback();
|
||
|
})
|
||
|
.catch(err => {
|
||
|
callback(err);
|
||
|
});
|
||
|
}
|
||
|
},
|
||
|
};
|
||
|
});
|
||
|
|
||
|
let summaryPromise;
|
||
|
|
||
|
if (validateFirst === true) {
|
||
|
// >>>>> Validate by serialization
|
||
|
summaryPromise = new Promise(async resolve => {
|
||
|
/* eslint-disable no-await-in-loop */
|
||
|
for (let i = 0; i < filledRules.length; i += 1) {
|
||
|
const errors = await validateRule(name, value, filledRules[i], options, messageVariables);
|
||
|
if (errors.length) {
|
||
|
resolve(errors);
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
/* eslint-enable */
|
||
|
|
||
|
resolve([]);
|
||
|
});
|
||
|
} else {
|
||
|
// >>>>> Validate by parallel
|
||
|
const rulePromises = filledRules.map(rule =>
|
||
|
validateRule(name, value, rule, options, messageVariables),
|
||
|
);
|
||
|
|
||
|
summaryPromise = (validateFirst
|
||
|
? finishOnFirstFailed(rulePromises)
|
||
|
: finishOnAllFailed(rulePromises)
|
||
|
).then(errors => {
|
||
|
if (!errors.length) {
|
||
|
return [];
|
||
|
}
|
||
|
|
||
|
return Promise.reject(errors);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Internal catch error to avoid console error log.
|
||
|
summaryPromise.catch(e => e);
|
||
|
|
||
|
return summaryPromise;
|
||
|
}
|
||
|
|
||
|
async function finishOnAllFailed(rulePromises) {
|
||
|
return Promise.all(rulePromises).then(errorsList => {
|
||
|
const errors = [].concat(...errorsList);
|
||
|
|
||
|
return errors;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
async function finishOnFirstFailed(rulePromises) {
|
||
|
let count = 0;
|
||
|
|
||
|
return new Promise(resolve => {
|
||
|
rulePromises.forEach(promise => {
|
||
|
promise.then(errors => {
|
||
|
if (errors.length) {
|
||
|
resolve(errors);
|
||
|
}
|
||
|
|
||
|
count += 1;
|
||
|
if (count === rulePromises.length) {
|
||
|
resolve([]);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
}
|