import { Validator, Rule, ValidateResult, ValidateFunc } from './types'; import buildInValidators from './builtIn/validators'; // import builtInRules from './builtIn/rules'; import parseRules from './parseRules'; /** * @example * const vusionValidator = new VusionValidator(); * vusionValidator.validate(value, 'required | max(200)') * .then(() => { * }).catch((error: string) => { * }); * * @TODO: 相同环境下的 rules 应该是一样的,如何不用在每个组件中重复解析 */ export default class VusionValidator { validators: { [prop: string]: Validator }; rules: { [prop: string]: Rule | Array }; validatingRules: Array; context: Object; constructor( validators: { [prop: string]: Validator }, rules: { [prop: string]: Rule | string | Array }, validatingRules: string | Array = [], context: Object, ) { this.context = context; this.validators = Object.create(validators || buildInValidators); this.rules = Object.create(rules || {}); Object.keys(this.rules).forEach((key) => { const rule = this.rules[key]; const normalizedRule = this.normalizeRules(rule); if (normalizedRule !== rule) this.rules[key] = normalizedRule; }); this.validatingRules = this.normalizeRules(validatingRules) as Array; } async validate(value: any, trigger: string = '', options?: Object): Promise { const validatingRules = this.validatingRules.filter((rule) => !rule.ignore && (rule.trigger || '').includes(trigger)); for (let i = 0; i < validatingRules.length; i++) { const rule = validatingRules[i]; let validate: ValidateFunc; if (typeof rule.validate === 'string') { const validator = this.validators[rule.validate]; if (!validator) throw new Error('[@vusion/validator] Unknown validator: ' + rule.validate); validate = async (value: any, rule: Rule, options?: Object) => { let args: any | Array | (() => any | Array | Promise>) = rule.args; if (typeof args === 'function') args = args.call(this.context, value, rule, options); if (args instanceof Promise) args = await args; if (!Array.isArray(args)) args = args !== undefined ? [args] : []; let valid: boolean | Promise = validator(value, ...args); if (valid instanceof Promise) valid = await valid; options = Object.assign({ args }, options, args); if (!valid) return Promise.reject(this.formatMessage(rule.message || '', options)); else return Promise.resolve(); } } else validate = rule.validate; let result: ValidateResult | Promise; // @note: 如果 rule 中没有 required 字段,则自动忽略为空的情况 if (!rule.required && !buildInValidators.required(value)) result = true; else result = validate.call(this.context, value, rule, options); if (result instanceof Promise) result = await result; if (typeof result === 'string') return Promise.reject(this.formatMessage(result, options)); else if (typeof result === 'boolean') { if (result === false) return Promise.reject(this.formatMessage(rule.message || '', options)); } } return Promise.resolve(); } /** @TODO: i18n */ formatMessage(message: string, options?: { [prop: string]: any }): string { if (!options) return message; else return message.replace(/\{([a-zA-Z0-9_]+)\}/g, (m, $1) => options[$1]); } normalizeRules(rules: string | Rule | Array, originalName?: string): Rule | Array { if (typeof rules === 'object' && !Array.isArray(rules)) return rules; else if (typeof rules === 'string') return this.parseRules(rules, originalName); else if (Array.isArray(rules)) { if (rules.some((rule) => typeof rule === 'string')) { let normalizedRules: Array = []; rules.forEach((rule) => { if (typeof rule === 'string') normalizedRules.push(...this.parseRules(rule, originalName)); else normalizedRules.push(rule); }); return normalizedRules; } else return rules as Array; } } parseRules(rules: string, originalName?: string): Array { const parsedRules = parseRules(rules); const TRIGGER_CASES: { [prop: string]: string } = { 'i': 'input', 'b': 'blur', 'ib': 'input+blur', 'bi': 'blur+input', }; const resolveArgs = (args: string) => Function(`with (this) { return ${args} }`).bind(this.context); const finalRules: Array = []; parsedRules.forEach((parsedRule) => { const ruleName = parsedRule.rule; if (!parsedRule.rule) return; const index = ruleName.indexOf('(') if (~index) { parsedRule.rule = ruleName.slice(0, index); const args = '[' + ruleName.slice(index + 1, ruleName.length - 1) + ']'; parsedRule.args = resolveArgs(args); } if (parsedRule.trigger) { if (TRIGGER_CASES[parsedRule.trigger]) parsedRule.trigger = TRIGGER_CASES[parsedRule.trigger]; } if (originalName === parsedRule.rule) throw new Error('[@vusion/validator] Circular rule reference: ' + originalName); let builtInRule = this.rules[parsedRule.rule]; if (builtInRule) { if (typeof builtInRule === 'string' || Array.isArray(builtInRule) && builtInRule.some((rule) => typeof rule === 'string')) builtInRule = this.normalizeRules(builtInRule, parsedRule.rule); if (Array.isArray(builtInRule)) { if (parsedRule.args || parsedRule.trigger) console.warn('[@vusion/validator]', 'Cannot apply args or trigger to composite rules!'); finalRules.push(...builtInRule); } else { if (builtInRule.validate) parsedRule.validate = builtInRule.validate; else parsedRule.validate = parsedRule.rule; delete parsedRule.rule; finalRules.push(Object.assign({}, builtInRule, parsedRule)); } } else { throw new Error('[@vusion/validator] Unknown rule: ' + parsedRule.rule); // parsedRule.validate = parsedRule.rule; // delete parsedRule.rule; // finalRules.push(parsedRule); } }); return finalRules; } }