/**
* Copyright 2016, Yahoo! Inc.
* Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
import Ember from 'ember';
import Messages from 'ember-cp-validations/validators/messages';
import getOwner from 'ember-getowner-polyfill';
import { unwrapString } from 'ember-cp-validations/utils/utils';
const {
get,
set,
isNone
} = Ember;
const assign = Ember.assign || Ember.merge;
/**
* @class Base
* @module Validators
*/
const Base = Ember.Object.extend({
/**
* Options passed in to the validator when defined in the model
* @property options
* @type {Object}
*/
options: null,
/**
* Default validation options for this specific attribute
* @property defaultOptions
* @type {Object}
*/
defaultOptions: null,
/**
* Global validation options for this model
* @property globalOptions
* @type {Object}
*/
globalOptions: null,
/**
* Model instance
* @property model
* @type {Model}
*/
model: null,
/**
* Attributed name of the model this validator is attached to
* @property attribute
* @type {String}
*/
attribute: null,
/**
* Error message object. Populated by validators/messages
* @property errorMessages
* @type {Object}
*/
errorMessages: null,
/**
* Validator type
* @property _type
* @private
* @type {String}
*/
_type: null,
init() {
this._super(...arguments);
const globalOptions = get(this, 'globalOptions');
const defaultOptions = get(this, 'defaultOptions');
const options = get(this, 'options');
const owner = getOwner(this);
let errorMessages;
if (!isNone(owner)) {
// Since default error messages are stored in app/validators/messages, we have to look it up via the owner
errorMessages = owner._lookupFactory('validator:messages');
}
// If for some reason, we can't find the messages object (i.e. unit tests), use default
errorMessages = errorMessages || Messages;
set(this, 'options', this.buildOptions(options || {}, defaultOptions || {}, globalOptions || {}));
set(this, 'errorMessages', errorMessages.create());
},
/**
* Build options hook. Merges default options into options object.
* This method gets called on init and is the ideal place to normalize your options.
* The [presence validator](https://github.com/offirgolan/ember-cp-validations/blob/master/app/validators/presence.js) is a good example to checkout
* @method buildOptions
* @param {Object} options
* @param {Object} defaultOptions
* @param {Object} globalOptions
* @return {Object}
*/
buildOptions(options = {}, defaultOptions = {}, globalOptions = {}) {
const builtOptions = assign(assign(assign({}, globalOptions), defaultOptions), options);
// Overwrite the validator's value method if it exists in the options and remove it since
// there is no need for it to be passed around
this.value = builtOptions.value || this.value;
delete builtOptions.value;
return builtOptions;
},
/**
* Creates a new object and calls any option property that is a function with the validator context.
* This method is called right before `validate` and the returned object gets passed into the validate method as its options
* @method processOptions
* @return {Object}
*/
processOptions() {
const options = assign({}, get(this, 'options') || {});
const model = get(this, 'model');
const attribute = get(this, 'attribute');
Object.keys(options).forEach(key => {
const option = options[key];
if (typeof option === 'function' && key !== 'message') {
options[key] = option.call(this, model, attribute);
}
});
return options;
},
/**
* Used to retrieve the value to validate.
* This method gets called right before `validate` and the returned value
* gets passed into the validate method.
*
* @method value
* @param {Object} model
* @param {String} attribute
* @return The current value of `model[attribute]`
*/
value(model, attribute) {
return get(model, attribute);
},
/**
* Wrapper method to `value` that passes the necessary parameters
*
* @method getValue
* @private
* @return {Unknown} value
*/
getValue() {
return this.value(get(this, 'model'), get(this, 'attribute'));
},
/**
* The validate method is where all of your logic should go.
* It will get passed in the current value of the attribute this validator is attached to.
* Within the validator object, you will have access to the following properties:
* @method validate
* @param {Unknown} value The current value of the attribute
* @param {Object} options The built and processed options
* @param {Object} model The current model being evaluated
* @param {String} attribute The current attribute being evaluated
* @return
* One of the following types:
* - `Boolean`: `true` if the current value passed the validation
* - `String`: The error message
* - `Promise`: A promise that will either resolve or reject, and will finally return either `true` or the final error message string.
*/
validate() {
return true;
},
/**
* Used by all pre-defined validators to build an error message that is present
* in `validators/message` or declared in your i18n solution.
*
* If we extended our default messages to include `uniqueUsername: '{username} already exists'`,
* we can use this method to generate our error message.
*
* ```javascript
* validate(value, options) {
* var exists = false;
*
* options.description = 'Username';
* options.username = value;
*
* // check with server if username exists...
*
* if(exists) {
* return this.createErrorMessage('uniqueUsername', value, options);
* }
*
* return true;
* }
* ```
*
* If we input `johndoe` and that username already exists, the returned message would be `'johndoe already exists'`.
*
* @method createErrorMessage
* @param {String} type The type of message template to use
* @param {Unknown} value Current value being evaluated
* @param {Object} options Validator built and processed options (used as the message string context)
* @return {String} The generated message
*/
createErrorMessage(type, value, options = {}) {
const messages = this.get('errorMessages');
let message = unwrapString(options.message);
options.description = messages.getDescriptionFor(get(this, 'attribute'), options);
if (message) {
if (typeof message === 'string') {
message = messages.formatMessage(message, options);
} else if (typeof message === 'function') {
message = message.apply(this, arguments);
message = isNone(message) ? messages.getMessageFor(type, options) : messages.formatMessage(message, options);
}
} else {
message = messages.getMessageFor(type, options);
}
return message.trim();
}
});
Base.reopenClass({
/**
* Generate the needed depenent keys for this validator
*
* @method getDependentsFor
* @static
* @param {String} attribute
* @param {Object} options
* @return {Array} dependent keys
*/
getDependentsFor() {
return [];
}
});
export default Base;
/**
* Creating custom validators is very simple. To generate a validator named `unique-username` in Ember CLI
*
* ```bash
* ember generate validator unique-username
* ```
*
* This will create the following files
*
* * `app/validators/unique-username.js`
* * `tests/unit/validators/unique-username-test.js`
*
* ```javascript
* // app/validators/unique-username.js
*
* import BaseValidator from 'ember-cp-validations/validators/base';
*
* const UniqueUsername = BaseValidator.extend({
* validate(value, options, model, attribute) {
* return true;
* }
* });
*
* UniqueUsername.reopenClass({
* getDependentsFor(attribute, options) {
* return [];
* }
* });
*
* export default UniqueUsername;
* ```
*
* **Side Node**: Before we continue, I would suggest checking out the documentation for the {{#crossLink 'Base'}}Base Validator{{/crossLink}}.
*
* If you want to interact with the `store` within your validator, you can simply inject the service like you would a component.
* Since you have access to your model and the current value, you should be able to send the server the right information to determine if this username is unique.
*
* ```javascript
* // app/validators/unique-username.js
*
* import Ember from 'ember';
* import BaseValidator from 'ember-cp-validations/validators/base';
*
* const UniqueUsername = BaseValidator.extend({
* store: Ember.inject.service(),
*
* validate(value, options, model, attribute) {
* return this.get('store').findRecord('user', value).then((user) => {
* if(user && user.id === value) {
* let message = `The username '${value}' already exists.`;
* let meta = user.get('meta');
*
* if(options.showSuggestions && meta && meta.suggestions) {
* message += "What about one of the these: " + meta.suggestions.join(', ');
* }
* return message;
* } else {
* return true;
* }
* })
* }
* });
* ```
*
* ## Dependent Keys
*
* There will be times when your validator will be dependent on some other property or object. Instead of having to
* include them in your option's `dependentKeys`, you can declare them in the static `getDependentsFor` hook. This hook
* recieves two parameters. The first is the `attribute` that this validator is being added to, and the second are the `options`
* there were passed to this validator.
*
* From the above code sample:
*
* ```javascript
* // app/validators/unique-username.js
*
* import BaseValidator from 'ember-cp-validations/validators/base';
*
* const UniqueUsername = BaseValidator.extend({});
*
* UniqueUsername.reopenClass({
* getDependentsFor(attribute, options) {
* return [];
* }
* });
*
* export default UniqueUsername;
* ```
*
* All dependent keys are in reference to the model's `validations.attrs` object. So when you return `['username']`,
* it will add a dependent to `model.validations.attrs.username`. If you want to add a dependent on the model, your
* key needs to be prefixed with `_model`. So when you return `['_model.username']`, it will add a dependent to `model.username`.
* This means that if you have a dependent on a service, that service must be injected into the model since returning `['_model.myService.someProperty']`
* will be interpreted as `model.myService.someProperty`.
*
* ## Usage
*
* To use our unique-username validator we just have to add it to the model definition
*
* ```javascript
* var Validations = buildValidations({
* username: validator('unique-username', {
* showSuggestions: true
* }),
* });
*
* export default DS.Model.extend(Validations, {
* 'username': DS.attr('string'),
* });
* ```
*
* ## Testing
* As mentioned before, the generator created a unit test for your new custom validator.
*
* ```javascript
* // tests/unit/validators/unique-username-test.js
*
* import Ember from 'ember';
* import { moduleFor, test } from 'ember-qunit';
*
* moduleFor('validator:unique-username', 'Unit | Validator | unique-username', {
* needs: ['validator:messages']
* });
*
* test('it works', function(assert) {
* var validator = this.subject();
* assert.ok(validator);
* });
* ```
*
* A simple test for our validation method can be as such
*
* ```javascript
* test('username is unique', function(assert) {
* assert.expect(1);
*
* let validator = this.subject();
* let done = assert.async();
*
* validator.validate('johndoe42').then((message) => {
* assert.equal(message, true);
* done();
* });
* });
* ```
* @class Custom
* @module Validators
* @extends Base
*/
/**
* A validator can also be declared with an inline function. The function will be then wrapped in the {{#crossLink 'Base'}}Base Validator{{/crossLink}} class and used just like any other pre-defined validator.
*
* ```javascript
* // Example
* validator(function(value, options, model, attribute) {
* return value === options.username ? true : `must be ${options.username}`;
* } , {
* username: 'John' // Any options can be passed here
* })
* ```
*
* @class Inline
* @module Validators
* @extends Base
*/