diff --git a/lib/validations.js b/lib/validations.js index 0b9388e7..bcf6a805 100644 --- a/lib/validations.js +++ b/lib/validations.js @@ -46,6 +46,20 @@ function Validatable() { */ Validatable.validatesPresenceOf = getConfigurator('presence'); +/** + * Validate absence of one or more specified properties. + * A model should not include a property to be considered valid; fails when validated field not blank. + * + * For example, validate absence of reserved + * ``` + * Post.validatesAbsenceOf('reserved', { unless: 'special' }); + * + * @param {String} propertyName One or more property names. + * @options {Object} errMsg Optional custom error message. Default is "can't be set" + * @property {String} message Error message to use instead of default. + */ +Validatable.validatesAbsenceOf = getConfigurator('absence'); + /** * Validate length. Require a property length to be within a specified range. * Three kinds of validations: min, max, is. @@ -225,6 +239,15 @@ function validatePresence(attr, conf, err) { } } +/*! + * Absence validator + */ +function validateAbsence(attr, conf, err) { + if (!blank(this[attr])) { + err(); + } +} + /*! * Length validator */ @@ -332,6 +355,7 @@ function validateUniqueness(attr, conf, err, done) { var validators = { presence: validatePresence, + absence: validateAbsence, length: validateLength, numericality: validateNumericality, inclusion: validateInclusion, @@ -470,8 +494,11 @@ function validationFailed(inst, v, cb) { // 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; + if (skipValidation(inst, conf, 'if') + || skipValidation(inst, conf, 'unless')) { + if (cb) cb(true); + return false; + } var fail = false; var validator = validators[conf.validation]; @@ -500,7 +527,7 @@ function validationFailed(inst, v, cb) { message = 'is invalid'; } } - inst.errors.add(attr, message, code); + if (kind !== false) inst.errors.add(attr, message, code); fail = true; }); if (cb) { @@ -533,6 +560,7 @@ function skipValidation(inst, conf, kind) { var defaultMessages = { presence: 'can\'t be blank', + absence: 'can\'t be set', length: { min: 'too short', max: 'too long', diff --git a/test/validations.test.js b/test/validations.test.js index a5d52ae6..1a3faf55 100644 --- a/test/validations.test.js +++ b/test/validations.test.js @@ -159,6 +159,20 @@ describe('validations', function () { }); }); + + describe('absence', function () { + + it('should validate absence', function () { + User.validatesAbsenceOf('reserved', { if: 'locked' }); + var u = new User({reserved: 'foo', locked: true}); + u.isValid().should.not.be.true; + u.reserved = null; + u.isValid().should.be.true; + var u = new User({reserved: 'foo', locked: false}); + u.isValid().should.be.true; + }); + + }); describe('uniqueness', function () { it('should validate uniqueness', function (done) { @@ -242,6 +256,18 @@ describe('validations', function () { }); })).should.be.false; }); + + it('should work with if/unless', function (done) { + User.validatesUniquenessOf('email', { + if: function() { return true; }, + unless: function() { return false; } + }); + var u = new User({email: 'hello'}); + Boolean(u.isValid(function (valid) { + valid.should.be.true; + done(); + })).should.be.false; + }); }); describe('format', function () { @@ -266,7 +292,38 @@ describe('validations', function () { }); describe('custom', function () { - it('should validate using custom sync validation'); - it('should validate using custom async validation'); + it('should validate using custom sync validation', function() { + User.validate('email', function (err) { + if (this.email === 'hello') err(); + }); + var u = new User({email: 'hello'}); + Boolean(u.isValid()).should.be.false; + }); + + it('should validate and return detailed error messages', function() { + User.validate('global', function (err) { + if (this.email === 'hello' || this.email === 'hey') { + this.errors.add('hello', 'Cannot be `' + this.email + '`', 'invalid'); + err(false); // false: prevent global error message + } + }); + var u = new User({email: 'hello'}); + Boolean(u.isValid()).should.be.false; + u.errors.should.eql({ hello: [ 'Cannot be `hello`' ] }); + }); + + it('should validate using custom async validation', function(done) { + User.validateAsync('email', function (err, next) { + process.nextTick(next); + }, { + if: function() { return true; }, + unless: function() { return false; } + }); + var u = new User({email: 'hello'}); + Boolean(u.isValid(function (valid) { + valid.should.be.true; + done(); + })).should.be.false; + }); }); });