310 lines
7.1 KiB
TypeScript
310 lines
7.1 KiB
TypeScript
/* eslint no-console:0 */
|
|
|
|
import type {
|
|
ValidateError,
|
|
ValidateOption,
|
|
RuleValuePackage,
|
|
InternalRuleItem,
|
|
SyncErrorType,
|
|
RuleType,
|
|
Value,
|
|
Values,
|
|
} from './interface';
|
|
|
|
const formatRegExp = /%[sdj%]/g;
|
|
|
|
declare var ASYNC_VALIDATOR_NO_WARNING;
|
|
|
|
export let warning: (type: string, errors: SyncErrorType[]) => void = () => {};
|
|
|
|
// don't print warning message when in production env or node runtime
|
|
if (
|
|
typeof process !== 'undefined' &&
|
|
process.env &&
|
|
process.env.NODE_ENV !== 'production' &&
|
|
typeof window !== 'undefined' &&
|
|
typeof document !== 'undefined'
|
|
) {
|
|
warning = (type, errors) => {
|
|
if (
|
|
typeof console !== 'undefined' &&
|
|
console.warn &&
|
|
typeof ASYNC_VALIDATOR_NO_WARNING === 'undefined'
|
|
) {
|
|
if (errors.every(e => typeof e === 'string')) {
|
|
console.warn(type, errors);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
export function convertFieldsError(
|
|
errors: ValidateError[],
|
|
): Record<string, ValidateError[]> {
|
|
if (!errors || !errors.length) return null;
|
|
const fields = {};
|
|
errors.forEach(error => {
|
|
const field = error.field;
|
|
fields[field] = fields[field] || [];
|
|
fields[field].push(error);
|
|
});
|
|
return fields;
|
|
}
|
|
|
|
export function format(
|
|
template: ((...args: any[]) => string) | string,
|
|
...args: any[]
|
|
): string {
|
|
let i = 0;
|
|
const len = args.length;
|
|
if (typeof template === 'function') {
|
|
return template.apply(null, args);
|
|
}
|
|
if (typeof template === 'string') {
|
|
let str = template.replace(formatRegExp, x => {
|
|
if (x === '%%') {
|
|
return '%';
|
|
}
|
|
if (i >= len) {
|
|
return x;
|
|
}
|
|
switch (x) {
|
|
case '%s':
|
|
return String(args[i++]);
|
|
case '%d':
|
|
return (Number(args[i++]) as unknown) as string;
|
|
case '%j':
|
|
try {
|
|
return JSON.stringify(args[i++]);
|
|
} catch (_) {
|
|
return '[Circular]';
|
|
}
|
|
break;
|
|
default:
|
|
return x;
|
|
}
|
|
});
|
|
return str;
|
|
}
|
|
return template;
|
|
}
|
|
|
|
function isNativeStringType(type: string) {
|
|
return (
|
|
type === 'string' ||
|
|
type === 'url' ||
|
|
type === 'hex' ||
|
|
type === 'email' ||
|
|
type === 'date' ||
|
|
type === 'pattern'
|
|
);
|
|
}
|
|
|
|
export function isEmptyValue(value: Value, type?: string) {
|
|
if (value === undefined || value === null) {
|
|
return true;
|
|
}
|
|
if (type === 'array' && Array.isArray(value) && !value.length) {
|
|
return true;
|
|
}
|
|
if (isNativeStringType(type) && typeof value === 'string' && !value) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
export function isEmptyObject(obj: object) {
|
|
return Object.keys(obj).length === 0;
|
|
}
|
|
|
|
function asyncParallelArray(
|
|
arr: RuleValuePackage[],
|
|
func: ValidateFunc,
|
|
callback: (errors: ValidateError[]) => void,
|
|
) {
|
|
const results: ValidateError[] = [];
|
|
let total = 0;
|
|
const arrLength = arr.length;
|
|
|
|
function count(errors: ValidateError[]) {
|
|
results.push(...(errors || []));
|
|
total++;
|
|
if (total === arrLength) {
|
|
callback(results);
|
|
}
|
|
}
|
|
|
|
arr.forEach(a => {
|
|
func(a, count);
|
|
});
|
|
}
|
|
|
|
function asyncSerialArray(
|
|
arr: RuleValuePackage[],
|
|
func: ValidateFunc,
|
|
callback: (errors: ValidateError[]) => void,
|
|
) {
|
|
let index = 0;
|
|
const arrLength = arr.length;
|
|
|
|
function next(errors: ValidateError[]) {
|
|
if (errors && errors.length) {
|
|
callback(errors);
|
|
return;
|
|
}
|
|
const original = index;
|
|
index = index + 1;
|
|
if (original < arrLength) {
|
|
func(arr[original], next);
|
|
} else {
|
|
callback([]);
|
|
}
|
|
}
|
|
|
|
next([]);
|
|
}
|
|
|
|
function flattenObjArr(objArr: Record<string, RuleValuePackage[]>) {
|
|
const ret: RuleValuePackage[] = [];
|
|
Object.keys(objArr).forEach(k => {
|
|
ret.push(...(objArr[k] || []));
|
|
});
|
|
return ret;
|
|
}
|
|
|
|
export class AsyncValidationError extends Error {
|
|
errors: ValidateError[];
|
|
fields: Record<string, ValidateError[]>;
|
|
|
|
constructor(
|
|
errors: ValidateError[],
|
|
fields: Record<string, ValidateError[]>,
|
|
) {
|
|
super('Async Validation Error');
|
|
this.errors = errors;
|
|
this.fields = fields;
|
|
}
|
|
}
|
|
|
|
type ValidateFunc = (
|
|
data: RuleValuePackage,
|
|
doIt: (errors: ValidateError[]) => void,
|
|
) => void;
|
|
|
|
export function asyncMap(
|
|
objArr: Record<string, RuleValuePackage[]>,
|
|
option: ValidateOption,
|
|
func: ValidateFunc,
|
|
callback: (errors: ValidateError[]) => void,
|
|
source: Values,
|
|
): Promise<Values> {
|
|
if (option.first) {
|
|
const pending = new Promise<Values>((resolve, reject) => {
|
|
const next = (errors: ValidateError[]) => {
|
|
callback(errors);
|
|
return errors.length
|
|
? reject(new AsyncValidationError(errors, convertFieldsError(errors)))
|
|
: resolve(source);
|
|
};
|
|
const flattenArr = flattenObjArr(objArr);
|
|
asyncSerialArray(flattenArr, func, next);
|
|
});
|
|
pending.catch(e => e);
|
|
return pending;
|
|
}
|
|
const firstFields =
|
|
option.firstFields === true
|
|
? Object.keys(objArr)
|
|
: option.firstFields || [];
|
|
|
|
const objArrKeys = Object.keys(objArr);
|
|
const objArrLength = objArrKeys.length;
|
|
let total = 0;
|
|
const results: ValidateError[] = [];
|
|
const pending = new Promise<Values>((resolve, reject) => {
|
|
const next = (errors: ValidateError[]) => {
|
|
results.push.apply(results, errors);
|
|
total++;
|
|
if (total === objArrLength) {
|
|
callback(results);
|
|
return results.length
|
|
? reject(
|
|
new AsyncValidationError(results, convertFieldsError(results)),
|
|
)
|
|
: resolve(source);
|
|
}
|
|
};
|
|
if (!objArrKeys.length) {
|
|
callback(results);
|
|
resolve(source);
|
|
}
|
|
objArrKeys.forEach(key => {
|
|
const arr = objArr[key];
|
|
if (firstFields.indexOf(key) !== -1) {
|
|
asyncSerialArray(arr, func, next);
|
|
} else {
|
|
asyncParallelArray(arr, func, next);
|
|
}
|
|
});
|
|
});
|
|
pending.catch(e => e);
|
|
return pending;
|
|
}
|
|
|
|
function isErrorObj(
|
|
obj: ValidateError | string | (() => string),
|
|
): obj is ValidateError {
|
|
return !!(obj && (obj as ValidateError).message !== undefined);
|
|
}
|
|
|
|
function getValue(value: Values, path: string[]) {
|
|
let v = value;
|
|
for (let i = 0; i < path.length; i++) {
|
|
if (v == undefined) {
|
|
return v;
|
|
}
|
|
v = v[path[i]];
|
|
}
|
|
return v;
|
|
}
|
|
|
|
export function complementError(rule: InternalRuleItem, source: Values) {
|
|
return (oe: ValidateError | (() => string) | string): ValidateError => {
|
|
let fieldValue;
|
|
if (rule.fullFields) {
|
|
fieldValue = getValue(source, rule.fullFields);
|
|
} else {
|
|
fieldValue = source[(oe as any).field || rule.fullField];
|
|
}
|
|
if (isErrorObj(oe)) {
|
|
oe.field = oe.field || rule.fullField;
|
|
oe.fieldValue = fieldValue;
|
|
return oe;
|
|
}
|
|
return {
|
|
message: typeof oe === 'function' ? oe() : oe,
|
|
fieldValue,
|
|
field: ((oe as unknown) as ValidateError).field || rule.fullField,
|
|
};
|
|
};
|
|
}
|
|
|
|
export function deepMerge<T extends object>(target: T, source: Partial<T>): T {
|
|
if (source) {
|
|
for (const s in source) {
|
|
if (source.hasOwnProperty(s)) {
|
|
const value = source[s];
|
|
if (typeof value === 'object' && typeof target[s] === 'object') {
|
|
target[s] = {
|
|
...target[s],
|
|
...value,
|
|
};
|
|
} else {
|
|
target[s] = value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return target;
|
|
}
|