Skip to content

Commit

Permalink
Validator Composability (#566)
Browse files Browse the repository at this point in the history
  • Loading branch information
offirgolan authored Feb 13, 2018
1 parent f6f7fc9 commit cfe88a4
Show file tree
Hide file tree
Showing 8 changed files with 376 additions and 35 deletions.
2 changes: 1 addition & 1 deletion addon/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ import Validator from './validations/validator';
*
* export default Ember.Route.extend({
* model() {
* var container = this.get('container');
* const container = this.get('container');
* return User.create({ username: 'John', container })
* }
* });
Expand Down
25 changes: 25 additions & 0 deletions addon/utils/lookup-validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Lookup a validator of a specific type on the owner
*
* @param {Ember.Owner} owner
* @param {String} type
* @throws {Error} Validator not found
* @return {Class} Validator class
*/
export default function lookupValidator(owner, type) {
if (!owner) {
throw new Error(
`[ember-cp-validations] \`lookupValidator\` requires owner/container access.`
);
}

const validatorClass = owner.factoryFor(`validator:${type}`);

if (!validatorClass) {
throw new Error(
`[ember-cp-validations] Validator not found of type: ${type}.`
);
}

return validatorClass;
}
24 changes: 1 addition & 23 deletions addon/validations/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import { isEmpty, isNone } from '@ember/utils';
import { getOwner } from '@ember/application';
import EmberObject, { getWithDefault, computed, set, get } from '@ember/object';
import { A as emberArray, makeArray, isArray } from '@ember/array';

import Ember from 'ember';
import deepSet from '../utils/deep-set';
import ValidationResult from '../-private/result';
import ResultCollection from './result-collection';
import BaseValidator from '../validators/base';
import cycleBreaker from '../utils/cycle-breaker';
import shouldCallSuper from '../utils/should-call-super';
import lookupValidator from '../utils/lookup-validator';
import { flatten } from '../utils/array';
import {
isDsModel,
Expand Down Expand Up @@ -775,28 +775,6 @@ function createValidatorsFor(attribute, model) {
return validators;
}

/**
* Lookup a validators of a specific type on the owner
*
* @method lookupValidator
* @throws {Error} Validator not found
* @private
* @param {Ember.Owner} owner
* @param {String} type
* @return {Class} Validator class or undefined if not found
*/
function lookupValidator(owner, type) {
let validatorClass = owner.factoryFor(`validator:${type}`);

if (isNone(validatorClass)) {
throw new Error(
`[ember-cp-validations] Validator not found of type: ${type}.`
);
}

return validatorClass;
}

/**
* Call the passed resolve method. This is needed as run.debounce expects a
* static method to work properly.
Expand Down
90 changes: 83 additions & 7 deletions addon/validators/base.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { bool } from '@ember/object/computed';

import EmberObject, { set, get } from '@ember/object';
import EmberObject, { set, get, computed } from '@ember/object';
import { isNone } from '@ember/utils';
import { getOwner } from '@ember/application';
import Messages from 'ember-cp-validations/validators/messages';
import Options from 'ember-cp-validations/-private/options';
import lookupValidator from 'ember-cp-validations/utils/lookup-validator';
import {
unwrapString,
getValidatableValue,
mergeOptions
mergeOptions,
isPromise
} from 'ember-cp-validations/utils/utils';

class TestResult {
constructor(result) {
this.isValid = result === true;
this.message = typeof result === 'string' ? result : null;
}
}

/**
* @class Base
* @module Validators
Expand Down Expand Up @@ -72,6 +80,14 @@ const Base = EmberObject.extend({
*/
_type: null,

/**
* Validators cache used by `test` api
* @property _testValidatorCache
* @private
* @type {Object}
*/
_testValidatorCache: computed(() => ({})).readOnly(),

init() {
this._super(...arguments);
let globalOptions = get(this, 'globalOptions');
Expand Down Expand Up @@ -179,7 +195,7 @@ const Base = EmberObject.extend({
*
* ```javascript
* validate(value, options) {
* var exists = false;
* const exists = false;
*
* get(options, 'description') = 'Username';
* get(options, 'username') = value;
Expand All @@ -198,7 +214,7 @@ const Base = EmberObject.extend({
*
* @method createErrorMessage
* @param {String} type The type of message template to use
* @param {Mixed} value Current value being evaluated
* @param {Mixed} value Current value being evaluated
* @param {Object} options Validator built and processed options (used as the message string context)
* @return {String} The generated message
*/
Expand Down Expand Up @@ -226,6 +242,66 @@ const Base = EmberObject.extend({
}

return message.trim();
},

/**
* Easily compose complicated validations by using this method to validate
* against other validators.
*
* ```javascript
* validate(value, options, ...args) {
* let result = this.test('presence', value, { presence: true }, ...args);
*
* if (!result.isValid) {
* return result.message;
* }
*
* // You can even test against your own custom validators
* result = this.test('my-validator', value, { foo: 'bar' }, ...args);
*
* if (!result.isValid) {
* return result.message;
* }
*
* result = this.test('number', value, { integer: true }, ...args);
*
* // You can easily override the error message by returning your own.
* if (!result.isValid) {
* return 'This value must be an integer!';
* }
*
* // Add custom logic...
*
* return true;
* }
* ```
* @method test
* @param {String} type The validator type (e.x. 'presence', 'length', etc.)
* The following types are unsupported:
* 'alias', 'belongs-to', 'dependent', 'has-many'
* @param {...} args The params to pass through to the validator
* @return {Object} The test result object which will contain `isValid`
* and `message`. If the validator is async, then the
* return value will be a promise.
*/
test(type, ...args) {
const cache = this.get('_testValidatorCache');
const unsupportedTypes = ['alias', 'belongs-to', 'dependent', 'has-many'];

if (unsupportedTypes.includes(type)) {
throw new Error(
`[ember-cp-validations] The \`test\` API does not support validators of type: ${type}.`
);
}

cache[type] = cache[type] || lookupValidator(getOwner(this), type).create();
const result = cache[type].validate(...args);

if (isPromise(result)) {
return result.then(r => new TestResult(r), r => new TestResult(r));
}

return new TestResult(result);
}
});

Expand Down Expand Up @@ -346,7 +422,7 @@ export default Base;
* To use our unique-username validator we just have to add it to the model definition
*
* ```javascript
* var Validations = buildValidations({
* const Validations = buildValidations({
* username: validator('unique-username', {
* showSuggestions: true
* }),
Expand All @@ -371,7 +447,7 @@ export default Base;
* });
*
* test('it works', function(assert) {
* var validator = this.subject();
* const validator = this.subject();
* assert.ok(validator);
* });
* ```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ moduleFor('validator:<%= dasherizedModuleName %>', 'Unit | Validator | <%= dashe
});

test('it works', function(assert) {
var validator = this.subject();
const validator = this.subject();
assert.ok(validator);
});
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
"ember-export-application-global": "^2.0.0",
"ember-font-awesome": "^3.0.5",
"ember-load-initializers": "^1.0.0",
"ember-maybe-import-regenerator": "^0.1.6",
"ember-qunit-nice-errors": "^1.2.0",
"ember-resolver": "^4.3.0",
"ember-source": "~2.18.0",
"ember-truth-helpers": "1.3.0",
Expand Down
Loading

0 comments on commit cfe88a4

Please sign in to comment.