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([]);
 | |
|         }
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| }
 |