/* global Ember */
(function() {
var VERSION = '0.2.0';
if (Ember.libraries) {
Ember.libraries.register('Ember Validator', VERSION);
}
/**
* @module Ember.Validator
* @main Ember.Validator
*/
/**
* @class Ember.Validator
* @namespace Ember
* @extends Ember.Object
* @static
*/
Ember.Validator = Ember.Object.create({
options: {
/**
* Option to trim whitespace from string values before validation.
*
* @property trim
* @default true
* @type {Boolean}
*/
trim: true,
/**
* Option to set the trimmed value on the object. Trim must be true for
* this to work.
*
* @property trimApply
* @default true
* @type {Boolean}
*/
trimApply: true
},
/**
* Looks for rules property in the errorKey set in validations
*
* @private
* @method _getRulesForKey
* @pram {object} validations
* @param {string} key
*/
_getRulesForKey: function(validations, key) {
var property = validations[key];
if (property && Ember.typeOf(property.rules) === 'array') {
return property.rules;
} else {
Ember.Logger.warn('No valid defined rules found for property \''+
key + '\'.' + ' Please check your validation definitions.');
return [];
}
},
/**
* Creates the error message.
*
* @private
* @method _createResultMessage
* @param {String} errorKey
* @param {Ember.Validator.Rule} rule
*/
_createResultMessage: function(errorKey, rule) {
var propertyFormat = rule.get('propertyFormat'),
messageFormats = rule.get('messageFormats');
propertyFormat = propertyFormat ? propertyFormat : errorKey;
return this._formatMessage(rule, propertyFormat, messageFormats);
},
/**
* Formats the message based on the errorKey and messageFormats.
* The errorKey is always set as the first argument
*
* Related:
* {{#crossLink "Validator.Rule/propertyFormat:property"}}{{/crossLink}},
* {{#crossLink "Validator.Rule/messageFormats:property"}}{{/crossLink}}
*
* @private
* @method _formatMessage
* @param {Ember.Validator.Rule} rule
* @param {String} propertyFormat
* @param {Array} messageFormats
* @return {String} The formatted string
*/
_formatMessage: function(rule, propertyFormat, messageFormats) {
var formats = [],
message = rule.get('message');
Ember.assert('You must specify an error message for rule name ' +
'(%@)'.fmt(rule.get('name')), message);
formats = formats.concat(messageFormats);
formats.unshift(propertyFormat);
return Ember.String.fmt(message, formats);
},
/**
* Looks for a rule object defined as custom or defined in {@link Ember.Validator.Rules}.
* Any custom rules with the same name in {@link Ember.Validator.Rules} are merged.
*
* @private
* @method _getRuleObj
* @param {String} context
* @param {String} key
* @param {String} ruleName
* @return {Object} - The rule object
*/
_getRuleObj: function(context, key, ruleName) {
var validations = context.validations,
Rules = Ember.Validator.Rules,
customRule = validations[key][ruleName],
builtInRuleCopy = Ember.copy(Rules[ruleName]);
if (customRule) {
var rule = builtInRuleCopy ? Ember.merge(builtInRuleCopy, customRule) : customRule,
hasValidateMethod = typeof rule.validate === 'function';
Ember.assert('Must have validate function defined in custom rule.',
hasValidateMethod);
return Ember.Validator.Rule.create(rule, { name: ruleName });
}
if (builtInRuleCopy) {
return Ember.Validator.Rule.create(builtInRuleCopy);
} else {
Ember.assert('No valid rules were found.', false);
}
},
/**
* Responsible for running validation rules and adding the error to an
* instance of Ember.Validator.Error.
*
* Result generation will stop at the first failed validation per key.
*
* @private
* @method _generateResult
* @param {Object} context - The object doing the validation
* @param {Array} rules - rule names defined as strings
* @param {String} key - the current property being validated
*/
_generateResult: function(context, ruleNames, key, options) {
var self = this,
valueForKey = context.get(key),
result = context.get('validatorResult');
if (options.trim && Ember.typeOf(valueForKey) === 'string') {
var trimmed = valueForKey.trim();
if (options.trimApply) {
context.set(key, trimmed);
}
valueForKey = trimmed;
}
ruleNames.find(function(ruleName) {
var rule = self._getRuleObj(context, key, ruleName);
// Should only run rules on required or values that are not undefined
if (ruleName === 'required' || valueForKey !== undefined) {
// Add a context to rule instance for use in validate options
rule.set('context', context);
var didValidate = rule.validate(valueForKey, rule);
if (!didValidate) {
var message = options && options.squelch ?
null : self._createResultMessage(key, rule),
error = Ember.Validator.Error.create();
error.setProperties({
message: message,
context: context,
isValid: false,
ruleName: ruleName,
errorKey: key
});
result.set(key, error);
return true;
}
}
});
}
});
/**
* The base rule class which stores the validate method and message settings.
*
* @class Rule
* @constructor
* @namespace Validator
* @extends Ember.Object
*/
Ember.Validator.Rule = Ember.Object.extend({
/**
* Property used to customize the message formatting
*
* @property messageFormats
* @type array
*/
messageFormats: [],
/**
* Set this property when you want to customize the message to show something
* other than the default errorKey.
*
* See: {{#crossLink "Ember.Validator.Error/errorKey:property"}}errorKey{{/crossLink}}
*
* @property propertyFormat
* @type string
*/
propertyFormat: null,
/**
* The property used to display the error message. Can be set to a customized
* message with formatting or without.
*
* message is formatted like so:
*
* @example
* ```
* // %@1: errorKey || propertyFormat
* // %@2+: messageFormats
*
* '%@1 has invalid length, must be %@2 %@3 chars'.fmt(errorKey, messageFormats);
* ```
* errorKey is defaulted to %@1 and messageFormats are designated for %@2+
*
* Related: {{#crossLink "Validator.Error/errorKey:property"}}{{/crossLink}}
*
* @property message
* @uses messageFormats
* @type string
*/
message: null,
/**
* Define validations in this method and return the Boolean value.
*
* @method validate
* @param {*} value - The property value to validate
* @param {Object} options - The object validator with an object context included
* @return {Boolean}
*/
validate: function() {
Ember.assert('You must define a validate function for this to be a valid rule', false);
}
});
/**
* A static class used to defined reusable rules. Each property defined on the
* root of this class are wrapped in Ember.Validator.Rule.
*
* An example of adding more rules:
*
* ```javascript
* Ember.Validator.Rules.reopen({
* minLength: {
* min: 6,
*
* validate: function(value, options) {
* this.messageFormats = ['Minimum', options.min];
* return value.split('').length > options.min;
* },
*
* message: '%@2 of %@3 characters required.'
* }
* });
* ```
*
* Related:
* {{#crossLink "Validator.Rule"}}{{/crossLink}},
* http://emberjs.com/api/classes/Ember.String.html#method_fmt
*
* @static
* @class Rules
* @namespace Validator
*/
Ember.Validator.Rules = Ember.Object.create({
required: {
validate: function(value) {
return !Ember.isEmpty(value);
},
message: '%@1 is required'
},
number: {
validate: function(value) {
return !isNaN(parseInt(value, 10));
},
message: '%@1 is not a number'
}
});
/**
* Validation result object used to store the validation.
*
* @class Error
* @constructor
* @namespace Validator
* @extends Ember.Object
*/
Ember.Validator.Error = Ember.Object.extend({
/**
* @property message
* @type String
*/
message: null,
/**
* The object that is running the validation.
*
* @property context
* @type {Object || Ember.Object}
*/
context: null,
/**
* @property errorKey
* @type string
*/
errorKey: null,
/**
* @property isValid
* @type boolean
*/
isValid: null,
/**
* @property ruleName
* @type string
*/
ruleName: null
});
/**
* The array proxy which stores all the validation results
*
* @constructor
* @class Result
* @namespace Validator
* @extends Ember.ObjectProxy
*/
Ember.Validator.Result = Ember.ObjectProxy.extend({
/**
* All errors are set in the content property.
*
* @property content
* @type Object
*/
content: null,
/**
* @property error
* @type {Object}
*/
error: Ember.computed.alias('content'),
/**
* An array of all errors that exist in the content property
*
* @property errors
* @type {Array}
*/
errors: function() {
var content = this.get('content');
return Ember.keys(content).reduce(function(errors, key) {
errors.pushObject(content.get(key));
return errors;
}, []);
}.property('content'),
/**
* An array of all the error messages generated
*
* @property messages
* @type {Array}
*/
messages: Ember.computed.mapBy('errors', 'message'),
/**
* Set to false if any errors were generated in the validation
*
* @property isValid
* @type {Boolean}
*/
isValid: function() {
return Ember.isEmpty(this.get('errors'));
}.property('errors.@each')
});
/**
* Add this mixin to any object to add validation support. Exposes the validate()
* method which looks for a validations object.
*
* @class Support
* @static
* @namespace Validator
* @type {Ember.Mixin}
*/
Ember.Validator.Support = Ember.Mixin.create({
init: function() {
this._super();
this.set('validatorResult', Ember.Validator.Result.create({ content: {} }));
},
/**
* The property where validations are defined.
*
* @example
* ```javascript
* App.Person = Ember.Object.extend(Ember.Validator.Support, {
* name: null,
* phone: null,
*
* validations: {
* name: {
* rules: ['required']
* },
* phone: {
* rules: ['required', 'phone']
* phone: function(value, options) {
* // run validations
* }
* }
* }
* });
* ```
*
* @property validations
* @required
* @type {Object}
*/
validations: null,
/**
* Runs all validations defined in the validations object and stores results.
* The method returns a results object.
*
* ### Getting validation
* ```
* var person = App.Person.create({ name: null, age: 29 });
*
* person.validate().get('isValid') // false;;
* ```
*
* ### Getting error messages
* ```
* person.validate().get('error.name.message'); // 'name is required'
* ```
*
* You can also choose which keys to run validations on by passing an array
* to properties inside an options object.
* ```javascript
* person.validate({ properties: ['name', 'age'] });
* ```
*
* ### Supressing (Squelch) Error Messages
*
* Running validations without generating messages is possible with squelch.
* This is useful for when you want to validate an object but not generate
* error messages when using messages in a view.
*
* ```javascript
* person.validate({ squelch: true });
* ```
*
* Related:
* {{#crossLink "Validator.Results"}}{{/crossLink}},
* {{#crossLink "Validator.Support/validations:property"}}{{/crossLink}}
*
* @method validate
* @param {Object} options
* @return {Ember.Validator.Result}
*/
validate: function(options) {
var self = this,
validations = this.get('validations'),
Validator = Ember.Validator,
keys = Ember.keys(validations),
defaultOptions = Em.copy(Ember.Validator.options);
if (options) {
options = Ember.merge(defaultOptions, options);
} else {
options = defaultOptions;
}
Ember.assert('You do not have a \'validations\' object defined', validations);
// Check if keys are being sent as args in the method before checking
// validations object.
if (options.properties) {
keys = options.properties;
}
// Prep and/or clear out old errors
this.set('validatorResult.content', Ember.Validator.Error.create());
keys.forEach(function(key) {
var rules = Validator._getRulesForKey(validations, key);
Validator._generateResult(self, rules, key, options);
});
return this.get('validatorResult');
}
});
})();