UNPKG

15.7 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 if(Array.isArray(path2)){
297 path2 = path2[0];
298 }
299 pos = path2.indexOf('*');
300 if (pos === -1) {
301 return path2;
302 }
303 path2 = path2.substr(0, pos) + value + path2.substr(pos + 1);
304 });
305 if(Array.isArray(path)){
306 path[0] = path2;
307 path2 = path;
308 }
309 return path2;
310 },
311
312 _replaceWildCardsMessages: function (nums) {
313 var customMessages = this.messages.customMessages;
314 var self = this;
315 Object.keys(customMessages).forEach(function (key) {
316 if (nums) {
317 var newKey = self._replaceWildCards(key, nums);
318 customMessages[newKey] = customMessages[key];
319 }
320 });
321
322 this.messages._setCustom(customMessages);
323 },
324 /**
325 * Prepare rules if it comes in Array. Check for objects. Need for type validation.
326 *
327 * @param {array} rulesArray
328 * @return {array}
329 */
330 _prepareRulesArray: function (rulesArray) {
331 var rules = [];
332
333 for (var i = 0, len = rulesArray.length; i < len; i++) {
334 if (typeof rulesArray[i] === 'object') {
335 for (var rule in rulesArray[i]) {
336 rules.push({
337 name: rule,
338 value: rulesArray[i][rule]
339 });
340 }
341 } else {
342 rules.push(rulesArray[i]);
343 }
344 }
345
346 return rules;
347 },
348
349 /**
350 * Determines if the attribute is supplied with the original data object.
351 *
352 * @param {array} attribute
353 * @return {boolean}
354 */
355 _suppliedWithData: function (attribute) {
356 return this.input.hasOwnProperty(attribute);
357 },
358
359 /**
360 * Extract a rule and a value from a ruleString (i.e. min:3), rule = min, value = 3
361 *
362 * @param {string} ruleString min:3
363 * @return {object} object containing the name of the rule and value
364 */
365 _extractRuleAndRuleValue: function (ruleString) {
366 var rule = {},
367 ruleArray;
368
369 rule.name = ruleString;
370
371 if (ruleString.indexOf(':') >= 0) {
372 ruleArray = ruleString.split(':');
373 rule.name = ruleArray[0];
374 rule.value = ruleArray.slice(1).join(":");
375 }
376
377 return rule;
378 },
379
380 /**
381 * Determine if attribute has any of the given rules
382 *
383 * @param {string} attribute
384 * @param {array} findRules
385 * @return {boolean}
386 */
387 _hasRule: function (attribute, findRules) {
388 var rules = this.rules[attribute] || [];
389 for (var i = 0, len = rules.length; i < len; i++) {
390 if (findRules.indexOf(rules[i].name) > -1) {
391 return true;
392 }
393 }
394 return false;
395 },
396
397 /**
398 * Determine if attribute has any numeric-based rules.
399 *
400 * @param {string} attribute
401 * @return {Boolean}
402 */
403 _hasNumericRule: function (attribute) {
404 return this._hasRule(attribute, this.numericRules);
405 },
406
407 /**
408 * Determine if rule is validatable
409 *
410 * @param {Rule} rule
411 * @param {mixed} value
412 * @return {boolean}
413 */
414 _isValidatable: function (rule, value) {
415 if (Rules.isImplicit(rule.name)) {
416 return true;
417 }
418
419 return this.getRule('required').validate(value);
420 },
421
422 /**
423 * Determine if we should stop validating.
424 *
425 * @param {string} attribute
426 * @param {boolean} rulePassed
427 * @return {boolean}
428 */
429 _shouldStopValidating: function (attribute, rulePassed) {
430
431 var stopOnAttributes = this.stopOnAttributes;
432 if (typeof stopOnAttributes === 'undefined' || stopOnAttributes === false || rulePassed === true) {
433 return false;
434 }
435
436 if (stopOnAttributes instanceof Array) {
437 return stopOnAttributes.indexOf(attribute) > -1;
438 }
439
440 return true;
441 },
442
443 /**
444 * Set custom attribute names.
445 *
446 * @param {object} attributes
447 * @return {void}
448 */
449 setAttributeNames: function (attributes) {
450 this.messages._setAttributeNames(attributes);
451 },
452
453 /**
454 * Set the attribute formatter.
455 *
456 * @param {fuction} func
457 * @return {void}
458 */
459 setAttributeFormatter: function (func) {
460 this.messages._setAttributeFormatter(func);
461 },
462
463 /**
464 * Get validation rule
465 *
466 * @param {string} name
467 * @return {Rule}
468 */
469 getRule: function (name) {
470 return Rules.make(name, this);
471 },
472
473 /**
474 * Stop on first error.
475 *
476 * @param {boolean|array} An array of attributes or boolean true/false for all attributes.
477 * @return {void}
478 */
479 stopOnError: function (attributes) {
480 this.stopOnAttributes = attributes;
481 },
482
483 /**
484 * Determine if validation passes
485 *
486 * @param {function} passes
487 * @return {boolean|undefined}
488 */
489 passes: function (passes) {
490 var async = this._checkAsync('passes', passes);
491 if (async) {
492 return this.checkAsync(passes);
493 }
494 return this.check();
495 },
496
497 /**
498 * Determine if validation fails
499 *
500 * @param {function} fails
501 * @return {boolean|undefined}
502 */
503 fails: function (fails) {
504 var async = this._checkAsync('fails', fails);
505 if (async) {
506 return this.checkAsync(function () {}, fails);
507 }
508 return !this.check();
509 },
510
511 /**
512 * Check if validation should be called asynchronously
513 *
514 * @param {string} funcName Name of the caller
515 * @param {function} callback
516 * @return {boolean}
517 */
518 _checkAsync: function (funcName, callback) {
519 var hasCallback = typeof callback === 'function';
520 if (this.hasAsync && !hasCallback) {
521 throw funcName + ' expects a callback when async rules are being tested.';
522 }
523
524 return this.hasAsync || hasCallback;
525 }
526
527};
528
529/**
530 * Set messages for language
531 *
532 * @param {string} lang
533 * @param {object} messages
534 * @return {this}
535 */
536Validator.setMessages = function (lang, messages) {
537 Lang._set(lang, messages);
538 return this;
539};
540
541/**
542 * Get messages for given language
543 *
544 * @param {string} lang
545 * @return {Messages}
546 */
547Validator.getMessages = function (lang) {
548 return Lang._get(lang);
549};
550
551/**
552 * Set default language to use
553 *
554 * @param {string} lang
555 * @return {void}
556 */
557Validator.useLang = function (lang) {
558 this.prototype.lang = lang;
559};
560
561/**
562 * Get default language
563 *
564 * @return {string}
565 */
566Validator.getDefaultLang = function () {
567 return this.prototype.lang;
568};
569
570/**
571 * Set the attribute formatter.
572 *
573 * @param {fuction} func
574 * @return {void}
575 */
576Validator.setAttributeFormatter = function (func) {
577 this.prototype.attributeFormatter = func;
578};
579
580/**
581 * Stop on first error.
582 *
583 * @param {boolean|array} An array of attributes or boolean true/false for all attributes.
584 * @return {void}
585 */
586Validator.stopOnError = function (attributes) {
587 this.prototype.stopOnAttributes = attributes;
588};
589
590/**
591 * Register custom validation rule
592 *
593 * @param {string} name
594 * @param {function} fn
595 * @param {string} message
596 * @return {void}
597 */
598Validator.register = function (name, fn, message) {
599 var lang = Validator.getDefaultLang();
600 Rules.register(name, fn);
601 Lang._setRuleMessage(lang, name, message);
602};
603
604/**
605 * Register custom validation rule
606 *
607 * @param {string} name
608 * @param {function} fn
609 * @param {string} message
610 * @return {void}
611 */
612Validator.registerImplicit = function (name, fn, message) {
613 var lang = Validator.getDefaultLang();
614 Rules.registerImplicit(name, fn);
615 Lang._setRuleMessage(lang, name, message);
616};
617
618/**
619 * Register asynchronous validation rule
620 *
621 * @param {string} name
622 * @param {function} fn
623 * @param {string} message
624 * @return {void}
625 */
626Validator.registerAsync = function (name, fn, message) {
627 var lang = Validator.getDefaultLang();
628 Rules.registerAsync(name, fn);
629 Lang._setRuleMessage(lang, name, message);
630};
631
632/**
633 * Register asynchronous validation rule
634 *
635 * @param {string} name
636 * @param {function} fn
637 * @param {string} message
638 * @return {void}
639 */
640Validator.registerAsyncImplicit = function (name, fn, message) {
641 var lang = Validator.getDefaultLang();
642 Rules.registerAsyncImplicit(name, fn);
643 Lang._setRuleMessage(lang, name, message);
644};
645
646/**
647 * Register validator for missed validation rule
648 *
649 * @param {string} name
650 * @param {function} fn
651 * @param {string} message
652 * @return {void}
653 */
654Validator.registerMissedRuleValidator = function(fn, message) {
655 Rules.registerMissedRuleValidator(fn, message);
656};
657
658module.exports = Validator;