Skip to content

Commit

Permalink
Conditional validations
Browse files Browse the repository at this point in the history
  • Loading branch information
1602 committed Oct 11, 2011
1 parent 9de9e59 commit 8a05e1f
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 72 deletions.
165 changes: 96 additions & 69 deletions lib/validatable.js
Expand Up @@ -11,6 +11,65 @@ Validatable.validatesInclusionOf = getConfigurator('inclusion');
Validatable.validatesExclusionOf = getConfigurator('exclusion'); Validatable.validatesExclusionOf = getConfigurator('exclusion');
Validatable.validatesFormatOf = getConfigurator('format'); Validatable.validatesFormatOf = getConfigurator('format');


// implementation of validators
var validators = {
presence: function (attr, conf, err) {
if (blank(this[attr])) {
err();
}
},
length: function (attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;

var len = this[attr].length;
if (conf.min && len < conf.min) {
err('min');
}
if (conf.max && len > conf.max) {
err('max');
}
if (conf.is && len !== conf.is) {
err('is');
}
},
numericality: function (attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;

if (typeof this[attr] !== 'number') {
return err('number');
}
if (conf.int && this[attr] !== Math.round(this[attr])) {
return err('int');
}
},
inclusion: function (attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;

if (!~conf.in.indexOf(this[attr])) {
err()
}
},
exclusion: function (attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;

if (~conf.in.indexOf(this[attr])) {
err()
}
},
format: function (attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;

if (typeof this[attr] === 'string') {
if (!this[attr].match(conf['with'])) {
err();
}
} else {
err();
}
}
};


function getConfigurator(name) { function getConfigurator(name) {
return function () { return function () {
configure(this, name, arguments); configure(this, name, arguments);
Expand All @@ -19,39 +78,45 @@ function getConfigurator(name) {


Validatable.prototype.isValid = function () { Validatable.prototype.isValid = function () {
var valid = true, inst = this; var valid = true, inst = this;

// exit with success when no errors
if (!this.constructor._validations) { if (!this.constructor._validations) {
Object.defineProperty(this, 'errors', { cleanErrors(this);
enumerable: false,
configurable: true,
value: false
});
return valid; return valid;
} }

Object.defineProperty(this, 'errors', { Object.defineProperty(this, 'errors', {
enumerable: false, enumerable: false,
configurable: true, configurable: true,
value: new Errors value: new Errors
}); });

this.constructor._validations.forEach(function (v) { this.constructor._validations.forEach(function (v) {
if (validationFailed(inst, v)) { if (validationFailed(inst, v)) {
valid = false; valid = false;
} }
}); });
if (valid) { if (valid) cleanErrors(this);
Object.defineProperty(this, 'errors', {
enumerable: false,
configurable: true,
value: false
});
}
return valid; return valid;
}; };


function cleanErrors(inst) {
Object.defineProperty(inst, 'errors', {
enumerable: false,
configurable: true,
value: false
});
}

function validationFailed(inst, v) { function validationFailed(inst, v) {
var attr = v[0]; var attr = v[0];
var conf = v[1]; var conf = v[1];

// here we should check skip validation conditions (if, unless) // here we should check skip validation conditions (if, unless)
// that can be specified in conf // that can be specified in conf
if (skipValidation(inst, conf, 'if')) return false;
if (skipValidation(inst, conf, 'unless')) return false;

var fail = false; var fail = false;
validators[conf.validation].call(inst, attr, conf, function onerror(kind) { validators[conf.validation].call(inst, attr, conf, function onerror(kind) {
var message; var message;
Expand All @@ -78,6 +143,25 @@ function validationFailed(inst, v) {
return fail; return fail;
} }


function skipValidation(inst, conf, kind) {
var doValidate = true;
if (typeof conf[kind] === 'function') {
doValidate = conf[kind].call(inst);
if (kind === 'unless') doValidate = !doValidate;
} else if (typeof conf[kind] === 'string') {
if (inst.hasOwnProperty(conf[kind])) {
doValidate = inst[conf[kind]];
if (kind === 'unless') doValidate = !doValidate;
} else if (typeof inst[conf[kind]] === 'function') {
doValidate = inst[conf[kind]].call(inst);
if (kind === 'unless') doValidate = !doValidate;
} else {
doValidate = kind === 'if';
}
}
return !doValidate;
}

var defaultMessages = { var defaultMessages = {
presence: 'can\'t be blank', presence: 'can\'t be blank',
length: { length: {
Expand Down Expand Up @@ -115,63 +199,6 @@ function nullCheck(attr, conf, err) {
return false; return false;
} }


var validators = {
presence: function (attr, conf, err) {
if (blank(this[attr])) {
err();
}
},
length: function (attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;

var len = this[attr].length;
if (conf.min && len < conf.min) {
err('min');
}
if (conf.max && len > conf.max) {
err('max');
}
if (conf.is && len !== conf.is) {
err('is');
}
},
numericality: function (attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;

if (typeof this[attr] !== 'number') {
return err('number');
}
if (conf.int && this[attr] !== Math.round(this[attr])) {
return err('int');
}
},
inclusion: function (attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;

if (!~conf.in.indexOf(this[attr])) {
err()
}
},
exclusion: function (attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;

if (~conf.in.indexOf(this[attr])) {
err()
}
},
format: function (attr, conf, err) {
if (nullCheck.call(this, attr, conf, err)) return;

if (typeof this[attr] === 'string') {
if (!this[attr].match(conf['with'])) {
err();
}
} else {
err();
}
}
};

function blank(v) { function blank(v) {
if (typeof v === 'undefined') return true; if (typeof v === 'undefined') return true;
if (v instanceof Array && v.length === 0) return true; if (v instanceof Array && v.length === 0) return true;
Expand Down
39 changes: 36 additions & 3 deletions test/validations_test.coffee
Expand Up @@ -14,6 +14,8 @@ User = schema.define 'User',
age: Number age: Number
gender: String gender: String
domain: String domain: String
pendingPeriod: Number
createdByAdmin: Boolean


validAttributes = validAttributes =
name: 'Anatoliy' name: 'Anatoliy'
Expand All @@ -22,11 +24,12 @@ validAttributes =
age: 26 age: 26
gender: 'male' gender: 'male'
domain: '1602' domain: '1602'

createdByAdmin: false
User.validatesPresenceOf 'email', 'name' createdByScript: true



it 'should validate presence', (test) -> it 'should validate presence', (test) ->
User.validatesPresenceOf 'email', 'name'

user = new User user = new User
test.ok not user.isValid(), 'User is not valid' test.ok not user.isValid(), 'User is not valid'
test.ok user.errors.email, 'Attr email in errors' test.ok user.errors.email, 'Attr email in errors'
Expand All @@ -44,6 +47,36 @@ it 'should validate presence', (test) ->
test.ok not user.errors.name, 'Attr name valid' test.ok not user.errors.name, 'Attr name valid'
test.done() test.done()


it 'should allow to skip validations', (test) ->
User.validatesPresenceOf 'pendingPeriod', if: 'createdByAdmin'
User.validatesLengthOf 'domain', is: 2, unless: 'createdByScript'

user = new User validAttributes
test.ok user.isValid()

user.createdByAdmin = true
test.ok not user.isValid()
test.ok user.errors.pendingPeriod.length

user.pendingPeriod = 1
test.ok user.isValid()

user.createdByScript = false
test.ok not user.isValid()
test.ok user.errors.domain.length

user.domain = '12'
test.ok user.isValid()

User.validatesLengthOf 'domain', is: 3, unless: -> @domain != 'xyz'
test.ok user.isValid()

user.domain = 'xyz'
test.ok not user.isValid() # is: 3 passed, but is: 2 failed


test.done()



it 'should throw error on save if required', (test) -> it 'should throw error on save if required', (test) ->
user = new User user = new User
Expand Down

0 comments on commit 8a05e1f

Please sign in to comment.