validatorjs2
Version:
Validation library inspired by Laravel's Validator
663 lines (570 loc) • 15.9 kB
JavaScript
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;