// Copyright (C) 2011 Neal Stewart // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies // of the Software, and to permit persons to whom the Software is furnished to do // so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. (function(Backbone) { // Require Underscore and Backbone if there's a `require` function. // This makes `backbone.validations` work on the server or when using // `browserify`. if (typeof require !== 'undefined') { _ = require('underscore'); Backbone = require('backbone'); } // Premade Validators Backbone.Validations = {}; var validators = { "custom" : function(methodName, attributeName, model, valueToSet) { return model[methodName](attributeName, valueToSet); }, "required" : function(isRequired, attributeName, model, valueToSet) { if (isRequired && (_.isNull(valueToSet) || _.isUndefined(valueToSet) || valueToSet === "")) { return "required"; } else { return false; } }, "in" : function(whitelist, attributeName, model, valueToSet) { return _.include(whitelist, valueToSet) ? undefined : "in"; }, "email" : function(type, attributeName, model, valueToSet) { var emailRegex = new RegExp("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", "i"); if (_.isString(valueToSet) && !valueToSet.match(emailRegex)) { return "email"; } }, "url" : function(type, attributeName, model, valueToSet) { // taken from jQuery UI validation var urlRegex = /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i; if (_.isString(valueToSet) && !valueToSet.match(urlRegex)) { return "url"; } }, "number" : function(type, attributeName, model, valueToSet) { return isNaN(valueToSet) ? 'number' : undefined; }, "Array": function(type, attributeName, model, valueToSet) { return _.isArray(valueToSet) ? undefined : 'Array'; }, "Boolean": function(type, attributeName, model, valueToSet) { return _.isBoolean(valueToSet) ? undefined : 'Boolean'; }, "String": function(type, attributeName, model, valueToSet) { return _.isString(valueToSet) ? undefined : 'String'; }, "digits": function (type, attributeName, model, valueToSet) { var isBeingSet = !_.isUndefined(valueToSet); return (!/^\d+$/.test(valueToSet) && isBeingSet) ? 'digits' : undefined; }, "pattern" : function(pattern, attributeName, model, valueToSet) { if (_.isString(valueToSet)) { if (pattern.test(valueToSet)) { return false; } else { return "pattern"; } } }, "min" : function(minimumValue, attributeName, model, valueToSet) { if (valueToSet < minimumValue) { return "min"; } }, "max" : function(maximumValue, attributeName, model, valueToSet) { if (valueToSet > maximumValue) { return "max"; } }, "minlength" : function(minlength, attributeName, model, valueToSet) { if (_.isString(valueToSet)) { if (valueToSet.length < minlength) { return "minlength"; } } }, "maxlength" : function(maxlength, attributeName, model, valueToSet) { if (_.isString(valueToSet)) { if (valueToSet.length > maxlength) { return "maxlength"; } } }, "arrayElem": function(validator, attributeName, model, valueToSet) { if (_.isArray(valueToSet) && !_.all(valueToSet, validator)) { return "validElem"; } else return false; } }; var customValidators = {}; var getCustomValidator = function(name) { var cv = customValidators[name]; if (!cv) { throw "custom validator '"+name+"' could not be found."; } return cv; }; Backbone.Validations.addValidator = function(name, validator) { if (validators.hasOwnProperty(name) || customValidators.hasOwnProperty(name)) { throw "existing validator"; } customValidators[name] = validator; }; /* The newValidate method overrides validate in Backbone.Model. It has the same interface as the validate function which you would provide. The returned object looks like this: { attributeName : ["required", "of", "errors"], otherAttributeName: ["and", "so", "on"] } */ function newValidate(attributes) { var errorsForAttribute, errorHasOccured, errors = {}; for (var attrName in this._attributeValidators) { var valueToSet = attributes[attrName]; var validateAttribute = this._attributeValidators[attrName]; if (validateAttribute) { errorsForAttribute = validateAttribute(this, valueToSet); } if (errorsForAttribute) { errorHasOccured = true; errors[attrName] = errorsForAttribute; } } return errorHasOccured ? errors : false; } function createMinValidator(attributeName, minimumValue) { return _.bind(validators.min, null, minimumValue); } function createMaxValidator(attributeName, maximumValue) { return _.bind(validators.max, null, maximumValue); } /* createValidator takes in: - the model - the name of the attribute - the type of validation - the description of the validation returns a function that takes in: - the value being set for the attribute and either returns nothing (undefined), or the error name (string). */ function createValidator(attributeName, type, description) { var validator, validatorMethod, customValidator; if (type === "type") { type = description; } validator = validators[type]; if (!validator) { validator = getCustomValidator(type); } if (!validator) { throw "Improper validation type '"+type+"'" ; } return _.bind(validator, null, description, attributeName); } function createAttributeValidator(attributeName, attributeDescription) { var validatorsForAttribute = [], type, desc; for (type in attributeDescription) { desc = attributeDescription[type]; validatorsForAttribute.push(createValidator(attributeName, type, desc)); } return function(model, valueToSet, hasOverridenError, options) { var validator, result, errors = []; for (var i = 0, length = validatorsForAttribute.length; i < length; i++) { validator = validatorsForAttribute[i]; result = validator(model, valueToSet); if (result) { if (_.isArray(result)) { errors = errors.concat(result); } else { errors.push(result); } } } if (errors.length) { return errors; } else { return false; } }; } function createValidators(modelValidations) { var attributeValidations, attributeValidators = {}; for (var attrName in modelValidations) { attributeValidations = modelValidations[attrName]; attributeValidators[attrName] = createAttributeValidator(attrName, attributeValidations); } return attributeValidators; } var oldValidate = Backbone.Model.prototype._validate; function newPerformValidation(attrs, options) { var result = oldValidate.apply(this, arguments); if(!result){ _.each(this.validationError, function(error, name) { this.trigger('invalid:' + name, this, this.validationError, options); }, this); } return result; } // save the old backbone var oldModel = Backbone.Model; // Constructor for our new Validations Model Backbone.Validations.Model = Backbone.Model.extend({ constructor : function() { // if they pass an object, construct the new validations if (typeof this.validate === "object" && this.validate !== null) { if (!this.constructor.prototype._attributeValidators) { this.constructor.prototype._attributeValidators = createValidators(this.validate); this.constructor.prototype.validate = newValidate; this.constructor.prototype._validate = newPerformValidation; } } oldModel.apply(this, arguments); } }); // Override Backbone.Model with our new Model Backbone.Model = Backbone.Validations.Model; // Requisite noConflict Backbone.Validations.Model.noConflict = function() { Backbone.Model = oldModel; }; }(typeof Backbone === 'undefined' ? null : Backbone));