UNPKG

15.6 kBJavaScriptView Raw
1var Rules = require('./rules');
2var Lang = require('./lang');
3var Errors = require('./errors');
4var Attributes = require('./attributes');
5var AsyncResolvers = require('./async');
6
7var Validator = function (input, rules, customMessages) {
8 var lang = Validator.getDefaultLang();
9 this.input = input || {};
10
11 this.messages = Lang._make(lang);
12 this.messages._setCustom(customMessages);
13 this.setAttributeFormatter(Validator.prototype.attributeFormatter);
14
15 this.errors = new Errors();
16 this.errorCount = 0;
17
18 this.hasAsync = false;
19 this.rules = this._parseRules(rules);
20};
21
22Validator.prototype = {
23
24 constructor: Validator,
25
26 /**
27 * Default language
28 *
29 * @type {string}
30 */
31 lang: 'en',
32
33 /**
34 * Numeric based rules
35 *
36 * @type {array}
37 */
38 numericRules: ['integer', 'numeric'],
39
40 /**
41 * Attribute formatter.
42 *
43 * @type {function}
44 */
45 attributeFormatter: Attributes.formatter,
46
47 /**
48 * Run validator
49 *
50 * @return {boolean} Whether it passes; true = passes, false = fails
51 */
52 check: function () {
53 var self = this;
54
55 for (var attribute in this.rules) {
56 var attributeRules = this.rules[attribute];
57 var inputValue = this._objectPath(this.input, attribute);
58
59 if (this._hasRule(attribute, ['sometimes']) && !this._suppliedWithData(attribute)) {
60 continue;
61 }
62
63 for (var i = 0, len = attributeRules.length, rule, ruleOptions, rulePassed; i < len; i++) {
64 ruleOptions = attributeRules[i];
65 rule = this.getRule(ruleOptions.name);
66
67 if (!this._isValidatable(rule, inputValue)) {
68 continue;
69 }
70
71 rulePassed = rule.validate(inputValue, ruleOptions.value, attribute);
72 if (!rulePassed) {
73 this._addFailure(rule);
74 }
75
76 if (this._shouldStopValidating(attribute, rulePassed)) {
77 break;
78 }
79 }
80 }
81
82 return this.errorCount === 0;
83 },
84
85 /**
86 * Run async validator
87 *
88 * @param {function} passes
89 * @param {function} fails
90 * @return {void}
91 */
92 checkAsync: function (passes, fails) {
93 var _this = this;
94 passes = passes || function () {};
95 fails = fails || function () {};
96
97 var failsOne = function (rule, message) {
98 _this._addFailure(rule, message);
99 };
100
101 var resolvedAll = function (allPassed) {
102 if (allPassed) {
103 passes();
104 } else {
105 fails();
106 }
107 };
108
109 var asyncResolvers = new AsyncResolvers(failsOne, resolvedAll);
110
111 var validateRule = function (inputValue, ruleOptions, attribute, rule) {
112 return function () {
113 var resolverIndex = asyncResolvers.add(rule);
114 rule.validate(inputValue, ruleOptions.value, attribute, function () {
115 asyncResolvers.resolve(resolverIndex);
116 });
117 };
118 };
119
120 for (var attribute in this.rules) {
121 var attributeRules = this.rules[attribute];
122 var inputValue = this._objectPath(this.input, attribute);
123
124 if (this._hasRule(attribute, ['sometimes']) && !this._suppliedWithData(attribute)) {
125 continue;
126 }
127
128 for (var i = 0, len = attributeRules.length, rule, ruleOptions; i < len; i++) {
129 ruleOptions = attributeRules[i];
130
131 rule = this.getRule(ruleOptions.name);
132
133 if (!this._isValidatable(rule, inputValue)) {
134 continue;
135 }
136
137 validateRule(inputValue, ruleOptions, attribute, rule)();
138 }
139 }
140
141 asyncResolvers.enableFiring();
142 asyncResolvers.fire();
143 },
144
145 /**
146 * Add failure and error message for given rule
147 *
148 * @param {Rule} rule
149 */
150 _addFailure: function (rule) {
151 var msg = this.messages.render(rule);
152 this.errors.add(rule.attribute, msg);
153 this.errorCount++;
154 },
155
156 /**
157 * Flatten nested object, normalizing { foo: { bar: 1 } } into: { 'foo.bar': 1 }
158 *
159 * @param {object} nested object
160 * @return {object} flattened object
161 */
162 _flattenObject: function (obj) {
163 var flattened = {};
164
165 function recurse(current, property) {
166 if (!property && Object.getOwnPropertyNames(current).length === 0) {
167 return;
168 }
169 if (Object(current) !== current || Array.isArray(current)) {
170 flattened[property] = current;
171 } else {
172 var isEmpty = true;
173 for (var p in current) {
174 isEmpty = false;
175 recurse(current[p], property ? property + "." + p : p);
176 }
177 if (isEmpty) {
178 flattened[property] = {};
179 }
180 }
181 }
182 if (obj) {
183 recurse(obj);
184 }
185 return flattened;
186 },
187
188 /**
189 * Extract value from nested object using string path with dot notation
190 *
191 * @param {object} object to search in
192 * @param {string} path inside object
193 * @return {any|void} value under the path
194 */
195 _objectPath: function (obj, path) {
196 if (Object.prototype.hasOwnProperty.call(obj, path)) {
197 return obj[path];
198 }
199
200 var keys = path.replace(/\[(\w+)\]/g, ".$1").replace(/^\./, "").split(".");
201 var copy = {};
202 for (var attr in obj) {
203 if (Object.prototype.hasOwnProperty.call(obj, attr)) {
204 copy[attr] = obj[attr];
205 }
206 }
207
208 for (var i = 0, l = keys.length; i < l; i++) {
209 if (Object.hasOwnProperty.call(copy, keys[i])) {
210 copy = copy[keys[i]];
211 } else {
212 return;
213 }
214 }
215 return copy;
216 },
217
218 /**
219 * Parse rules, normalizing format into: { attribute: [{ name: 'age', value: 3 }] }
220 *
221 * @param {object} rules
222 * @return {object}
223 */
224 _parseRules: function (rules) {
225
226 var parsedRules = {};
227 rules = this._flattenObject(rules);
228
229 for (var attribute in rules) {
230
231 var rulesArray = rules[attribute];
232
233 this._parseRulesCheck(attribute, rulesArray, parsedRules);
234 }
235 return parsedRules;
236
237
238 },
239
240 _parseRulesCheck: function (attribute, rulesArray, parsedRules, wildCardValues) {
241 if (attribute.indexOf('*') > -1) {
242 this._parsedRulesRecurse(attribute, rulesArray, parsedRules, wildCardValues);
243 } else {
244 this._parseRulesDefault(attribute, rulesArray, parsedRules, wildCardValues);
245 }
246 },
247
248 _parsedRulesRecurse: function (attribute, rulesArray, parsedRules, wildCardValues) {
249 var parentPath = attribute.substr(0, attribute.indexOf('*') - 1);
250 var propertyValue = this._objectPath(this.input, parentPath);
251
252 if (propertyValue) {
253 for (var propertyNumber = 0; propertyNumber < propertyValue.length; propertyNumber++) {
254 var workingValues = wildCardValues ? wildCardValues.slice() : [];
255 workingValues.push(propertyNumber);
256 this._parseRulesCheck(attribute.replace('*', propertyNumber), rulesArray, parsedRules, workingValues);
257 }
258 }
259 },
260
261 _parseRulesDefault: function (attribute, rulesArray, parsedRules, wildCardValues) {
262 var attributeRules = [];
263
264 if (rulesArray instanceof Array) {
265 rulesArray = this._prepareRulesArray(rulesArray);
266 }
267
268 if (typeof rulesArray === 'string') {
269 rulesArray = rulesArray.split('|');
270 }
271
272 for (var i = 0, len = rulesArray.length, rule; i < len; i++) {
273 rule = typeof rulesArray[i] === 'string' ? this._extractRuleAndRuleValue(rulesArray[i]) : rulesArray[i];
274 if (rule.value) {
275 rule.value = this._replaceWildCards(rule.value, wildCardValues);
276 this._replaceWildCardsMessages(wildCardValues);
277 }
278
279 if (Rules.isAsync(rule.name)) {
280 this.hasAsync = true;
281 }
282 attributeRules.push(rule);
283 }
284
285 parsedRules[attribute] = attributeRules;
286 },
287
288 _replaceWildCards: function (path, nums) {
289
290 if (!nums) {
291 return path;
292 }
293
294 var path2 = path;
295 nums.forEach(function (value) {
296 var pos = path2.indexOf('*');
297 if (pos === -1) {
298 return path2;
299 }
300 path2 = path2.substr(0, pos) + value + path2.substr(pos + 1);
301 });
302 return path2;
303 },
304
305 _replaceWildCardsMessages: function (nums) {
306 var customMessages = this.messages.customMessages;
307 var self = this;
308 Object.keys(customMessages).forEach(function (key) {
309 if (nums) {
310 var newKey = self._replaceWildCards(key, nums);
311 customMessages[newKey] = customMessages[key];
312 }
313 });
314
315 this.messages._setCustom(customMessages);
316 },
317 /**
318 * Prepare rules if it comes in Array. Check for objects. Need for type validation.
319 *
320 * @param {array} rulesArray
321 * @return {array}
322 */
323 _prepareRulesArray: function (rulesArray) {
324 var rules = [];
325
326 for (var i = 0, len = rulesArray.length; i < len; i++) {
327 if (typeof rulesArray[i] === 'object') {
328 for (var rule in rulesArray[i]) {
329 rules.push({
330 name: rule,
331 value: rulesArray[i][rule]
332 });
333 }
334 } else {
335 rules.push(rulesArray[i]);
336 }
337 }
338
339 return rules;
340 },
341
342 /**
343 * Determines if the attribute is supplied with the original data object.
344 *
345 * @param {array} attribute
346 * @return {boolean}
347 */
348 _suppliedWithData: function (attribute) {
349 return this.input.hasOwnProperty(attribute);
350 },
351
352 /**
353 * Extract a rule and a value from a ruleString (i.e. min:3), rule = min, value = 3
354 *
355 * @param {string} ruleString min:3
356 * @return {object} object containing the name of the rule and value
357 */
358 _extractRuleAndRuleValue: function (ruleString) {
359 var rule = {},
360 ruleArray;
361
362 rule.name = ruleString;
363
364 if (ruleString.indexOf(':') >= 0) {
365 ruleArray = ruleString.split(':');
366 rule.name = ruleArray[0];
367 rule.value = ruleArray.slice(1).join(":");
368 }
369
370 return rule;
371 },
372
373 /**
374 * Determine if attribute has any of the given rules
375 *
376 * @param {string} attribute
377 * @param {array} findRules
378 * @return {boolean}
379 */
380 _hasRule: function (attribute, findRules) {
381 var rules = this.rules[attribute] || [];
382 for (var i = 0, len = rules.length; i < len; i++) {
383 if (findRules.indexOf(rules[i].name) > -1) {
384 return true;
385 }
386 }
387 return false;
388 },
389
390 /**
391 * Determine if attribute has any numeric-based rules.
392 *
393 * @param {string} attribute
394 * @return {Boolean}
395 */
396 _hasNumericRule: function (attribute) {
397 return this._hasRule(attribute, this.numericRules);
398 },
399
400 /**
401 * Determine if rule is validatable
402 *
403 * @param {Rule} rule
404 * @param {mixed} value
405 * @return {boolean}
406 */
407 _isValidatable: function (rule, value) {
408 if (Rules.isImplicit(rule.name)) {
409 return true;
410 }
411
412 return this.getRule('required').validate(value);
413 },
414
415 /**
416 * Determine if we should stop validating.
417 *
418 * @param {string} attribute
419 * @param {boolean} rulePassed
420 * @return {boolean}
421 */
422 _shouldStopValidating: function (attribute, rulePassed) {
423
424 var stopOnAttributes = this.stopOnAttributes;
425 if (typeof stopOnAttributes === 'undefined' || stopOnAttributes === false || rulePassed === true) {
426 return false;
427 }
428
429 if (stopOnAttributes instanceof Array) {
430 return stopOnAttributes.indexOf(attribute) > -1;
431 }
432
433 return true;
434 },
435
436 /**
437 * Set custom attribute names.
438 *
439 * @param {object} attributes
440 * @return {void}
441 */
442 setAttributeNames: function (attributes) {
443 this.messages._setAttributeNames(attributes);
444 },
445
446 /**
447 * Set the attribute formatter.
448 *
449 * @param {fuction} func
450 * @return {void}
451 */
452 setAttributeFormatter: function (func) {
453 this.messages._setAttributeFormatter(func);
454 },
455
456 /**
457 * Get validation rule
458 *
459 * @param {string} name
460 * @return {Rule}
461 */
462 getRule: function (name) {
463 return Rules.make(name, this);
464 },
465
466 /**
467 * Stop on first error.
468 *
469 * @param {boolean|array} An array of attributes or boolean true/false for all attributes.
470 * @return {void}
471 */
472 stopOnError: function (attributes) {
473 this.stopOnAttributes = attributes;
474 },
475
476 /**
477 * Determine if validation passes
478 *
479 * @param {function} passes
480 * @return {boolean|undefined}
481 */
482 passes: function (passes) {
483 var async = this._checkAsync('passes', passes);
484 if (async) {
485 return this.checkAsync(passes);
486 }
487 return this.check();
488 },
489
490 /**
491 * Determine if validation fails
492 *
493 * @param {function} fails
494 * @return {boolean|undefined}
495 */
496 fails: function (fails) {
497 var async = this._checkAsync('fails', fails);
498 if (async) {
499 return this.checkAsync(function () {}, fails);
500 }
501 return !this.check();
502 },
503
504 /**
505 * Check if validation should be called asynchronously
506 *
507 * @param {string} funcName Name of the caller
508 * @param {function} callback
509 * @return {boolean}
510 */
511 _checkAsync: function (funcName, callback) {
512 var hasCallback = typeof callback === 'function';
513 if (this.hasAsync && !hasCallback) {
514 throw funcName + ' expects a callback when async rules are being tested.';
515 }
516
517 return this.hasAsync || hasCallback;
518 }
519
520};
521
522/**
523 * Set messages for language
524 *
525 * @param {string} lang
526 * @param {object} messages
527 * @return {this}
528 */
529Validator.setMessages = function (lang, messages) {
530 Lang._set(lang, messages);
531 return this;
532};
533
534/**
535 * Get messages for given language
536 *
537 * @param {string} lang
538 * @return {Messages}
539 */
540Validator.getMessages = function (lang) {
541 return Lang._get(lang);
542};
543
544/**
545 * Set default language to use
546 *
547 * @param {string} lang
548 * @return {void}
549 */
550Validator.useLang = function (lang) {
551 this.prototype.lang = lang;
552};
553
554/**
555 * Get default language
556 *
557 * @return {string}
558 */
559Validator.getDefaultLang = function () {
560 return this.prototype.lang;
561};
562
563/**
564 * Set the attribute formatter.
565 *
566 * @param {fuction} func
567 * @return {void}
568 */
569Validator.setAttributeFormatter = function (func) {
570 this.prototype.attributeFormatter = func;
571};
572
573/**
574 * Stop on first error.
575 *
576 * @param {boolean|array} An array of attributes or boolean true/false for all attributes.
577 * @return {void}
578 */
579Validator.stopOnError = function (attributes) {
580 this.prototype.stopOnAttributes = attributes;
581};
582
583/**
584 * Register custom validation rule
585 *
586 * @param {string} name
587 * @param {function} fn
588 * @param {string} message
589 * @return {void}
590 */
591Validator.register = function (name, fn, message) {
592 var lang = Validator.getDefaultLang();
593 Rules.register(name, fn);
594 Lang._setRuleMessage(lang, name, message);
595};
596
597/**
598 * Register custom validation rule
599 *
600 * @param {string} name
601 * @param {function} fn
602 * @param {string} message
603 * @return {void}
604 */
605Validator.registerImplicit = function (name, fn, message) {
606 var lang = Validator.getDefaultLang();
607 Rules.registerImplicit(name, fn);
608 Lang._setRuleMessage(lang, name, message);
609};
610
611/**
612 * Register asynchronous validation rule
613 *
614 * @param {string} name
615 * @param {function} fn
616 * @param {string} message
617 * @return {void}
618 */
619Validator.registerAsync = function (name, fn, message) {
620 var lang = Validator.getDefaultLang();
621 Rules.registerAsync(name, fn);
622 Lang._setRuleMessage(lang, name, message);
623};
624
625/**
626 * Register asynchronous validation rule
627 *
628 * @param {string} name
629 * @param {function} fn
630 * @param {string} message
631 * @return {void}
632 */
633Validator.registerAsyncImplicit = function (name, fn, message) {
634 var lang = Validator.getDefaultLang();
635 Rules.registerAsyncImplicit(name, fn);
636 Lang._setRuleMessage(lang, name, message);
637};
638
639/**
640 * Register validator for missed validation rule
641 *
642 * @param {string} name
643 * @param {function} fn
644 * @param {string} message
645 * @return {void}
646 */
647Validator.registerMissedRuleValidator = function(fn, message) {
648 Rules.registerMissedRuleValidator(fn, message);
649};
650
651module.exports = Validator;