diff --git a/lib/validatable.js b/lib/validatable.js index 54bcce2a..b02465e0 100644 --- a/lib/validatable.js +++ b/lib/validatable.js @@ -11,6 +11,65 @@ Validatable.validatesInclusionOf = getConfigurator('inclusion'); Validatable.validatesExclusionOf = getConfigurator('exclusion'); 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) { return function () { configure(this, name, arguments); @@ -19,39 +78,45 @@ function getConfigurator(name) { Validatable.prototype.isValid = function () { var valid = true, inst = this; + + // exit with success when no errors if (!this.constructor._validations) { - Object.defineProperty(this, 'errors', { - enumerable: false, - configurable: true, - value: false - }); + 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) { - Object.defineProperty(this, 'errors', { - enumerable: false, - configurable: true, - value: 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; @@ -78,6 +143,25 @@ function validationFailed(inst, v) { 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: { @@ -115,63 +199,6 @@ function nullCheck(attr, conf, err) { 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) { if (typeof v === 'undefined') return true; if (v instanceof Array && v.length === 0) return true; diff --git a/test/validations_test.coffee b/test/validations_test.coffee index aeba758f..d3e2c3cf 100644 --- a/test/validations_test.coffee +++ b/test/validations_test.coffee @@ -14,6 +14,8 @@ User = schema.define 'User', age: Number gender: String domain: String + pendingPeriod: Number + createdByAdmin: Boolean validAttributes = name: 'Anatoliy' @@ -22,11 +24,12 @@ validAttributes = age: 26 gender: 'male' domain: '1602' - -User.validatesPresenceOf 'email', 'name' - + createdByAdmin: false + createdByScript: true it 'should validate presence', (test) -> + User.validatesPresenceOf 'email', 'name' + user = new User test.ok not user.isValid(), 'User is not valid' 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.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) -> user = new User