UNPKG

validatorjs2

Version:

Validation library inspired by Laravel's Validator

663 lines (570 loc) 15.9 kB
var Rules = require('./rules'); var Lang = require('./lang'); var Errors = require('./errors'); var Attributes = require('./attributes'); var AsyncResolvers = require('./async'); var Validator = function (input, rules, customMessages) { var lang = Validator.getDefaultLang(); this.input = input || {}; this.messages = Lang._make(lang); this.messages._setCustom(customMessages); this.setAttributeFormatter(Validator.prototype.attributeFormatter); this.errors = new Errors(); this.errorCount = 0; this.hasAsync = false; this.rules = this._parseRules(rules); }; Validator.prototype = { constructor: Validator, /** * Default language * * @type {string} */ lang: 'en', /** * Numeric based rules * * @type {array} */ numericRules: ['integer', 'numeric'], /** * Attribute formatter. * * @type {function} */ attributeFormatter: Attributes.formatter, /** * Run validator * * @return {boolean} Whether it passes; true = passes, false = fails */ check: function () { var self = this; for (var attribute in this.rules) { var attributeRules = this.rules[attribute]; var inputValue = this._objectPath(this.input, attribute); if (this._hasRule(attribute, ['sometimes']) && !this._suppliedWithData(attribute)) { continue; } for (var i = 0, len = attributeRules.length, rule, ruleOptions, rulePassed; i < len; i++) { ruleOptions = attributeRules[i]; rule = this.getRule(ruleOptions.name); if (!this._isValidatable(rule, inputValue)) { continue; } rulePassed = rule.validate(inputValue, ruleOptions.value, attribute); if (!rulePassed) { this._addFailure(rule); } if (this._shouldStopValidating(attribute, rulePassed)) { break; } } } return this.errorCount === 0; }, /** * Run async validator * * @param {function} passes * @param {function} fails * @return {void} */ checkAsync: function (passes, fails) { var _this = this; passes = passes || function () {}; fails = fails || function () {}; var failsOne = function (rule, message) { _this._addFailure(rule, message); }; var resolvedAll = function (allPassed) { if (allPassed) { passes(); } else { fails(); } }; var asyncResolvers = new AsyncResolvers(failsOne, resolvedAll); var validateRule = function (inputValue, ruleOptions, attribute, rule) { return function () { var resolverIndex = asyncResolvers.add(rule); rule.validate(inputValue, ruleOptions.value, attribute, function () { asyncResolvers.resolve(resolverIndex); }); }; }; for (var attribute in this.rules) { var attributeRules = this.rules[attribute]; var inputValue = this._objectPath(this.input, attribute); if (this._hasRule(attribute, ['sometimes']) && !this._suppliedWithData(attribute)) { continue; } for (var i = 0, len = attributeRules.length, rule, ruleOptions; i < len; i++) { ruleOptions = attributeRules[i]; rule = this.getRule(ruleOptions.name); if (!this._isValidatable(rule, inputValue)) { continue; } validateRule(inputValue, ruleOptions, attribute, rule)(); } } asyncResolvers.enableFiring(); asyncResolvers.fire(); }, /** * Add failure and error message for given rule * * @param {Rule} rule */ _addFailure: function (rule) { var msg = this.messages.render(rule); this.errors.add(rule.attribute, msg); this.errorCount++; }, /** * Flatten nested object, normalizing { foo: { bar: 1 } } into: { 'foo.bar': 1 } * * @param {object} nested object * @return {object} flattened object */ _flattenObject: function (obj) { var flattened = {}; function recurse(current, property) { if (!property && Object.getOwnPropertyNames(current).length === 0) { return; } if (Object(current) !== current || Array.isArray(current)) { flattened[property] = current; } else { var isEmpty = true; for (var p in current) { isEmpty = false; recurse(current[p], property ? property + '.' + p : p); } if (isEmpty) { flattened[property] = {}; } } } if (obj) { recurse(obj); } return flattened; }, /** * Extract value from nested object using string path with dot notation * * @param {object} object to search in * @param {string} path inside object * @return {any|void} value under the path */ _objectPath: function (obj, path) { if (Object.prototype.hasOwnProperty.call(obj, path)) { return obj[path]; } var keys = path.replace(/\[(\w+)\]/g, '.$1').replace(/^\./, '').split('.'); var copy = {}; for (var attr in obj) { if (Object.prototype.hasOwnProperty.call(obj, attr)) { copy[attr] = obj[attr]; } } for (var i = 0, l = keys.length; i < l; i++) { if (typeof copy === 'object' && copy !== null && Object.hasOwnProperty.call(copy, keys[i])) { copy = copy[keys[i]]; } else { return; } } return copy; }, /** * Parse rules, normalizing format into: { attribute: [{ name: 'age', value: 3 }] } * * @param {object} rules * @return {object} */ _parseRules: function (rules) { var parsedRules = {}; rules = this._flattenObject(rules); for (var attribute in rules) { var rulesArray = rules[attribute]; this._parseRulesCheck(attribute, rulesArray, parsedRules); } return parsedRules; }, _parseRulesCheck: function (attribute, rulesArray, parsedRules, wildCardValues) { if (attribute.indexOf('*') > -1) { this._parsedRulesRecurse(attribute, rulesArray, parsedRules, wildCardValues); } else { this._parseRulesDefault(attribute, rulesArray, parsedRules, wildCardValues); } }, _parsedRulesRecurse: function (attribute, rulesArray, parsedRules, wildCardValues) { var parentPath = attribute.substr(0, attribute.indexOf('*') - 1); var propertyValue = this._objectPath(this.input, parentPath); if (propertyValue) { for (var propertyNumber = 0; propertyNumber < propertyValue.length; propertyNumber++) { var workingValues = wildCardValues ? wildCardValues.slice() : []; workingValues.push(propertyNumber); this._parseRulesCheck(attribute.replace('*', propertyNumber), rulesArray, parsedRules, workingValues); } } }, _parseRulesDefault: function (attribute, rulesArray, parsedRules, wildCardValues) { var attributeRules = []; if (rulesArray instanceof Array) { rulesArray = this._prepareRulesArray(rulesArray); } if (typeof rulesArray === 'string') { rulesArray = rulesArray.split('|'); } for (var i = 0, len = rulesArray.length, rule; i < len; i++) { rule = typeof rulesArray[i] === 'string' ? this._extractRuleAndRuleValue(rulesArray[i]) : rulesArray[i]; if (rule.value) { rule.value = this._replaceWildCards(rule.value, wildCardValues); this._replaceWildCardsMessages(wildCardValues); } if (Rules.isAsync(rule.name)) { this.hasAsync = true; } attributeRules.push(rule); } parsedRules[attribute] = attributeRules; }, _replaceWildCards: function (path, nums) { if (!nums) { return path; } var path2 = path; nums.forEach(function (value) { if(Array.isArray(path2)){ path2 = path2[0]; } const pos = path2.indexOf('*'); if (pos === -1) { return path2; } path2 = path2.substr(0, pos) + value + path2.substr(pos + 1); }); if(Array.isArray(path)){ path[0] = path2; path2 = path; } return path2; }, _replaceWildCardsMessages: function (nums) { var customMessages = this.messages.customMessages; var self = this; Object.keys(customMessages).forEach(function (key) { if (nums) { var newKey = self._replaceWildCards(key, nums); customMessages[newKey] = customMessages[key]; } }); this.messages._setCustom(customMessages); }, /** * Prepare rules if it comes in Array. Check for objects. Need for type validation. * * @param {array} rulesArray * @return {array} */ _prepareRulesArray: function (rulesArray) { var rules = []; for (var i = 0, len = rulesArray.length; i < len; i++) { if (typeof rulesArray[i] === 'object') { for (var rule in rulesArray[i]) { rules.push({ name: rule, value: rulesArray[i][rule] }); } } else { rules.push(rulesArray[i]); } } return rules; }, /** * Determines if the attribute is supplied with the original data object. * * @param {array} attribute * @return {boolean} */ _suppliedWithData: function (attribute) { return this.input.hasOwnProperty(attribute); }, /** * Extract a rule and a value from a ruleString (i.e. min:3), rule = min, value = 3 * * @param {string} ruleString min:3 * @return {object} object containing the name of the rule and value */ _extractRuleAndRuleValue: function (ruleString) { var rule = {}, ruleArray; rule.name = ruleString; if (ruleString.indexOf(':') >= 0) { ruleArray = ruleString.split(':'); rule.name = ruleArray[0]; rule.value = ruleArray.slice(1).join(':'); } return rule; }, /** * Determine if attribute has any of the given rules * * @param {string} attribute * @param {array} findRules * @return {boolean} */ _hasRule: function (attribute, findRules) { var rules = this.rules[attribute] || []; for (var i = 0, len = rules.length; i < len; i++) { if (findRules.indexOf(rules[i].name) > -1) { return true; } } return false; }, /** * Determine if attribute has any numeric-based rules. * * @param {string} attribute * @return {Boolean} */ _hasNumericRule: function (attribute) { return this._hasRule(attribute, this.numericRules); }, /** * Determine if rule is validatable * * @param {Rule} rule * @param {mixed} value * @return {boolean} */ _isValidatable: function (rule, value) { if (Array.isArray(value)) { return true; } if (Rules.isImplicit(rule.name)) { return true; } return this.getRule('required').validate(value); }, /** * Determine if we should stop validating. * * @param {string} attribute * @param {boolean} rulePassed * @return {boolean} */ _shouldStopValidating: function (attribute, rulePassed) { var stopOnAttributes = this.stopOnAttributes; if (typeof stopOnAttributes === 'undefined' || stopOnAttributes === false || rulePassed === true) { return false; } if (stopOnAttributes instanceof Array) { return stopOnAttributes.indexOf(attribute) > -1; } return true; }, /** * Set custom attribute names. * * @param {object} attributes * @return {void} */ setAttributeNames: function (attributes) { this.messages._setAttributeNames(attributes); }, /** * Set the attribute formatter. * * @param {fuction} func * @return {void} */ setAttributeFormatter: function (func) { this.messages._setAttributeFormatter(func); }, /** * Get validation rule * * @param {string} name * @return {Rule} */ getRule: function (name) { return Rules.make(name, this); }, /** * Stop on first error. * * @param {boolean|array} An array of attributes or boolean true/false for all attributes. * @return {void} */ stopOnError: function (attributes) { this.stopOnAttributes = attributes; }, /** * Determine if validation passes * * @param {function} passes * @return {boolean|undefined} */ passes: function (passes) { var async = this._checkAsync('passes', passes); if (async) { return this.checkAsync(passes); } return this.check(); }, /** * Determine if validation fails * * @param {function} fails * @return {boolean|undefined} */ fails: function (fails) { var async = this._checkAsync('fails', fails); if (async) { return this.checkAsync(function () {}, fails); } return !this.check(); }, /** * Check if validation should be called asynchronously * * @param {string} funcName Name of the caller * @param {function} callback * @return {boolean} */ _checkAsync: function (funcName, callback) { var hasCallback = typeof callback === 'function'; if (this.hasAsync && !hasCallback) { throw funcName + ' expects a callback when async rules are being tested.'; } return this.hasAsync || hasCallback; } }; /** * Set messages for language * * @param {string} lang * @param {object} messages * @return {this} */ Validator.setMessages = function (lang, messages) { Lang._set(lang, messages); return this; }; /** * Get messages for given language * * @param {string} lang * @return {Messages} */ Validator.getMessages = function (lang) { return Lang._get(lang); }; /** * Set default language to use * * @param {string} lang * @return {void} */ Validator.useLang = function (lang) { this.prototype.lang = lang; }; /** * Get default language * * @return {string} */ Validator.getDefaultLang = function () { return this.prototype.lang; }; /** * Set the attribute formatter. * * @param {fuction} func * @return {void} */ Validator.setAttributeFormatter = function (func) { this.prototype.attributeFormatter = func; }; /** * Stop on first error. * * @param {boolean|array} An array of attributes or boolean true/false for all attributes. * @return {void} */ Validator.stopOnError = function (attributes) { this.prototype.stopOnAttributes = attributes; }; /** * Register custom validation rule * * @param {string} name * @param {function} fn * @param {string} message * @return {void} */ Validator.register = function (name, fn, message, fnReplacement) { var lang = Validator.getDefaultLang(); Rules.register(name, fn); Lang._setRuleMessage(lang, name, message); }; /** * Register custom validation rule * * @param {string} name * @param {function} fn * @param {string} message * @param {function} fnReplacement * @return {void} */ Validator.registerImplicit = function (name, fn, message, fnReplacement) { var lang = Validator.getDefaultLang(); Rules.registerImplicit(name, fn); Lang._setRuleMessage(lang, name, message); }; /** * Register asynchronous validation rule * * @param {string} name * @param {function} fn * @param {string} message * @return {void} */ Validator.registerAsync = function (name, fn, message, fnReplacement) { var lang = Validator.getDefaultLang(); Rules.registerAsync(name, fn); Lang._setRuleMessage(lang, name, message); }; /** * Register asynchronous validation rule * * @param {string} name * @param {function} fn * @param {string} message * @return {void} */ Validator.registerAsyncImplicit = function (name, fn, message) { var lang = Validator.getDefaultLang(); Rules.registerAsyncImplicit(name, fn); Lang._setRuleMessage(lang, name, message); }; /** * Register validator for missed validation rule * * @param {string} name * @param {function} fn * @param {string} message * @return {void} */ Validator.registerMissedRuleValidator = function(fn, message) { Rules.registerMissedRuleValidator(fn, message); }; module.exports = Validator;