NEW - A flexible Validation module with async support #238

Open
wants to merge 1 commit into
from
View
195 Source/Validation/Validation.js.js
@@ -0,0 +1,195 @@
+/*
+---
+
+name: Validation
+
+description: A Validation System
+
+license: MIT-style license
+
+authors:
+ - Arian Stolwijk
+
+requires:
+ - Core/Class
+ - Core/Object
+
+provides: [Validation]
+
+...
+*/
+
+(function(){
+
+var rules = {};
+
+var Validation = this.Validation = new Class({
+
+ Implements: [Options, Events],
+
+ options: {
+ stopOnFail: false
+ },
+
+ rules: [],
+ failed: [],
+
+ initialize: function(rules, options){
+ this.setOptions(options);
+ if (rules) this.addRules(Array.from(rules));
+ },
+
+ addRule: function(rule, options){
+ rule = Validation.lookupRule(rule);
+ if (rule){
+ if (options) Object.merge(rule.options, options);
+ this.rules.include(rule);
+ }
+ return this;
+ },
+
+ addRules: function(rules){
+ for (var i = 0, l = rules.length; i < l; i++) this.addRule(rules[i]);
+ return this;
+ },
+
+ validate: function(value, options){
+
+ var old = this.options;
+ options = Object.append({stopOnFail: old.stopOnFail}, options);
+
+ var rules = this.rules, rule, length = rules.length,
+ passed = [], progressed = [], failed = [],
+ self = this;
+
+ var progress = function(result){
+ if (!rule) return;
+
+ if (!Type.isObject(result)) result = {passed: result};
+ Object.append(result, {rule: rule, name: rule.name, value: value, options: rule.options});
+
+ progressed.push(result);
+ (result.passed ? passed : failed).push(result);
+ self.fireEvent('progress', [result, progressed, passed, failed, rules]);
+ if (passed.length == length){ // all rules passed
+ self.fireEvent('success', [passed]);
+ } else if (
+ (!result && options.stopOnFail) // first one failed
+ || (progressed.length == length) // all failed
+ ){
+ this.failed = failed;
+ self.fireEvent('failure', [failed]);
+ } else { // validate next rule
+ validate();
+ }
+ };
+
+ var validate = function(){
+ rule = rules[progressed.length];
+ if (rule.async) rule.rule.call(self, value, rule.options, progress);
+ else progress(rule.rule.call(self, value, rule.options));
+ };
+ validate();
+
+ return !failed.length;
+ },
+
+ getErrors: function(fn){
+ return Validation.report(this.failed, fn);
+ }
+
+}).extend({
+
+ validate: function(rules, value, success, failure, progress, options){
+ if (arguments.length == 2 && typeOf(rules) != 'array'){
+ var rule = Validation.lookupRule(rules);
+ if (!rule.async){
+ var result = rule.rule(value, rule.options);
+ return (typeOf(result) == 'object') ? result.passed : result;
+ }
+ }
+ options = Object.merge({}, options || {}, {
+ onSuccess: success,
+ onFailure: failure,
+ onProgress: progress
+ });
+ return (new Validation(rules, options)).validate(value);
+ },
+
+ defineRule: function(name, rule, options){
+ rules[name] = Object.merge({
+ name: name,
+ rule: rule,
+ options: {}
+ }, options);
+ return this;
+ },
+
+ defineRegExpRule: function(name, regexp, options){
+ return Validation.defineRule(name, function(value){
+ return regexp.test(value);
+ }, options);
+ },
+
+ defineAsyncRule: function(name, rule, options){
+ options = options || {};
+ options.async = true;
+ return Validation.defineRule(name, rule, options);
+ },
+
+ lookupRule: function(rule){
+ var type = typeOf(rule);
+ if (type != 'object'){
+ switch (typeOf(rule)){
+ case 'string': return rules[rule];
+ case 'function': return {rule: rule};
+ }
+ return null;
+ }
+ return (rule.name && !rule.rule)
+ ? Object.merge({}, rules[rule.name], rule) : rule;
+ },
+
+ report: function(failed, fn){
+ return (fn ? failed.map(fn) : failed);
+ }
+
+});
+
+// Overload
+Validation.extend({
+ defineRules: Validation.defineRule.overloadSetter(true),
+ defineRegExpRules: Validation.defineRegExpRule.overloadSetter(true),
+ defineAsyncRules: Validation.defineAsyncRule.overloadSetter(true)
+});
+
+// Defining some default rules
+Validation.defineRules({
+
+ empty: function(value){
+ return (value == null || value == '');
+ },
+
+ required: function(value){
+ return (value != null && value != '');
+ },
+
+ equals: function(value, options){
+ return (value == options.equals);
+ },
+
+ between: function(value, options){
+ return (value > options.min && value < options.max);
+ },
+
+ minLength: function(value, options){
+ return (value.length >= options.minLength);
+ },
+
+ maxLength: function(value, options){
+ return (value.length <= options.maxLength);
+ }
+
+});
+
+})();
View
181 Specs/1.3/Validation/Validation.js.js
@@ -0,0 +1,181 @@
+/*
+Script: Validation.js
+ Specs for Validation.js
+
+License:
+ MIT-style license.
+*/
+
+describe('Validation', function(){
+
+ it('should do a simple validation', function(){
+ var validation = new Validation('required');
+ expect(validation.validate('')).toBeFalsy();
+ expect(validation.validate('Moo!!')).toBeTruthy();
+ });
+
+ it('should add a rule', function(){
+ var validation = new Validation();
+ validation.addRule('empty');
+ expect(validation.rules.length).toEqual(1);
+ });
+
+ it('should define, add and validate a rule with options', function(){
+ var spy = jasmine.createSpy('MyTestingRuleWithOptions');
+ Validation.defineRule('MyTestingRuleWithOptions', spy);
+
+ var value = 'MyTestValue', options = {first: 1, second: 2, third: 3};
+ var validation = new Validation({
+ name: 'MyTestingRuleWithOptions',
+ options: options
+ });
+ validation.validate(value);
+
+ expect(spy).toHaveBeenCalledWith(value, options);
+ });
+
+ it('should fire the failure event with the failed rules', function(){
+ Validation.defineRules({
+ MyErrorNamesTest1: Function.from(false),
+ MyErrorNamesTest2: Function.from(false),
+ MyErrorNamesTest3: Function.from(false),
+ MyErrorNamesTest4: Function.from(false)
+ });
+
+ var ruleNames = [
+ 'MyErrorNamesTest1',
+ 'MyErrorNamesTest2',
+ 'MyErrorNamesTest3',
+ 'MyErrorNamesTest4'
+ ];
+
+ var testOption = {foo: 'testOption'};
+
+ var val = new Validation(ruleNames.slice(0, 3));
+ val.addRule('MyErrorNamesTest4', testOption);
+
+ var errors = [];
+ val.addEvent('failure', function(rules){
+ errors = rules;
+ });
+
+ var res = val.validate('foo');
+
+ expect(res).toEqual(false);
+
+ errors.each(function(error, i){
+ expect(error.name).toEqual(ruleNames[i]);
+ expect(error.value).toEqual('foo');
+ });
+
+ expect(errors[3].options).toEqual(testOption);
+ });
+
+ it('should test the Validation.validate shortcut', function(){
+ expect(Validation.validate({
+ name: 'between',
+ options: {min: 3, max: 6}
+ }, 5)).toBeTruthy();
+ expect(Validation.validate('empty', '')).toBeTruthy();
+ expect(Validation.validate('empty', 'asdf')).toBeFalsy();
+ });
+
+ it('should validate when a defined rule returns a object', function(){
+ var returnedErrors = [1, 2, 3];
+ Validation.defineRule('ObjectRule', function(){
+ return {passed: false, errors: returnedErrors};
+ });
+
+ // Shortcut function
+ expect(Validation.validate('ObjectRule', 'moo')).toBeFalsy();
+
+ // using the Validation Class
+ var val = new Validation('ObjectRule');
+ expect(val.validate('moo')).toBeFalsy();
+
+ // Checking the errors
+ val.getErrors().each(function(error, i){
+ expect(error.error).toEqual(returnedErrors[i]);
+ expect(error.name).toEqual('ObjectRule');
+ expect(error.value).toEqual('moo');
+ });
+ });
+
+ describe('Async Rules', function(){
+
+ Validation.defineAsyncRule('async1', function(value, options, progress){
+ progress.delay(2, null, options.val && value == options.val);
+ });
+ Validation.defineAsyncRule('async2', function(value, options, progress){
+ progress.delay(2, null, options.res);
+ });
+
+ it('should validate the asyn rules', function(){
+
+ var success = jasmine.createSpy('success'),
+ failure = jasmine.createSpy('failure'),
+ progress = jasmine.createSpy('progress');
+
+ Validation.validate([
+ {name: 'async1', options: {val: 'foo'}},
+ {name: 'async2', options: {res: true}}
+ ], 'foo', success, failure, progress);
+
+ waits(30);
+
+ runs(function(){
+ expect(success).toHaveBeenCalled();
+ expect(failure).not.toHaveBeenCalled();
+ expect(progress.callCount).toEqual(2);
+ });
+
+ });
+
+ });
+
+ describe('Default Rules', function(){
+
+ it('should test the empty rule', function(){
+ var val = new Validation('empty');
+ expect(val.validate('')).toBeTruthy();
+ expect(val.validate('meh')).toBeFalsy();
+ });
+
+ it('should test the required rule', function(){
+ var val = new Validation('required');
+ expect(val.validate('foo')).toBeTruthy();
+ expect(val.validate('')).toBeFalsy();
+ });
+
+ it('should test the equals rule', function(){
+ var val = new Validation();
+ val.addRule('equals', {equals: 'mootools'});
+ expect(val.validate('mootools')).toBeTruthy();
+ expect(val.validate('moo')).toBeFalsy();
+ });
+
+ it('should test the between rule', function(){
+ var validation = new Validation();
+ validation.addRule('between', {min: 1, max: 5});
+ expect(validation.validate(3)).toBeTruthy();
+ expect(validation.validate(0.5)).toBeFalsy();
+ expect(validation.validate(7)).toBeFalsy();
+ });
+
+ it('should test the minLength rule', function(){
+ var validation = new Validation();
+ validation.addRule('minLength', {minLength: 4});
+ expect(validation.validate('mooing')).toBeTruthy();
+ expect(validation.validate('moo')).toBeFalsy();
+ });
+
+ it('should test the maxLength rule', function(){
+ var validation = new Validation();
+ validation.addRule('maxLength', {maxLength: 4});
+ expect(validation.validate('moo')).toBeTruthy();
+ expect(validation.validate('mooing')).toBeFalsy();
+ });
+
+ });
+
+});
View
7 Specs/Configuration.js
@@ -44,7 +44,8 @@ Configuration.sets = {
'Types/String.Extras', 'Types/String.QueryString', 'Types/Number.Format',
'Types/Hash', 'Types/Hash.Extras', 'Types/Date',
'Types/Date.Extras', 'Locale/Locale',
- 'Utilities/Color', 'Utilities/Group', 'Utilities/Table'
+ 'Utilities/Color', 'Utilities/Group', 'Utilities/Table',
+ 'Validation/Validation'
]
},
@@ -154,7 +155,9 @@ Configuration.source = {
'Utilities/Color',
'Utilities/Group',
- 'Utilities/Table'
+ 'Utilities/Table',
+
+ 'Validation/Validation'
]
},