├── .gitignore ├── .jshintrc ├── .travis.yml ├── index.js ├── lib ├── errors.js ├── index.js ├── lang │ ├── de.json │ ├── en-pr.json │ └── en.json ├── language.js ├── manager.js ├── response.js └── rules │ ├── _chriso.js │ ├── array.js │ ├── between.js │ ├── boolean.js │ ├── camelCase.js │ ├── equal.js │ ├── greaterThan.js │ ├── in.js │ ├── lessThan.js │ ├── longer.js │ ├── shorter.js │ ├── snakeCase.js │ ├── string.js │ ├── studlyCase.js │ └── within.js ├── package.json ├── readme.md └── spec ├── E2ESpec.js ├── LanguageSpec.js ├── ManagerSpec.js ├── ResponseSpec.js ├── RulesSpec.js ├── ValidatorSpec.js └── fixture ├── dict.json └── rules ├── a.js ├── multi.js └── someOtherFile.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | node_modules 27 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "unused": true 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Validator = module.exports.validator = require('./lib'); 2 | 3 | module.exports = function () { 4 | return new Validator(); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | function RuleError () { 4 | Error.apply(this, arguments); 5 | } 6 | RuleError.prototype = Object.create(Error.prototype); 7 | 8 | function ValidationError (response) { 9 | this.name = 'ValidationError'; 10 | 11 | _.merge(this, response); 12 | } 13 | ValidationError.prototype = Object.create(Error.prototype); 14 | 15 | module.exports.RuleError = RuleError; 16 | module.exports.ValidationError = ValidationError; 17 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var Bluebird = require('bluebird'); 2 | var _ = require('lodash'); 3 | 4 | var Response = require('./response'); 5 | var Manager = require('./manager'); 6 | var Language = require('./language'); 7 | 8 | var ValidationError = require('./errors').ValidationError; 9 | 10 | // Symbol between the rule name and its parameters in string-type rules. 11 | var methodDelimiter = ':'; 12 | // Symbol between arguments in rules. 13 | var argumentDelimiter = ','; 14 | 15 | function Validator () { 16 | this.validators = new Manager(); 17 | this.language = new Language(); 18 | this.language.builtIn('en'); 19 | } 20 | 21 | /** 22 | * Checks to ensure all data required by the ruleset is present, and trims 23 | * the rules to remove rules that apply to data which is not present. 24 | * 25 | * @param {Response} response 26 | * @param {Object} data 27 | * @param {Object} rules 28 | * @return {Object} 29 | */ 30 | Validator.prototype.extractRequired = function (response, data, rules) { 31 | var output = {}; 32 | var key; 33 | 34 | for (key in rules) { 35 | // Just add it to the output if the data key exists. 36 | if (isPresent(data, key)) { 37 | continue; 38 | } 39 | 40 | // Check to see if it's required at all. If so, add an error. 41 | for (var i = 0, l = rules[key].length; i < l; i++) { 42 | var rule = rules[key][i]; 43 | 44 | switch (rule[0]) { 45 | case 'required': 46 | // Add an error if the key is not present. 47 | if (!isPresent(data, key)) { 48 | response.addError(key, i); 49 | } 50 | break; 51 | case 'requiredWith': 52 | // Add an error if the key is not present at the "with" 53 | // key is. 54 | if (!isPresent(data, key) && isPresent(data, rule[1])) { 55 | response.addError(key, i); 56 | } 57 | break; 58 | case 'requiredWithout': 59 | // Add an error if the key is not present at the 60 | // "without" was not. 61 | if (!isPresent(data, key) && !isPresent(data, rule[1])) { 62 | response.addError(key, i); 63 | } 64 | break; 65 | } 66 | } 67 | 68 | // Do no further validation on missing items. 69 | delete rules[key]; 70 | delete data[key]; 71 | } 72 | 73 | // Now remove all "required"s as they aren't actually validators. 74 | for (key in rules) { 75 | rules[key] = _.reject(rules[key], isRequired); 76 | } 77 | 78 | return output; 79 | }; 80 | 81 | /** 82 | * Formats rules from the option string-based format, to arrays. Attempts 83 | * to case data types when appropriate. 84 | */ 85 | Validator.prototype.fulfillRules = function (rules) { 86 | for (var key in rules) { 87 | rules[key] = rules[key].map(fulfillRule); 88 | } 89 | }; 90 | 91 | /** 92 | * Attempts to run a validation. 93 | * 94 | * @param {Object} data 95 | * @param {Object} rules 96 | * @return {Promise} 97 | */ 98 | Validator.prototype.try = function (data, rules) { 99 | rules = clone(rules); 100 | data = clone(data); 101 | 102 | var response = new Response(this.language, data, rules); 103 | this.fulfillRules(rules); 104 | this.extractRequired(response, data, rules); 105 | 106 | var todo = [], promise; 107 | // Add promise to the todo for every validator in every rule. 108 | for (var key in rules) { 109 | for (var i = 0, l = rules[key].length; i < l; i++) { 110 | promise = this.validators 111 | .run(data, key, rules[key][i]) 112 | .then(addError.bind(this, response, key, i)); 113 | 114 | todo.push(promise); 115 | } 116 | } 117 | 118 | // Run all the todos through and pass back the response object 119 | return Bluebird.all(todo) 120 | .then(function () { 121 | return response; 122 | }); 123 | }; 124 | 125 | /** 126 | * Attempts to run a validation with .try(), throwing a ValidationError on failure. 127 | * 128 | * @return {Promise} 129 | */ 130 | Validator.prototype.tryOrFail = function () { 131 | // Apply arguments to .try() and simply throw an exception if it fails 132 | return this.try.apply(this, arguments) 133 | .then(function (result) { 134 | if (result.failed) { 135 | throw new ValidationError(result); 136 | } 137 | 138 | return result; 139 | }); 140 | }; 141 | 142 | /** 143 | * Parses string-type rule inputs into usable arrays. 144 | * @param {Array|String} rule 145 | * @return {Array} 146 | */ 147 | function fulfillRule (rule) { 148 | // If it's already an array, do nothing. 149 | if (_.isArray(rule)) { 150 | return rule; 151 | } 152 | 153 | var division = rule.indexOf(methodDelimiter); 154 | // If the rule has not parameters, simply output it. 155 | if (division === -1) { 156 | return [rule.trim()]; 157 | } 158 | 159 | var output = [ rule.slice(0, division).trim() ]; 160 | // Loop over all the arguments 161 | var args = rule.slice(division + 1).split(argumentDelimiter); 162 | for (var i = 0, l = args.length; i < l; i++) { 163 | var arg = args[i].trim(); 164 | 165 | // JSON parse will fix types for booleans, numbers, and 166 | // arrays/objects 167 | try { 168 | arg = JSON.parse(arg); 169 | } catch (e) {} 170 | 171 | output.push(arg); 172 | } 173 | 174 | return output; 175 | } 176 | 177 | /** 178 | * Handler function. Expected to be partially bound and have the "result" 179 | * fulfilled, to prevent the need to define functions within a loop. 180 | * 181 | * @param {Response} response 182 | * @param {String} key 183 | * @param {Number} ruleIndex 184 | * @param {Boolean} result 185 | */ 186 | function addError (response, key, ruleIndex, result) { 187 | if (!result) { 188 | response.addError(key, ruleIndex); 189 | } 190 | } 191 | 192 | /** 193 | * Returns whether the rule is "required". 194 | * @param {[]String} rule 195 | * @return {Boolean} 196 | */ 197 | function isRequired(rule) { 198 | return rule[0].indexOf('required') === 0; 199 | } 200 | 201 | 202 | /** 203 | * Checks if the give key is present in the dataset (not undefined or null). 204 | * @param {Object} data 205 | * @param {String} key 206 | * @return {Boolean} 207 | */ 208 | function isPresent(data, key) { 209 | // Loosely equal to null to check for undefined/null. 210 | // Do nothing if the key exists in the dataset. 211 | return typeof data[key] !== 'undefined' && data[key] !== null; 212 | } 213 | 214 | /** 215 | * Simple function to clone an object's enumerable properties. 216 | * @param {Object} obj 217 | * @return {Object} 218 | */ 219 | function clone (obj) { 220 | var output = {}; 221 | for (var key in obj) { 222 | if (_.isPlainObject(obj[key])) { 223 | output[key] = clone(obj[key]); 224 | } else { 225 | output[key] = obj[key]; 226 | } 227 | } 228 | 229 | return output; 230 | } 231 | 232 | module.exports = Validator; 233 | -------------------------------------------------------------------------------- /lib/lang/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "array": "<%= key %> muss ein array sein.", 3 | "boolean": "<%= key %> muss ein boolean sein.", 4 | "date": "<%= key %> muss ein Datum sein.", 5 | "float": "<%= key %> muss eine Dezimalzahl sein.", 6 | "int": "<%= key %> muss eine ganze Zahl sein.", 7 | "numeric": "<%= key %> muss eine Zahl sein", 8 | "string": "<%= key %> muss Text sein.", 9 | "after": "<%= key %> muss nach <%= args[0] %> sein.", 10 | "before": "<%= key %> muss vor <%= args[0] %> sein.", 11 | "alpha": "<%= key %> muss alphabetisch sein.", 12 | "alphanumeric": "<%= key %> muss alphanumerisch sein.", 13 | "ascii": "<%= key %> darf nur ASCII-Zeichen enthalten.", 14 | "base64": "<%= key %> muss base64-codiert sein.", 15 | "between": "<%= key %> muss zwischen <%= args[0] %> und <%= args[1] %> Zeichen lang sein.", 16 | "byteLength": "<%= key %> muss zwischen <%= args[0] %> und <%= args[1] %> Bytes lang sein.", 17 | "contains": "<%= key %> muss <%= args[0] %> beinhalten.", 18 | "creditCard": "<%= key %> ist keine gültige Kreditkartennummer.", 19 | "email": "<%= key %> muss eine gültige email sein.", 20 | "FQDN": "<%= key %> ist keine gültige Domain.", 21 | "hexadecimal": "<%= key %> muss eine hexadecimale Ziffer sein.", 22 | "hexColor": "<%= key %> muss ein hexadecimaler Farbencode sein.", 23 | "IP": "<%= key %> muss eine gültige IP-Adresse sein.", 24 | "ISBN": "<%= key %> muss eine gültige ISBN sein.", 25 | "JSON": "<%= key %> muss gültiger JSON sein.", 26 | "length": "<%= key %> muss <%= args[0] %> Zeichen lang sein.", 27 | "longer": "<%= key %> muss mehr als <%= args[0] %> Zeichen enthalten.", 28 | "lowercase": "<%= key %> muss klein geschrieben werden.", 29 | "matches": "<%= key %> is nicht im korrekten Format.", 30 | "mongoId": "<%= key %> muss eine gültige ID sein.", 31 | "shorter": "<%= key %> muss weniger als <%= args[0] %> Zeichen enthalten.", 32 | "uppercase": "<%= key %> muss groß geschrieben werden.", 33 | "URL": "<%= key %> muss eine gültige URL sein.", 34 | "UUID": "<%= key %> ist keine gültige UUID<%= args[0] || '' %>.", 35 | "divisibleBy": "<%= key %> muss ein Vielfaches von <%= args[0] %> sein.", 36 | "greaterThan": "<%= key %> muss größer als <%= args[0] %> sein.", 37 | "lessThan": "<%= key %> muss kleiner als <%= args[0] %> sein.", 38 | "within": "<%= key %> muss größer als <%= args[0] %> und kleiner als <%= args[1] %> sein.", 39 | "equals": "<%= key %> muss <%= args[0] %> entsprechen.", 40 | "in": "<%= key %> muss einer Wahl von diesen entsprechen: <%= args.join(', ') %>.", 41 | "not": "<%= key %> ist nicht gültig.", 42 | "required": "<%= key %> ist notwendig.", 43 | "requiredWith": "<%= key %> ist notwendig.", 44 | "requiredWithout": "<%= key %> ist notwendig.", 45 | "$missing": "<%= key %> ist nicht korrekt." 46 | } 47 | -------------------------------------------------------------------------------- /lib/lang/en-pr.json: -------------------------------------------------------------------------------- 1 | { 2 | "array": "Arr! The <%= key %> ain't an array.", 3 | "boolean": "Arr! The <%= key %> ain't a boolean.", 4 | "date": "Arr! The <%= key %> ain't a date.", 5 | "float": "Arr! The <%= key %> ain't a decimal number.", 6 | "int": "Arr! The <%= key %> ain't an integer.", 7 | "numeric": "Arr! The <%= key %> ain't numeric.", 8 | "string": "Arr! The <%= key %> ain't textual.", 9 | "after": "Arr! The <%= key %> ain't after <%= args[0] %>.", 10 | "before": "Arr! The <%= key %> ain't before <%= args[0] %>.", 11 | "alpha": "Arr! The <%= key %> ain't alphabetical.", 12 | "alphanumeric": "Arr! The <%= key %> ain't alphanumeric.", 13 | "ascii": "Arr! The <%= key %> contains pesky non-ascii characters!", 14 | "base64": "Arr! The <%= key %> ain't base64 encoded.", 15 | "between": "Arr! The <%= key %> must be <%= args[0] %> and <%= args[1] %> characters long.", 16 | "byteLength": "Arr! The <%= key %> must be <%= args[0] %> and <%= args[1] %> bytes long.", 17 | "contains": "Arr! The <%= key %> must contain <%= args[0] %>.", 18 | "creditCard": "Arr! The <%= key %> not be a valid credit card number.", 19 | "email": "Arr! The <%= key %> not be a valid email.", 20 | "FQDN": "Arr! The <%= key %> not be a valid domain name.", 21 | "hexadecimal": "Arr! The <%= key %> not be a hexadecimal number.", 22 | "hexColor": "Arr! The <%= key %> note be a hexadecimal color.", 23 | "IP": "Arr! The <%= key %> not be a valid IP address.", 24 | "ISBN": "Arr! The <%= key %> not be valid ISBN.", 25 | "JSON": "Arr! The <%= key %> not be valid JSON.", 26 | "length": "Arr! The <%= key %> not be <%= args[0] %> characters long.", 27 | "longer": "Arr! The <%= key %> ain't more than <%= args[0] %> characters long.", 28 | "lowercase": "Arr! The <%= key %> ain't lowercase.", 29 | "matches": "Arr! The <%= key %> not be the right format.", 30 | "mongoId": "Arr! The <%= key %> not be a valid ID.", 31 | "shorter": "Arr! The <%= key %> ain't shorter than <%= args[0] %> characters.", 32 | "shorter": "Arr! The <%= key %> ain't shorter than <%= args[0] %> characters.", 33 | "uppercase": "Arr! The <%= key %> ain't uppercase.", 34 | "URL": "Arr! The <%= key %> not be a valid URL.", 35 | "UUID": "Arr! The <%= key %> not be a valid UUID<%= args[0] || '' %>!", 36 | "divisibleBy": "Arr! The <%= key %> ain't multiple of <%= args[0] %>.", 37 | "greaterThan": "Arr! The <%= key %> ain't more than <%= args[0] %>.", 38 | "lessThan": "Arr! The <%= key %> ain't less than <%= args[0] %>!", 39 | "within": "Arr! The <%= key %> ain't greater than <%= args[0] %> and less than <%= args[1] %>!", 40 | "equals": "Arr! The <%= key %> ain't matchin' <%= args[0] %>.", 41 | "in": "Arr! The <%= key %> be one of: <%= args.join(', ') %>.", 42 | "not": "Arr! The <%= key %> be invalid!", 43 | "required": "Arr! The <%= key %> be required.", 44 | "requiredWith": "Arr! The <%= key %> be required.", 45 | "requiredWithout": "Arr! The <%= key %> be required.", 46 | "$missing": "Arr! The <%= key %> be invalid." 47 | } 48 | -------------------------------------------------------------------------------- /lib/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "array": "The <%= key %> must be an array.", 3 | "boolean": "The <%= key %> must be a boolean.", 4 | "date": "The <%= key %> must be a date.", 5 | "float": "The <%= key %> must be a decimal number.", 6 | "int": "The <%= key %> must be an integer.", 7 | "numeric": "The <%= key %> must be numeric.", 8 | "string": "The <%= key %> must be textual.", 9 | "after": "The <%= key %> must be after <%= args[0] %>.", 10 | "before": "The <%= key %> must be before <%= args[0] %>.", 11 | "alpha": "The <%= key %> must be alphabetical.", 12 | "alphanumeric": "The <%= key %> must be alphanumeric.", 13 | "ascii": "The <%= key %> may only contain ascii characters.", 14 | "base64": "The <%= key %> must be base64 encoded.", 15 | "between": "The <%= key %> must be between <%= args[0] %> and <%= args[1] %> characters long.", 16 | "byteLength": "The <%= key %> must be <%= args[0] %> and <%= args[1] %> bytes long.", 17 | "contains": "The <%= key %> must contain <%= args[0] %>.", 18 | "creditCard": "The <%= key %> is not a valid credit card number.", 19 | "email": "The <%= key %> must be a valid email.", 20 | "FQDN": "The <%= key %> is not a valid domain name.", 21 | "hexadecimal": "The <%= key %> must be a hexadecimal number.", 22 | "hexColor": "The <%= key %> must be a hexadecimal color.", 23 | "IP": "The <%= key %> must be a valid IP address.", 24 | "ISBN": "The <%= key %> must be a valid ISBN.", 25 | "JSON": "The <%= key %> must be valid JSON.", 26 | "length": "The <%= key %> must be <%= args[0] %> characters long.", 27 | "longer": "The <%= key %> must be more than <%= args[0] %> characters long.", 28 | "lowercase": "The <%= key %> must be lowercase.", 29 | "matches": "The <%= key %> is in the wrong format.", 30 | "mongoId": "The <%= key %> must be a valid ID.", 31 | "shorter": "The <%= key %> must be shorter than <%= args[0] %> characters.", 32 | "uppercase": "The <%= key %> must be uppercase.", 33 | "URL": "The <%= key %> must be a valid URL.", 34 | "UUID": "The <%= key %> is not a valid UUID<%= args[0] || '' %>.", 35 | "divisibleBy": "The <%= key %> should be a multiple of <%= args[0] %>.", 36 | "greaterThan": "The <%= key %> should be greater than <%= args[0] %>.", 37 | "lessThan": "The <%= key %> should be less than <%= args[0] %>.", 38 | "within": "The <%= key %> should be greater than <%= args[0] %> and less than <%= args[1] %>.", 39 | "equals": "The <%= key %> must match <%= args[0] %>.", 40 | "in": "The <%= key %> must be one of: <%= args.join(', ') %>.", 41 | "not": "The <%= key %> is invalid.", 42 | "required": "The <%= key %> is required.", 43 | "requiredWith": "The <%= key %> is required.", 44 | "requiredWithout": "The <%= key %> is required.", 45 | "$missing": "The <%= key %> is invalid." 46 | } 47 | -------------------------------------------------------------------------------- /lib/language.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | function Language () { 4 | this.dictionary = {}; 5 | this.globals = {}; 6 | } 7 | 8 | /** 9 | * Loads a built-in language set. 10 | * @param {String} name 11 | * @return {Language} 12 | */ 13 | Language.prototype.builtIn = function (name) { 14 | this.set(__dirname + '/lang/' + name + '.json'); 15 | return this; 16 | }; 17 | 18 | /** 19 | * Updates the language to use the given dictionary. If its an object, it 20 | * will be used raw. If it's a string, we'll load it as necessary. 21 | * 22 | * @param {String|Object} dictionary 23 | * @return {Language} 24 | */ 25 | Language.prototype.set = function (dictionary) { 26 | if (_.isString(dictionary)) { 27 | this.dictionary = require(dictionary); 28 | } else { 29 | this.dictionary = dictionary; 30 | } 31 | 32 | return this; 33 | }; 34 | 35 | /** 36 | * Updates an existing language set with the given key/value(s): if the 37 | * first arguments is an object, it will be extended over the dictionary. 38 | * 39 | * @param {String|Object} key 40 | * @param {String} value 41 | * @return {Language} 42 | */ 43 | Language.prototype.extend = function (key, value) { 44 | if (typeof value === 'undefined') { 45 | _.extend(this.dictionary, key); 46 | } else { 47 | this.dictionary[key] = value; 48 | } 49 | 50 | return this; 51 | }; 52 | 53 | /** 54 | * Renders a dictionary entry with the given data. 55 | * 56 | * @param {String} key 57 | * @param {Object} data 58 | * @return {String} 59 | */ 60 | Language.prototype.resolve = function (key, data) { 61 | var template = this.dictionary[key]; 62 | if (typeof template === 'undefined') { 63 | template = this.dictionary.$missing; 64 | } 65 | 66 | return _.template(template, _.extend({}, this.globals, data)); 67 | }; 68 | 69 | /** 70 | * Adds the give kv pair into the "global" template variables. 71 | * @param {String} key 72 | * @param {*} value 73 | * @return {Language} 74 | */ 75 | Language.prototype.global = function (key, value) { 76 | this.globals[key] = value; 77 | return this; 78 | }; 79 | 80 | module.exports = Language; 81 | -------------------------------------------------------------------------------- /lib/manager.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | var _ = require('lodash'); 5 | var Bluebird = require('bluebird'); 6 | 7 | var RuleError = require('./errors').RuleError; 8 | 9 | function Manager () { 10 | this.rules = {}; 11 | this.loadDir(__dirname + '/rules'); 12 | } 13 | 14 | /** 15 | * Loads a directory full of rule defintions. It's expected that every file 16 | * exports a function which is a valid rule, and that the file name is the 17 | * rule name. Alternately it may export an object which will be extended 18 | * atop the existing rules. 19 | * 20 | * @param {String} path 21 | * @return {Manager} 22 | */ 23 | Manager.prototype.loadDir = function (loadPath) { 24 | // Get all the files in that directory. 25 | var files = fs.readdirSync(loadPath); 26 | 27 | for (var i = 0, l = files.length; i < l; i++) { 28 | var file = files[i]; 29 | // Ignore non-.js files 30 | if (path.extname(file) !== '.js') { 31 | continue; 32 | } 33 | 34 | // Pull out the file base name and the path to require. 35 | var module = require(path.join(loadPath, file)); 36 | if (typeof module === 'function') { 37 | this.rules[path.basename(file, '.js')] = module; 38 | } else { 39 | _.extend(this.rules, module); 40 | } 41 | } 42 | 43 | return this; 44 | }; 45 | 46 | /** 47 | * Runs a validation for a rule. 48 | * 49 | * @param {Object} data 50 | * @param {String} key 51 | * @param {Array} params 52 | * @return {Promise} 53 | */ 54 | Manager.prototype.run = function (data, key, params) { 55 | var rule = this.rules[params[0]]; 56 | var args = [key, data[key]].concat(params.slice(1)); 57 | 58 | // Rule lookup failed, resolve to an error. 59 | if (typeof rule === 'undefined') { 60 | return Bluebird.reject(new RuleError(params[0] + ' is not defined.')); 61 | } 62 | 63 | var output = rule.apply(data, args); 64 | if (typeof output.then === 'function') { 65 | // If it returned a promise, just give it back. 66 | return output; 67 | } else { 68 | // Otherwise resolve it explicitly 69 | return Bluebird.resolve(output); 70 | } 71 | }; 72 | 73 | /** 74 | * Adds a new rule to the manager. Takes a named function as its only 75 | * only parameter, or takes an explcit name as its first param and 76 | * any function as its second. 77 | * 78 | * @param {String|Function} name 79 | * @param {Function} fn 80 | * @return {Manager} 81 | */ 82 | Manager.prototype.add = function (name, fn) { 83 | if (typeof fn === 'undefined') { 84 | // Try to match the function name, casting it to a string. 85 | var parts = ('' + name).match(/^function (.*?) ?\(/i); 86 | // If we didn't get a match... 87 | if (parts === null) { 88 | throw new RuleError('Unable to recognize name in rule definition'); 89 | } 90 | // If we got an anonymous function. 91 | if (parts[1] === '') { 92 | throw new RuleError('Cannot define rule based on anonymous function. ' + 93 | 'Please pass a name explicitly'); 94 | } 95 | 96 | fn = name; 97 | name = parts[1]; 98 | } 99 | 100 | this.rules[name] = fn; 101 | return this; 102 | }; 103 | 104 | module.exports = Manager; 105 | -------------------------------------------------------------------------------- /lib/response.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | /** 4 | * Used to pass back to the consumer. Contains validation errors and success 5 | * status. 6 | * 7 | * @param {Object} language 8 | * @param {Object} data 9 | * @param {String} rules 10 | */ 11 | function Response (language, data, rules) { 12 | this.language = language; 13 | this.data = data; 14 | this.rules = rules; 15 | 16 | this.errors = {}; 17 | this.hasErrors = false; 18 | 19 | var self = this; 20 | // Define the "passed" property 21 | Object.defineProperty(this, 'passed', { 22 | enumerable: true, 23 | get: function () { 24 | return !self.hasErrors; 25 | } 26 | }); 27 | // Define the "failed" property 28 | Object.defineProperty(this, 'failed', { 29 | enumerable: true, 30 | get: function () { 31 | return self.hasErrors; 32 | } 33 | }); 34 | } 35 | 36 | /** 37 | * Adds a new error to the response, and sets the status to having failed. 38 | * @param {String} key [description] 39 | * @param {Number} ruleIndex [description] 40 | */ 41 | Response.prototype.addError = function (key, ruleIndex) { 42 | if (typeof this.errors[key] === 'undefined') { 43 | this.errors[key] = []; 44 | } 45 | 46 | var ruleSet = this.rules[key][ruleIndex]; 47 | var message = this.language.resolve(ruleSet[0], { 48 | key: key, 49 | value: this.data[key], 50 | args: ruleSet.slice(1) 51 | }); 52 | 53 | this.errors[key].push(message); 54 | this.hasErrors = true; 55 | }; 56 | 57 | 58 | // Bind lodash methods to the response, passing them through to the 59 | // errors object. 60 | var methods = [ 'keys', 'values', 'pairs', 'invert', 'pick', 'omit', 'forIn', 'has' ]; 61 | methods.forEach(function (method) { 62 | Response.prototype[method] = function () { 63 | var args = [ this.errors ].concat(arguments); 64 | return _[method].apply(_, args); 65 | }; 66 | }); 67 | 68 | module.exports = Response; 69 | -------------------------------------------------------------------------------- /lib/rules/_chriso.js: -------------------------------------------------------------------------------- 1 | var validator = require('validator'); 2 | 3 | // Mapping of chriso/validation names => artisan-validator names. 4 | var mapping = { 5 | contains: 'contains', 6 | matches: 'matches', 7 | isEmail: 'email', 8 | isURL: 'URL', 9 | isFQDN: 'FQDN', 10 | isIP: 'IP', 11 | isAlpha: 'alpha', 12 | isNumeric: 'numeric', 13 | isAlphanumeric: 'alphanumeric', 14 | isBase64: 'base64', 15 | isHexadecimal: 'hexadecimal', 16 | isHexColor: 'hexColor', 17 | isLowercase: 'lowercase', 18 | isUppercase: 'uppercase', 19 | isInt: 'int', 20 | isFloat: 'float', 21 | isDivisibleBy: 'divisibleBy', 22 | isNull: 'null', 23 | isLength: 'length', 24 | isByteLength: 'byteLength', 25 | isUUID: 'uUID', 26 | isDate: 'date', 27 | isAfter: 'after', 28 | isBefore: 'before', 29 | isCreditCard: 'creditCard', 30 | isISBN: 'ISBN', 31 | isJSON: 'JSON', 32 | isAscii: 'ascii', 33 | isVariableWidth: 'variableWidth', 34 | isMongoId: 'mongoId' 35 | }; 36 | 37 | module.exports = {}; 38 | 39 | for (var key in mapping) { 40 | module.exports[mapping[key]] = validate.bind(null, key); 41 | } 42 | 43 | function validate (fn) { 44 | return validator[fn].apply(validator, [].slice.call(arguments, 2)); 45 | } 46 | -------------------------------------------------------------------------------- /lib/rules/array.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | module.exports = function (key, value) { 4 | return _.isArray(value); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/rules/between.js: -------------------------------------------------------------------------------- 1 | module.exports = function (key, value, min, max) { 2 | return value.length > min && value.length < max; 3 | }; 4 | -------------------------------------------------------------------------------- /lib/rules/boolean.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | module.exports = function (key, value, expected) { 4 | if (!_.isBoolean(value)) { 5 | return false; 6 | } 7 | 8 | if (typeof expected !== 'undefined') { 9 | return value === expected; 10 | } else { 11 | return true; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /lib/rules/camelCase.js: -------------------------------------------------------------------------------- 1 | var pattern = /^[a-z][a-zA-Z0-9]*$/; 2 | 3 | module.exports = function (key, value) { 4 | return pattern.test(value); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/rules/equal.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | module.exports = function (key, value, expected) { 4 | try { 5 | assert.deepEqual(value, expected); 6 | return true; 7 | } catch (e) { 8 | if (e instanceof assert.AssertionError) { 9 | return false; 10 | } else { 11 | throw e; 12 | } 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /lib/rules/greaterThan.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | module.exports = function (key, value, min) { 4 | return _.isNumber(value) && value > min; 5 | }; 6 | -------------------------------------------------------------------------------- /lib/rules/in.js: -------------------------------------------------------------------------------- 1 | module.exports = function (key, value) { 2 | var expected = [].slice.call(arguments, 2); 3 | return expected.indexOf(value) !== -1; 4 | }; 5 | -------------------------------------------------------------------------------- /lib/rules/lessThan.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | module.exports = function (key, value, max) { 4 | return _.isNumber(value) && value < max; 5 | }; 6 | -------------------------------------------------------------------------------- /lib/rules/longer.js: -------------------------------------------------------------------------------- 1 | module.exports = function (key, value, min) { 2 | return value.length > min; 3 | }; 4 | -------------------------------------------------------------------------------- /lib/rules/shorter.js: -------------------------------------------------------------------------------- 1 | module.exports = function (key, value, min) { 2 | return value.length < min; 3 | }; 4 | -------------------------------------------------------------------------------- /lib/rules/snakeCase.js: -------------------------------------------------------------------------------- 1 | var pattern = /^[a-z0-9\_]+$/; 2 | 3 | module.exports = function (key, value) { 4 | return pattern.test(value); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/rules/string.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | module.exports = function (key, value) { 4 | return _.isString(value); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/rules/studlyCase.js: -------------------------------------------------------------------------------- 1 | var pattern = /^[A-Z][a-zA-Z0-9]*$/; 2 | 3 | module.exports = function (key, value) { 4 | return pattern.test(value); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/rules/within.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | module.exports = function (key, value, min, max) { 4 | return _.isNumber(value) && value >= min && value <= max; 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "artisan-validator", 3 | "version": "1.1.2", 4 | "description": "Fun and friendly data validation.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node node_modules/jasmine-node/bin/jasmine-node spec && node node_modules/jshint/bin/jshint lib" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/MCProHosting/artisan-validator.git" 12 | }, 13 | "keywords": [ 14 | "validator", 15 | "validation" 16 | ], 17 | "author": "Connor Peet ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/MCProHosting/artisan-validator/issues" 21 | }, 22 | "homepage": "https://github.com/MCProHosting/artisan-validator", 23 | "devDependencies": { 24 | "jasmine-node": "~2.0.0", 25 | "jshint": "^2.5.11" 26 | }, 27 | "dependencies": { 28 | "bluebird": "~2.3.11", 29 | "lodash": "~2.4.1", 30 | "validator": "~3.24.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Artisan Validator 2 | 3 | [![Build Status](https://travis-ci.org/MCProHosting/artisan-validator.svg)](https://travis-ci.org/MCProHosting/artisan-validator) 4 | [![Code Climate](https://codeclimate.com/github/MCProHosting/artisan-validator/badges/gpa.svg)](https://codeclimate.com/github/MCProHosting/artisan-validator) 5 | 6 | A module for simple and fun validation in Node.js. Install with: 7 | 8 | ``` 9 | npm install --save artisan-validator 10 | ``` 11 | 12 | Works in Node, or in the browser with Browserify. 13 | 14 | ### Quick Example 15 | 16 | ```js 17 | var validator = require('artisan-validator')(); 18 | var rules = { 19 | username: ['required', 'between: 4, 30', 'alphanumeric'], 20 | password: ['required', 'longer: 5'], 21 | acceptTOS: ['required', 'boolean: true'] 22 | }; 23 | 24 | validator.try(req.body, rules).then(function (result) { 25 | if (result.failed) { 26 | res.json(400, result.errors); 27 | } else { 28 | registerAccount(); 29 | } 30 | }); 31 | ``` 32 | 33 | ### Usage 34 | 35 | #### Making the Validator 36 | 37 | First we have to "check out" a validator instance to use later. That's easy enough: 38 | 39 | ```js 40 | var validator = require('artisan-validator')(); 41 | ``` 42 | 43 | #### Validating an Object 44 | 45 | All validations attempts return promises that are resolved to a results object. Although there are not any built-in asynchonous validations, you may very well want to add a [custom rule](#adding-custom-validators) that, for examples, checks to make sure a value does not already exist in the database. 46 | 47 | We use the `try(obj, rules)` method, which returns a promise that is always resolved to a [Results object](#working-with-the-results-object). 48 | 49 | ```js 50 | validator.try(req.body, { 51 | // Require the username, make sure it's between 4 and 30 alphanumeric characters. 52 | username: ['required', 'alphanumeric', 'between: 4, 30'], 53 | // Instead of 'between: 4, 30' we could alternately write ['between', 4, 30] 54 | password: ['required', 'not: contain: password'] 55 | }).then(function (result) { 56 | if (result.failed) { 57 | res.json(400, result.errors); 58 | // -> {"username": ["The username should be alphanumeric."]} 59 | } else { 60 | res.json(200, "Your data is valid :)"); 61 | } 62 | }); 63 | ``` 64 | 65 | ##### Writing the Rules w/ Arguments 66 | 67 | Rules are defined as an object with string keys that correspond to the expected input, as we saw above. The values should be an array of validators to run, like so: 68 | 69 | ```js 70 | { 71 | // This will effectively run `required()` 72 | a: [ 'required' ], 73 | // This will run `between(4, 6)` 74 | b: [ 'between: 4, 6' ], 75 | // So will this (we don't care how you space) 76 | c: [ 'between:4,6' ], 77 | // And this will also run `between(4, 6)` 78 | d: [[ 'between', 4, 6 ]] 79 | } 80 | ``` 81 | 82 | > Note: when working with the string-based shorthand, we try to typecase correctly, but that's its not 100% magical. If you need more precise control your validator's input types, use the array notation. 83 | 84 | ##### Working with the Results object 85 | 86 | The results object will always define the following properties: 87 | 88 | * `passed: Boolean` True if the validation rules passed, false otherwise. 89 | * `failed: Boolean` True if the validation rules failed, false otherwise. 90 | * `errors: Object` An object where keys are properties that had rules which failed (that is, values that are OK will not be included) and values are an array of strings of error messages, according to the [Language](#language). 91 | 92 | Additionally, we pass through the following lodash methods on the results that can be used to work with the errors object: `keys`, `values`, `pairs`, `invert`, `pick`, `omit`, `forIn`, `has`. 93 | 94 | #### Adding Custom Validators 95 | 96 | Custom validators may be defined on the validation instance. For example, if we wanted to make an incredibly useful validator to ensure a given input repeats "foo" a defined number of times, we might do the following: 97 | 98 | ```js 99 | validator.validators.add(function isFoo (key, value, times) { 100 | return value === Array(times + 1).join('foo'); 101 | }); 102 | // Make sure fooBlerg is present and equal to "foofoofoo" 103 | validator.try(req.body, { fooBlerg: ['required', 'isFoo: 3'] }); 104 | ``` 105 | 106 | We pull the function name if you pass in a named function, but alternately you can define rules by passing in the name as the first argument: `validation.add('foo', function () { ... })`. 107 | 108 | Your custom function always recieves at least two arguments, the key and value under validation, then is passed any arguments given in the ruleset. It is executed in the context of the input (`this` will be `req.body`, the case of the example) with the `$validator` property set to the validation instance. For example: 109 | 110 | ```js 111 | validator.validators.add(function spy (key, value, name, target) { 112 | console.log(this); // => { foo: 'bar' } 113 | console.log(key); // => 'foo' 114 | console.log(value); // => 'bar' 115 | console.log(name); // => 'James Bond' 116 | console.log(target); // => 'Mr. Goldfinger' 117 | return true; 118 | }); 119 | // Make sure fooBlerg is present and equal to "foofoofoo" 120 | validator.try({ foo: 'bar' }, { foo: ['spy: James Bond, Mr. Goldfinger'] }); 121 | ``` 122 | 123 | It should either return a boolean indicating a success status **or** return a promise that is resolved to a boolean true/false. 124 | 125 | #### Built-in Rules 126 | 127 | We build on the excellent foundation of [chriso/validator.js](https://github.com/chriso/validator.js) and define some of our own rules as well. 128 | 129 | ##### Generic 130 | 131 | * `equal(comparison)` - check if the value matches the comparison. is capable of doing a deep comparison between objects/arrays, via [assert.deepEquals](http://nodejs.org/api/assert.html#assert_assert_deepequal_actual_expected_message) 132 | * `in(values...)` - check if the value is in a array of allowed values. Warning: it's recommended to use array notation for this rule, unless all the values are strings and are not padded with spaces.. 133 | * `not(str...)` - inverts the given rule. for example: `[ 'not', 'length', 5 ]` will pass whenever the length is *not* 5 134 | * **★** `required()` - check that the value is present (not null and not undefined). **if required is not passed, no validation rules will be run for values that aren't present** 135 | * `requiredWith(key)` - ensure that the value is present if another "key" is also present. 136 | * `requiredWithout(key)` - ensure that the value is present if another "key" is not present. 137 | 138 | ##### Types 139 | * `array()` - check if the value is an array. 140 | * `boolean([value])` - Whether the value is a boolean. If a value is passed, we check that the boolean equals that value. 141 | * `date()` - check if the value is a date. 142 | * `float()` - check if the value is a float. 143 | * `int()` - check if the value is an integer. 144 | * `numeric()` - check if the value contains only numbers. 145 | * `string()` - check that the value is a string. 146 | 147 | ##### Dates 148 | 149 | * `after([date])` - check if the value is a date that's after the specified date (defaults to now). 150 | * `before([date])` - check if the value is a date that's before the specified date. 151 | 152 | ##### Arrays 153 | 154 | * `longer(min)` - check if the value's length is greater than an amount. 155 | * `shorter(max)` - check if the value's length is less than an amount. 156 | 157 | ##### Strings 158 | 159 | * `alpha()` - check if the value contains only letters (a-zA-Z). 160 | * `alphanumeric()` - check if the value contains only letters and numbers. 161 | * `ascii()` - check if the value contains ASCII chars only. 162 | * `base64()` - check if a string is base64 encoded. 163 | * `between(min, max)` - check if the value is a string with length within a range, exclusive. Note: this function takes into account surrogate pairs. 164 | * `byteLength(min [, max])` - check if the value's length (in bytes) falls in a range. 165 | * `camelCase()` - check if the value is camelCase (alphanumeric, first letter is lowercase). 166 | * `contains(seed)` - check if the value contains the seed. 167 | * `creditCard()` - check if the value is a credit card. 168 | * `email()` - check if the value is an email. 169 | * `FQDN([options])` - check if the value is a fully qualified domain name (e.g. domain.com). options is an object which defaults to { require_tld: true, allow_underscores: false }. 170 | * `hexadecimal()` - check if the value is a hexadecimal number. 171 | * `hexColor()` - check if the value is a hexadecimal color. 172 | * `IP([version])` - check if the value is an IP (version 4 or 6). 173 | * `ISBN([version])` - check if the value is an ISBN (version 10 or 13). 174 | * `JSON()` - check if the value is valid JSON (note: uses JSON.parse). 175 | * `length(amt)` - check if the value's length equals an amount. Note: this function takes into account surrogate pairs. 176 | * `longer(min)` - check if the value's length is greater than an amount. Note: this function takes into account surrogate pairs. 177 | * `lowercase()` - check if the value is lowercase. 178 | * `matches(pattern)` - check if string matches the pattern. 179 | * `mongoId()` - check if the value is a valid hex-encoded representation of a MongoDB ObjectId. 180 | * `shorter(max)` - check if the value's length is less than an amount. Note: this function takes into account surrogate pairs. 181 | * `snakeCase()` - check if the value is snake_case (alphanumeric with underscores, all lower case). 182 | * `studlyCase()` - check if the value is StudlyCase (alphanumeric, first letter uppercase). 183 | * `uppercase()` - check if the value is uppercase. 184 | * `URL([options])` - check if the value is an URL. options is an object which defaults to `{ protocols: ['http','https','ftp'], require_tld: true, require_protocol: false, allow_underscores: false, host_whitelist: false, host_blacklist: false }`. 185 | * `UUID([version])` - check if the value is a UUID (version 3, 4 or 5). 186 | * `variableWidth()` - check if the value contains a mixture of full and half-width chars. 187 | 188 | ##### Numbers 189 | 190 | * `divisibleBy(number)` - check if the value is a number that's divisible by another. 191 | * `greaterThan(min)` - check if the value is a number and is greater than an amount 192 | * `lessThan(max)` - check if the value is a number and is less than an amount. 193 | * `within(min, max)` - check if the value is a number within a range, inclusive. 194 | 195 | #### Language 196 | 197 | Error messages are generated from language files. Currently we only have the following sets: `en` (default), `en-pr`, `de`. You can load one of these sets or an entirely new set: 198 | 199 | ```js 200 | // Load a built-in set: 201 | validator.language.builtIn('en-pr'); 202 | // Or your own from a json file 203 | validator.language.set(__dirname + '/klingon.json'); 204 | // alternate, pass in an object directly: 205 | validator.language.set({ 'required': 'Y U NO GIVE US <%= key %>' }); 206 | ``` 207 | 208 | Or you can extend and overwrite it -- especially helpful when making custom rules. 209 | 210 | ```js 211 | // Update a single entry 212 | validator.language.extend('isFoo', '<%= key %>, which was <%= value %>, did not include <%= args[0] %> foos!'); 213 | // Or multiple at once 214 | validator.language.extend({ 215 | 'isFoo': '<%= key %>, which was <%= value %>, did not include <%= args[0] %> foos!', 216 | 'isBar': 'Need moar bar.' 217 | ); 218 | ``` 219 | 220 | > Note: care should be taken to use the escaped value syntax (`<%= something %>`) to prevent potential XSS. 221 | 222 | The markup for languages, as you can see, is fairly simple, using [Lodash's template functionality](https://lodash.com/docs#template). 223 | 224 | Keys for language entries should match up with the corresponding rule, or be a `$missing` catch-all language. We make the following variables available in the template: 225 | 226 | * `key` - Key of the value that has failed. 227 | * `value` - The value that failed. 228 | * `args` - Array of the arguments passed to the validator that failed. 229 | * Any global variables (see below) 230 | 231 | You can define "global" variables to be made accessible in these templates: 232 | 233 | ```js 234 | validator.language.global('meaningOfLife', 42); 235 | ``` 236 | 237 | #### Running Validators Manually 238 | 239 | You can manually run validators on an object as well. They will (regardless of the underlying function) return a promise that is resolved to boolean true or false. Note that this accepts only the verbose array notation, not the string notation. 240 | 241 | ```js 242 | validator.validators.run( 243 | { foo: 'foofoofoo' }, 244 | 'foo', ['isFoo', 3] 245 | ); 246 | ``` 247 | -------------------------------------------------------------------------------- /spec/E2ESpec.js: -------------------------------------------------------------------------------- 1 | 2 | // Some end-to-end tests just to make sure it all fits together :) 3 | describe('everythang', function () { 4 | var validator, rules; 5 | beforeEach(function () { 6 | 7 | validator = require('../index')(); 8 | rules = { 9 | username: ['required', 'between: 4, 30', 'alphanumeric'], 10 | password: ['required', 'longer: 5'], 11 | acceptTOS: ['required', 'boolean: true'] 12 | }; 13 | }); 14 | 15 | describe('tryOrFail', function () { 16 | it('passes', function (done) { 17 | validator.tryOrFail({ 18 | username: 'brendanashworth', 19 | password: 'secret', 20 | acceptTOS: true 21 | }, rules).then(function(result) { 22 | expect(result.name).toBe(undefined); 23 | 24 | expect(result.passed).toBe(true); 25 | expect(result.failed).toBe(false); 26 | expect(result.errors).toEqual({}); 27 | 28 | done(); 29 | }).catch(function(err) { 30 | this.fail(); 31 | 32 | done(); 33 | }); 34 | }); 35 | 36 | it('fails', function (done) { 37 | validator.tryOrFail({ 38 | username: 'brendanashworth', 39 | acceptTOS: false 40 | }, rules).then(function(result) { 41 | this.fail(); 42 | 43 | done(); 44 | }).catch(function(err) { 45 | expect(err.name).toEqual('ValidationError'); 46 | 47 | expect(err.passed).toBe(false); 48 | expect(err.failed).toBe(true); 49 | expect(err.errors).toEqual({ 50 | password: [ 'The password is required.' ], 51 | acceptTOS: [ 'The acceptTOS must be a boolean.' ] 52 | }); 53 | 54 | done(); 55 | }); 56 | }) 57 | }); 58 | 59 | it('passses', function (done) { 60 | validator.try({ 61 | username: 'connor4312', 62 | password: 'secret', 63 | acceptTOS: true 64 | }, rules).then(function (result) { 65 | expect(result.passed).toBe(true); 66 | expect(result.failed).toBe(false); 67 | expect(result.errors).toEqual({}); 68 | done(); 69 | }); 70 | }); 71 | 72 | it('fails', function (done) { 73 | validator.try({ 74 | username: 'connor4312', 75 | acceptTOS: false 76 | }, rules).then(function (result) { 77 | expect(result.passed).toBe(false); 78 | expect(result.failed).toBe(true); 79 | expect(result.errors).toEqual({ 80 | password: [ 'The password is required.' ], 81 | acceptTOS: [ 'The acceptTOS must be a boolean.' ] 82 | }); 83 | 84 | done(); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /spec/LanguageSpec.js: -------------------------------------------------------------------------------- 1 | var Language = require('../lib/language'); 2 | 3 | describe('language', function () { 4 | var language; 5 | beforeEach(function () { 6 | language = new Language(); 7 | }); 8 | 9 | it('sets the dictionary from object', function () { 10 | expect(language.dictionary).toEqual({}); 11 | language.set({ 'foo': 'bar' }); 12 | expect(language.dictionary).toEqual({ foo: 'bar' }); 13 | }); 14 | 15 | it('sets the dictionary from json', function () { 16 | expect(language.dictionary).toEqual({}); 17 | language.set(__dirname + '/fixture/dict.json'); 18 | expect(language.dictionary).toEqual({ foo: 'bar' }); 19 | }); 20 | 21 | it('extends the dictionary', function () { 22 | expect(language.dictionary).toEqual({}); 23 | language.extend('bar', 'baz'); 24 | language.extend({ fizz: 'buzz' }); 25 | expect(language.dictionary).toEqual({ bar: 'baz', fizz: 'buzz' }); 26 | }); 27 | 28 | it('resolves dictionary items', function () { 29 | language.set({ 'greet': 'Hello <%= who %>' }); 30 | expect(language.resolve('greet', { who: 'World' })).toBe('Hello World'); 31 | }); 32 | 33 | it('adds global variables', function () { 34 | language.set({ 'greet': 'Hello <%= who %>' }); 35 | language.global('who', 'World'); 36 | expect(language.resolve('greet')).toBe('Hello World'); 37 | }); 38 | 39 | it('loads built-in sets', function () { 40 | language.builtIn('en-pr'); 41 | expect(language.dictionary.array).toBe('Arr! The <%= key %> ain\'t an array.'); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /spec/ManagerSpec.js: -------------------------------------------------------------------------------- 1 | var Bluebird = require('bluebird'); 2 | var Manager = require('../lib/manager'); 3 | var errors = require('../lib/errors'); 4 | 5 | describe('validations manager', function () { 6 | var manager; 7 | beforeEach(function () { 8 | manager = new Manager(); 9 | }); 10 | 11 | describe('adding', function () { 12 | it('named function', function () { 13 | var rule = function myRule () {}; 14 | manager.add(rule); 15 | expect(manager.rules.myRule).toBe(rule); 16 | }); 17 | 18 | it('fails when adding anon functions / native code', function () { 19 | // Fails because functon has no name 20 | expect(function () { 21 | manager.add(function () {}); 22 | }).toThrow(new errors.RuleError()); 23 | 24 | // Failes because .bind creates an anon native code fn 25 | expect(function () { 26 | manager.add((function rule () {}).bind(null)); 27 | }).toThrow(new errors.RuleError()); 28 | }); 29 | 30 | it('an explicitly named function', function () { 31 | var rule = function () {}; 32 | manager.add('myRule', rule); 33 | expect(manager.rules.myRule).toBe(rule); 34 | }); 35 | 36 | it('loads a directory', function () { 37 | manager.loadDir(__dirname + '/fixture/rules'); 38 | expect(manager.rules.a()).toBe('a'); 39 | expect(manager.rules.b()).toBe('b'); 40 | expect(manager.rules.c()).toBe('c'); 41 | }); 42 | }); 43 | 44 | describe('runs', function () { 45 | it('should reject unknown rules', function (done) { 46 | var errored = false; 47 | manager.run({}, '', ['someSillyRule']) 48 | .catch(errors.RuleError, function () { 49 | errored = true; 50 | }) 51 | .finally(function () { 52 | expect(errored).toBe(true); 53 | done(); 54 | }); 55 | }); 56 | 57 | it('should pass values into validator', function (done) { 58 | var obj = { foo: 'bar' }; 59 | var called = false; 60 | manager.add(function funValidator (key, value, arg1, arg2) { 61 | expect(key).toBe('foo'); 62 | expect(value).toBe('bar'); 63 | expect(arg1).toBe('grr'); 64 | expect(arg2).toBe('blerg'); 65 | expect(this).toBe(obj); 66 | called = true; 67 | return true; 68 | }); 69 | manager.run(obj, 'foo', ['funValidator', 'grr', 'blerg']) 70 | .then(function () { 71 | expect(called).toBe(true); 72 | done(); 73 | }); 74 | }); 75 | 76 | it('should handle synchronous validators', function (done) { 77 | var obj = { foo: 'bar' }; 78 | var called = false; 79 | manager.add(function funValidator (key, value, arg) { 80 | called = true; 81 | return true; 82 | }); 83 | manager.run(obj, 'foo', ['funValidator']) 84 | .then(function (result) { 85 | expect(result).toBe(true); 86 | expect(called).toBe(true); 87 | done(); 88 | }); 89 | }); 90 | 91 | it('should handle asynchronous validators', function (done) { 92 | var obj = { foo: 'bar' }; 93 | var called = false; 94 | manager.add(function funValidator (key, value, arg) { 95 | called = true; 96 | return Bluebird.resolve(true); 97 | }); 98 | manager.run(obj, 'foo', ['funValidator']) 99 | .then(function (result) { 100 | expect(result).toBe(true); 101 | expect(called).toBe(true); 102 | done(); 103 | }); 104 | }); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /spec/ResponseSpec.js: -------------------------------------------------------------------------------- 1 | var Response = require('../lib/response'); 2 | 3 | describe('response', function () { 4 | var response, resolve; 5 | beforeEach(function () { 6 | resolve = jasmine.createSpy('language resolve'); 7 | response = new Response({ resolve: resolve }, { 'foo': 'bar' }, { 'foo': [['rule', 1, 2]] }); 8 | }); 9 | 10 | it('exposes base data', function () { 11 | expect(response.passed).toBe(true); 12 | expect(response.failed).toBe(false); 13 | expect(response.errors).toEqual({}); 14 | }); 15 | 16 | it('behaves after error has been added and displays message', function () { 17 | resolve.and.returnValue('message'); 18 | response.addError('foo', 0); 19 | expect(response.errors).toEqual({ 'foo': ['message']}); 20 | expect(response.failed).toBe(true); 21 | expect(response.passed).toBe(false); 22 | expect(resolve).toHaveBeenCalledWith('rule', { 23 | key: 'foo', 24 | value: 'bar', 25 | args: [1, 2] 26 | }); 27 | 28 | response.addError('foo', 0); 29 | expect(response.errors).toEqual({ 'foo': ['message', 'message']}); 30 | }); 31 | 32 | it('should pass through lodash', function () { 33 | response.addError('foo', 0); 34 | expect(response.keys()).toEqual(['foo']); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /spec/RulesSpec.js: -------------------------------------------------------------------------------- 1 | var Bluebird = require('bluebird'); 2 | var Manager = require('../lib/manager'); 3 | var errors = require('../lib/errors'); 4 | 5 | describe('validations', function () { 6 | var manager; 7 | beforeEach(function () { 8 | manager = new Manager(); 9 | }); 10 | 11 | describe('chriso\'s validator', function () { 12 | // Not going to test every single function. If specific issues arise 13 | // we'll test individual affected functions, but really I just want 14 | // to make sure that binding workds. 15 | it('basically works: no args, passing', function (done) { 16 | manager.run({ 'foo': 'asdf' }, 'foo', ['alpha']) 17 | .then(function (result) { 18 | expect(result).toBe(true); 19 | done(); 20 | }); 21 | }); 22 | it('basically works: no args, failing', function (done) { 23 | manager.run({ 'foo': 'asd123f' }, 'foo', ['alpha']) 24 | .then(function (result) { 25 | expect(result).toBe(false); 26 | done(); 27 | }); 28 | }); 29 | it('basically works: args, passing', function (done) { 30 | manager.run({ 'foo': '127.0.0.1' }, 'foo', ['IP', 4]) 31 | .then(function (result) { 32 | expect(result).toBe(true); 33 | done(); 34 | }); 35 | }); 36 | it('basically works: args, failing', function (done) { 37 | manager.run({ 'foo': '127.0.0.1' }, 'foo', ['IP', 6]) 38 | .then(function (result) { 39 | expect(result).toBe(false); 40 | done(); 41 | }); 42 | }); 43 | }); 44 | 45 | describe('between', function () { 46 | var fn = require('../lib/rules/between'); 47 | 48 | it('rejects lower/upper bound', function () { 49 | expect(fn(null, 'abc', 3, 6)).toBe(false); 50 | expect(fn(null, 'abcefg', 3, 6)).toBe(false); 51 | }); 52 | it('accepts within range', function () { 53 | expect(fn(null, 'abcd', 3, 6)).toBe(true); 54 | }); 55 | }); 56 | 57 | describe('boolean', function () { 58 | var fn = require('../lib/rules/boolean'); 59 | 60 | it('checks the type', function () { 61 | expect(fn(null, 'hi')).toBe(false); 62 | expect(fn(null, 0)).toBe(false); 63 | expect(fn(null, true)).toBe(true); 64 | expect(fn(null, false)).toBe(true); 65 | }); 66 | it('expects correctly', function () { 67 | expect(fn(null, true, true)).toBe(true); 68 | expect(fn(null, true, false)).toBe(false); 69 | expect(fn(null, false, true)).toBe(false); 70 | expect(fn(null, false, false)).toBe(true); 71 | }); 72 | }); 73 | 74 | describe('array', function () { 75 | var fn = require('../lib/rules/array'); 76 | 77 | it('checks the type', function () { 78 | expect(fn(null, 'hi')).toBe(false); 79 | expect(fn(null, 0)).toBe(false); 80 | expect(fn(null, [])).toBe(true); 81 | expect(fn(null, [42])).toBe(true); 82 | }); 83 | }); 84 | 85 | describe('shorter', function () { 86 | var fn = require('../lib/rules/shorter'); 87 | 88 | it('checks', function () { 89 | expect(fn(null, 'hi', 3)).toBe(true); 90 | expect(fn(null, 'hello', 3)).toBe(false); 91 | expect(fn(null, [], 0)).toBe(false); 92 | expect(fn(null, {}, 0)).toBe(false); 93 | }); 94 | }); 95 | 96 | describe('longer', function () { 97 | var fn = require('../lib/rules/longer'); 98 | 99 | it('checks', function () { 100 | expect(fn(null, 'hi', 3)).toBe(false); 101 | expect(fn(null, 'hello', 3)).toBe(true); 102 | expect(fn(null, [], 0)).toBe(false); 103 | expect(fn(null, {}, 0)).toBe(false); 104 | }); 105 | }); 106 | 107 | describe('greaterThan', function () { 108 | var fn = require('../lib/rules/greaterThan'); 109 | 110 | it('checks', function () { 111 | expect(fn(null, 'hi', 3)).toBe(false); 112 | expect(fn(null, {}, 3)).toBe(false); 113 | expect(fn(null, 3, 3)).toBe(false); 114 | expect(fn(null, 4, 3)).toBe(true); 115 | expect(fn(null, 40, 4)).toBe(true); 116 | }); 117 | }); 118 | 119 | describe('lessThan', function () { 120 | var fn = require('../lib/rules/lessThan'); 121 | 122 | it('checks', function () { 123 | expect(fn(null, 'hi', 3)).toBe(false); 124 | expect(fn(null, {}, 3)).toBe(false); 125 | expect(fn(null, 3, 3)).toBe(false); 126 | expect(fn(null, 1, 3)).toBe(true); 127 | expect(fn(null, -40, 4)).toBe(true); 128 | }); 129 | }); 130 | 131 | describe('within', function () { 132 | var fn = require('../lib/rules/within'); 133 | 134 | it('checks', function () { 135 | expect(fn(null, 'hi', 3, 6)).toBe(false); 136 | expect(fn(null, {}, 3, 6)).toBe(false); 137 | expect(fn(null, 2, 3, 6)).toBe(false); 138 | expect(fn(null, 5, 3, 6)).toBe(true); 139 | expect(fn(null, 9, 4)).toBe(false); 140 | }); 141 | }); 142 | 143 | describe('equal', function () { 144 | var fn = require('../lib/rules/equal'); 145 | 146 | it('checks strings', function () { 147 | expect(fn(null, 'hi', 'hi')).toBe(true); 148 | expect(fn(null, 'hi', 'hiii')).toBe(false); 149 | }); 150 | 151 | it('checks numbers', function () { 152 | expect(fn(null, 3, 3)).toBe(true); 153 | expect(fn(null, 3, '3')).toBe(true); 154 | expect(fn(null, 3, 6)).toBe(false); 155 | }); 156 | 157 | it('checks arrays', function () { 158 | expect(fn(null, [], [42, ['a']])).toBe(false); 159 | expect(fn(null, [42, ['b']], [42, ['a']])).toBe(false); 160 | expect(fn(null, [42, ['a']], [42, ['a']])).toBe(true); 161 | expect(fn(null, [42, ['a']], [])).toBe(false); 162 | 163 | }); 164 | 165 | it('checks objects', function () { 166 | expect(fn(null, {}, { a: 42, b: ['a']})).toBe(false); 167 | expect(fn(null, { a: 42, b: ['b']}, { a: 42, b: ['a']})).toBe(false); 168 | expect(fn(null, { a: 42, b: ['a']}, { a: 42, b: ['a']})).toBe(true); 169 | expect(fn(null, { a: 42, b: ['a']}, {})).toBe(false); 170 | }); 171 | }); 172 | 173 | describe('in', function () { 174 | var fn = require('../lib/rules/in'); 175 | 176 | it('checks', function () { 177 | expect(fn(null, 'a', 'b', 'c', 'd')).toBe(false); 178 | expect(fn(null, 'b', 'b', 'c', 'd')).toBe(true); 179 | }); 180 | }); 181 | 182 | describe('string', function () { 183 | var fn = require('../lib/rules/string'); 184 | 185 | it('checks', function () { 186 | expect(fn(null, 'a')).toBe(true); 187 | expect(fn(null, 0)).toBe(false); 188 | expect(fn(null, [])).toBe(false); 189 | }); 190 | }); 191 | 192 | describe('cases', function () { 193 | var snake = require('../lib/rules/snakeCase'); 194 | var studly = require('../lib/rules/studlyCase'); 195 | var camel = require('../lib/rules/camelCase'); 196 | 197 | it('snakes', function () { 198 | expect(snake(null, 'hello')).toBe(true); 199 | expect(snake(null, 'hello_world')).toBe(true); 200 | expect(snake(null, 'hello_world!')).toBe(false); 201 | expect(snake(null, 'helloWorld')).toBe(false); 202 | expect(snake(null, 'helloWorld!')).toBe(false); 203 | expect(snake(null, 'HelloWorld')).toBe(false); 204 | expect(snake(null, 'HelloWorld!')).toBe(false); 205 | }); 206 | 207 | it('studlys', function () { 208 | expect(studly(null, 'hello')).toBe(false); 209 | expect(studly(null, 'hello_world')).toBe(false); 210 | expect(studly(null, 'hello_world!')).toBe(false); 211 | expect(studly(null, 'helloWorld')).toBe(false); 212 | expect(studly(null, 'helloWorld!')).toBe(false); 213 | expect(studly(null, 'HelloWorld')).toBe(true); 214 | expect(studly(null, 'HelloWorld!')).toBe(false); 215 | }); 216 | 217 | it('camels', function () { 218 | expect(camel(null, 'hello')).toBe(true); 219 | expect(camel(null, 'hello_world')).toBe(false); 220 | expect(camel(null, 'hello_world!')).toBe(false); 221 | expect(camel(null, 'helloWorld')).toBe(true); 222 | expect(camel(null, 'helloWorld!')).toBe(false); 223 | expect(camel(null, 'HelloWorld')).toBe(false); 224 | expect(camel(null, 'HelloWorld!')).toBe(false); 225 | }); 226 | }); 227 | }); 228 | -------------------------------------------------------------------------------- /spec/ValidatorSpec.js: -------------------------------------------------------------------------------- 1 | var Validator = require('../lib/index'); 2 | var Bluebird = require('bluebird'); 3 | 4 | describe('the main validator', function () { 5 | var validator; 6 | beforeEach(function () { 7 | validator = new Validator(); 8 | }); 9 | 10 | it('should fulfill rules', function () { 11 | var rules = { a: ['foo: 1, 2, a: b, true'], b: ['required', ['foo', 1, '2']]}; 12 | validator.fulfillRules(rules); 13 | expect(rules).toEqual({ a: [['foo', 1, 2, 'a: b', true]], b: [['required'], ['foo', 1, '2']]}); 14 | }); 15 | 16 | it('should not modify the rules object', function (done) { 17 | var data = { foo: 'bar' }; 18 | var rules = { foo: ['required']}; 19 | spyOn(validator.validators, 'run').and.returnValue(Bluebird.resolve(true)); 20 | 21 | validator.try(data, rules).then(function (result) { 22 | expect(rules).toEqual({ foo: ['required']}); 23 | done(); 24 | }); 25 | }); 26 | 27 | describe('extractRequired', function () { 28 | 29 | it('should work with required', function () { 30 | var data = { a: 1, b: 2, c: null, d: undefined }; 31 | var rules = { b: [['required']], c: [['required']], d: [] }; 32 | var response = { addError: jasmine.createSpy('addError') }; 33 | validator.extractRequired(response, data, rules); 34 | 35 | expect(rules).toEqual({ b: []}); 36 | expect(response.addError).toHaveBeenCalledWith('c', 0); 37 | }); 38 | 39 | it('should work with requiredWith', function () { 40 | var data = { a: 1, b: null, c: undefined }; 41 | var rules = { b: [['requiredWith', 'a']], c: [['requiredWith', 'b']] }; 42 | var response = { addError: jasmine.createSpy('addError') }; 43 | validator.extractRequired(response, data, rules); 44 | 45 | expect(rules).toEqual({ }); 46 | expect(response.addError).toHaveBeenCalledWith('b', 0); 47 | expect(response.addError).not.toHaveBeenCalledWith('c', 0); 48 | }); 49 | 50 | it('should work with requiredWithout', function () { 51 | var data = { a: 1, b: null, c: undefined }; 52 | var rules = { b: [['requiredWithout', 'a']], c: [['requiredWithout', 'c']] }; 53 | var response = { addError: jasmine.createSpy('addError') }; 54 | validator.extractRequired(response, data, rules); 55 | 56 | expect(rules).toEqual({ }); 57 | expect(response.addError).not.toHaveBeenCalledWith('b', 0); 58 | expect(response.addError).toHaveBeenCalledWith('c', 0); 59 | }); 60 | }); 61 | 62 | describe('try()', function () { 63 | 64 | it('should with success', function (done) { 65 | var data = { foo: 'bar' }; 66 | var rules = { foo: ['rule: arg', 'noargs']}; 67 | spyOn(validator.validators, 'run'); 68 | validator.validators.run.and.returnValue(Bluebird.resolve(true)); 69 | 70 | validator.try(data, rules).then(function (result) { 71 | expect(validator.validators.run).toHaveBeenCalledWith(data, 'foo', ['rule', 'arg']); 72 | expect(validator.validators.run).toHaveBeenCalledWith(data, 'foo', ['noargs']); 73 | expect(result.passed).toBe(true); 74 | expect(result.failed).toBe(false); 75 | expect(result.errors).toEqual({}); 76 | done(); 77 | }); 78 | }); 79 | 80 | it('should with failure', function (done) { 81 | var data = { foo: 'bar' }; 82 | var rules = { foo: ['rule']}; 83 | spyOn(validator.validators, 'run'); 84 | spyOn(validator.language, 'resolve'); 85 | validator.validators.run.and.returnValue(Bluebird.resolve(false)); 86 | validator.language.resolve.and.returnValue('An error'); 87 | 88 | validator.try(data, rules).then(function (result) { 89 | expect(validator.validators.run).toHaveBeenCalledWith(data, 'foo', ['rule']); 90 | expect(validator.language.resolve).toHaveBeenCalledWith('rule', { 91 | key: 'foo', 92 | value: 'bar', 93 | args: [] 94 | }); 95 | 96 | expect(result.passed).toBe(false); 97 | expect(result.failed).toBe(true); 98 | expect(result.errors).toEqual({ foo: ['An error'] }); 99 | done(); 100 | }); 101 | }); 102 | 103 | it('should not for undefined values when not required', function (done) { 104 | var data = {}; 105 | var rules = { foo: ['rule']}; 106 | spyOn(validator.validators, 'run'); 107 | 108 | validator.try(data, rules).then(function (result) { 109 | expect(validator.validators.run).not.toHaveBeenCalled(); 110 | expect(result.passed).toBe(true); 111 | done(); 112 | }); 113 | }); 114 | 115 | it('should not for null values when not required', function (done) { 116 | var data = { foo: null }; 117 | var rules = { foo: ['rule']}; 118 | spyOn(validator.validators, 'run'); 119 | 120 | validator.try(data, rules).then(function (result) { 121 | expect(validator.validators.run).not.toHaveBeenCalled(); 122 | expect(result.passed).toBe(true); 123 | done(); 124 | }); 125 | }); 126 | 127 | it('should fail if not present value required', function (done) { 128 | var data = { foo: null }; 129 | var rules = { foo: ['required', 'rule']}; 130 | spyOn(validator.validators, 'run'); 131 | 132 | validator.try(data, rules).then(function (result) { 133 | expect(validator.validators.run).not.toHaveBeenCalled(); 134 | expect(result.passed).toBe(false); 135 | done(); 136 | }); 137 | }); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /spec/fixture/dict.json: -------------------------------------------------------------------------------- 1 | { "foo": "bar" } 2 | -------------------------------------------------------------------------------- /spec/fixture/rules/a.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { return 'a'; }; 2 | -------------------------------------------------------------------------------- /spec/fixture/rules/multi.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | b: function () { return 'b'; }, 3 | c: function () { return 'c'; } 4 | } 5 | -------------------------------------------------------------------------------- /spec/fixture/rules/someOtherFile.txt: -------------------------------------------------------------------------------- 1 | :) 2 | --------------------------------------------------------------------------------