Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
NEW - A flexible Validation module with async support
  • Loading branch information
Arian committed Apr 24, 2011
1 parent d847456 commit 398b3d6
Show file tree
Hide file tree
Showing 3 changed files with 381 additions and 2 deletions.
195 changes: 195 additions & 0 deletions 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);
}

});

})();
181 changes: 181 additions & 0 deletions 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();
});

});

});
7 changes: 5 additions & 2 deletions Specs/Configuration.js
Expand Up @@ -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'
]
},

Expand Down Expand Up @@ -154,7 +155,9 @@ Configuration.source = {

'Utilities/Color',
'Utilities/Group',
'Utilities/Table'
'Utilities/Table',

'Validation/Validation'

]
},
Expand Down

0 comments on commit 398b3d6

Please sign in to comment.