Conditional validations

This commit is contained in:
Anatoliy Chakkaev 2011-10-11 23:52:03 +04:00
parent 9de9e590d6
commit 8a05e1ffad
2 changed files with 167 additions and 107 deletions

View File

@ -11,110 +11,7 @@ Validatable.validatesInclusionOf = getConfigurator('inclusion');
Validatable.validatesExclusionOf = getConfigurator('exclusion'); Validatable.validatesExclusionOf = getConfigurator('exclusion');
Validatable.validatesFormatOf = getConfigurator('format'); Validatable.validatesFormatOf = getConfigurator('format');
function getConfigurator(name) { // implementation of validators
return function () {
configure(this, name, arguments);
};
}
Validatable.prototype.isValid = function () {
var valid = true, inst = this;
if (!this.constructor._validations) {
Object.defineProperty(this, 'errors', {
enumerable: false,
configurable: true,
value: false
});
return valid;
}
Object.defineProperty(this, 'errors', {
enumerable: false,
configurable: true,
value: new Errors
});
this.constructor._validations.forEach(function (v) {
if (validationFailed(inst, v)) {
valid = false;
}
});
if (valid) {
Object.defineProperty(this, 'errors', {
enumerable: false,
configurable: true,
value: false
});
}
return valid;
};
function validationFailed(inst, v) {
var attr = v[0];
var conf = v[1];
// here we should check skip validation conditions (if, unless)
// that can be specified in conf
var fail = false;
validators[conf.validation].call(inst, attr, conf, function onerror(kind) {
var message;
if (conf.message) {
message = conf.message;
}
if (!message && defaultMessages[conf.validation]) {
message = defaultMessages[conf.validation];
}
if (!message) {
message = 'is invalid';
}
if (kind) {
if (message[kind]) {
// get deeper
message = message[kind];
} else if (defaultMessages.common[kind]) {
message = defaultMessages.common[kind];
}
}
inst.errors.add(attr, message);
fail = true;
});
return fail;
}
var defaultMessages = {
presence: 'can\'t be blank',
length: {
min: 'too short',
max: 'too long',
is: 'length is wrong'
},
common: {
blank: 'is blank',
'null': 'is null'
},
numericality: {
'int': 'is not an integer',
'number': 'is not a number'
},
inclusion: 'is not included in the list',
exclusion: 'is reserved'
};
function nullCheck(attr, conf, err) {
var isNull = this[attr] === null || !(attr in this);
if (isNull) {
if (!conf.allowNull) {
err('null');
}
return true;
} else {
if (blank(this[attr])) {
if (!conf.allowBlank) {
err('blank');
}
return true;
}
}
return false;
}
var validators = { var validators = {
presence: function (attr, conf, err) { presence: function (attr, conf, err) {
if (blank(this[attr])) { if (blank(this[attr])) {
@ -172,6 +69,136 @@ var validators = {
} }
}; };
function getConfigurator(name) {
return function () {
configure(this, name, arguments);
};
}
Validatable.prototype.isValid = function () {
var valid = true, inst = this;
// exit with success when no errors
if (!this.constructor._validations) {
cleanErrors(this);
return valid;
}
Object.defineProperty(this, 'errors', {
enumerable: false,
configurable: true,
value: new Errors
});
this.constructor._validations.forEach(function (v) {
if (validationFailed(inst, v)) {
valid = false;
}
});
if (valid) cleanErrors(this);
return valid;
};
function cleanErrors(inst) {
Object.defineProperty(inst, 'errors', {
enumerable: false,
configurable: true,
value: false
});
}
function validationFailed(inst, v) {
var attr = v[0];
var conf = v[1];
// here we should check skip validation conditions (if, unless)
// that can be specified in conf
if (skipValidation(inst, conf, 'if')) return false;
if (skipValidation(inst, conf, 'unless')) return false;
var fail = false;
validators[conf.validation].call(inst, attr, conf, function onerror(kind) {
var message;
if (conf.message) {
message = conf.message;
}
if (!message && defaultMessages[conf.validation]) {
message = defaultMessages[conf.validation];
}
if (!message) {
message = 'is invalid';
}
if (kind) {
if (message[kind]) {
// get deeper
message = message[kind];
} else if (defaultMessages.common[kind]) {
message = defaultMessages.common[kind];
}
}
inst.errors.add(attr, message);
fail = true;
});
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 = {
presence: 'can\'t be blank',
length: {
min: 'too short',
max: 'too long',
is: 'length is wrong'
},
common: {
blank: 'is blank',
'null': 'is null'
},
numericality: {
'int': 'is not an integer',
'number': 'is not a number'
},
inclusion: 'is not included in the list',
exclusion: 'is reserved'
};
function nullCheck(attr, conf, err) {
var isNull = this[attr] === null || !(attr in this);
if (isNull) {
if (!conf.allowNull) {
err('null');
}
return true;
} else {
if (blank(this[attr])) {
if (!conf.allowBlank) {
err('blank');
}
return true;
}
}
return false;
}
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;

View File

@ -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'
@ -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'
@ -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