243 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			243 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
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';
 | 
						|
import type { InternalNamePath, RuleError, RuleObject, ValidateOptions } from '../interface';
 | 
						|
 | 
						|
// Remove incorrect original ts define
 | 
						|
const AsyncValidator: any = RawAsyncValidator;
 | 
						|
 | 
						|
/**
 | 
						|
 * Replace with template.
 | 
						|
 *   `I'm ${name}` + { name: 'bamboo' } = I'm bamboo
 | 
						|
 */
 | 
						|
function replaceMessage(template: string, kv: Record<string, string>): string {
 | 
						|
  return template.replace(/\$\{\w+\}/g, (str: string) => {
 | 
						|
    const key = str.slice(2, -1);
 | 
						|
    return kv[key];
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
async function validateRule(
 | 
						|
  name: string,
 | 
						|
  value: any,
 | 
						|
  rule: RuleObject,
 | 
						|
  options: ValidateOptions,
 | 
						|
  messageVariables?: Record<string, string>,
 | 
						|
): Promise<string[]> {
 | 
						|
  const cloneRule = { ...rule };
 | 
						|
 | 
						|
  // Bug of `async-validator`
 | 
						|
  delete (cloneRule as any).ruleIndex;
 | 
						|
  delete (cloneRule as any).trigger;
 | 
						|
 | 
						|
  // We should special handle array validate
 | 
						|
  let subRuleField: RuleObject = null;
 | 
						|
  if (cloneRule && cloneRule.type === 'array' && cloneRule.defaultField) {
 | 
						|
    subRuleField = cloneRule.defaultField;
 | 
						|
    delete cloneRule.defaultField;
 | 
						|
  }
 | 
						|
 | 
						|
  const validator = new AsyncValidator({
 | 
						|
    [name]: [cloneRule],
 | 
						|
  });
 | 
						|
 | 
						|
  const messages = setValues({}, defaultValidateMessages, options.validateMessages);
 | 
						|
  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: number) =>
 | 
						|
        // Wrap VueNode with `key`
 | 
						|
        isValidElement(message) ? cloneVNode(message, { key: `error_${index}` }) : message,
 | 
						|
      );
 | 
						|
    } else {
 | 
						|
      console.error(errObj);
 | 
						|
      result = [(messages.default as () => string)()];
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  if (!result.length && subRuleField) {
 | 
						|
    const subResults: string[][] = await Promise.all(
 | 
						|
      (value as any[]).map((subValue: any, i: number) =>
 | 
						|
        validateRule(`${name}.${i}`, subValue, subRuleField, options, messageVariables),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
 | 
						|
    return subResults.reduce((prev, errors) => [...prev, ...errors], []);
 | 
						|
  }
 | 
						|
 | 
						|
  // Replace message with variables
 | 
						|
  const kv = {
 | 
						|
    ...(rule as Record<string, string | number>),
 | 
						|
    name,
 | 
						|
    enum: (rule.enum || []).join(', '),
 | 
						|
    ...messageVariables,
 | 
						|
  };
 | 
						|
 | 
						|
  const fillVariableResult = result.map(error => {
 | 
						|
    if (typeof error === 'string') {
 | 
						|
      return replaceMessage(error, kv);
 | 
						|
    }
 | 
						|
    return error;
 | 
						|
  });
 | 
						|
 | 
						|
  return fillVariableResult;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * 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: InternalNamePath,
 | 
						|
  value: any,
 | 
						|
  rules: RuleObject[],
 | 
						|
  options: ValidateOptions,
 | 
						|
  validateFirst: boolean | 'parallel',
 | 
						|
  messageVariables?: Record<string, string>,
 | 
						|
) {
 | 
						|
  const name = namePath.join('.');
 | 
						|
 | 
						|
  // Fill rule with context
 | 
						|
  const filledRules: RuleObject[] = rules
 | 
						|
    .map((currentRule, ruleIndex) => {
 | 
						|
      const originValidatorFunc = currentRule.validator;
 | 
						|
      const cloneRule = {
 | 
						|
        ...currentRule,
 | 
						|
        ruleIndex,
 | 
						|
      };
 | 
						|
 | 
						|
      // Replace validator if needed
 | 
						|
      if (originValidatorFunc) {
 | 
						|
        cloneRule.validator = (rule: RuleObject, val: any, callback: (error?: string) => void) => {
 | 
						|
          let hasPromise = false;
 | 
						|
 | 
						|
          // Wrap callback only accept when promise not provided
 | 
						|
          const wrappedCallback = (...args: string[]) => {
 | 
						|
            // 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 as Promise<void>)
 | 
						|
              .then(() => {
 | 
						|
                callback();
 | 
						|
              })
 | 
						|
              .catch(err => {
 | 
						|
                callback(err || ' ');
 | 
						|
              });
 | 
						|
          }
 | 
						|
        };
 | 
						|
      }
 | 
						|
 | 
						|
      return cloneRule;
 | 
						|
    })
 | 
						|
    .sort(({ warningOnly: w1, ruleIndex: i1 }, { warningOnly: w2, ruleIndex: i2 }) => {
 | 
						|
      if (!!w1 === !!w2) {
 | 
						|
        // Let keep origin order
 | 
						|
        return i1 - i2;
 | 
						|
      }
 | 
						|
 | 
						|
      if (w1) {
 | 
						|
        return 1;
 | 
						|
      }
 | 
						|
 | 
						|
      return -1;
 | 
						|
    });
 | 
						|
 | 
						|
  // Do validate rules
 | 
						|
  let summaryPromise: Promise<RuleError[]>;
 | 
						|
 | 
						|
  if (validateFirst === true) {
 | 
						|
    // >>>>> Validate by serialization
 | 
						|
    summaryPromise = new Promise(async (resolve, reject) => {
 | 
						|
      /* eslint-disable no-await-in-loop */
 | 
						|
      for (let i = 0; i < filledRules.length; i += 1) {
 | 
						|
        const rule = filledRules[i];
 | 
						|
        const errors = await validateRule(name, value, rule, options, messageVariables);
 | 
						|
        if (errors.length) {
 | 
						|
          reject([{ errors, rule }]);
 | 
						|
          return;
 | 
						|
        }
 | 
						|
      }
 | 
						|
      /* eslint-enable */
 | 
						|
 | 
						|
      resolve([]);
 | 
						|
    });
 | 
						|
  } else {
 | 
						|
    // >>>>> Validate by parallel
 | 
						|
    const rulePromises: Promise<RuleError>[] = filledRules.map(rule =>
 | 
						|
      validateRule(name, value, rule, options, messageVariables).then(errors => ({ errors, rule })),
 | 
						|
    );
 | 
						|
 | 
						|
    summaryPromise = (
 | 
						|
      validateFirst ? finishOnFirstFailed(rulePromises) : finishOnAllFailed(rulePromises)
 | 
						|
    ).then((errors: RuleError[]): RuleError[] | Promise<RuleError[]> => {
 | 
						|
      // Always change to rejection for Field to catch
 | 
						|
      return Promise.reject<RuleError[]>(errors);
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  // Internal catch error to avoid console error log.
 | 
						|
  summaryPromise.catch(e => e);
 | 
						|
 | 
						|
  return summaryPromise;
 | 
						|
}
 | 
						|
 | 
						|
async function finishOnAllFailed(rulePromises: Promise<RuleError>[]): Promise<RuleError[]> {
 | 
						|
  return Promise.all(rulePromises).then(
 | 
						|
    (errorsList: RuleError[]): RuleError[] | Promise<RuleError[]> => {
 | 
						|
      const errors: RuleError[] = [].concat(...errorsList);
 | 
						|
 | 
						|
      return errors;
 | 
						|
    },
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
async function finishOnFirstFailed(rulePromises: Promise<RuleError>[]): Promise<RuleError[]> {
 | 
						|
  let count = 0;
 | 
						|
 | 
						|
  return new Promise(resolve => {
 | 
						|
    rulePromises.forEach(promise => {
 | 
						|
      promise.then(ruleError => {
 | 
						|
        if (ruleError.errors.length) {
 | 
						|
          resolve([ruleError]);
 | 
						|
        }
 | 
						|
 | 
						|
        count += 1;
 | 
						|
        if (count === rulePromises.length) {
 | 
						|
          resolve([]);
 | 
						|
        }
 | 
						|
      });
 | 
						|
    });
 | 
						|
  });
 | 
						|
}
 |