Skip to content

Commit

Permalink
add support for nested keys in validation rules
Browse files Browse the repository at this point in the history
  • Loading branch information
Offir Golan committed Mar 5, 2016
1 parent d948c72 commit 74e6376
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 23 deletions.
40 changes: 40 additions & 0 deletions addon/utils/assign.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Copyright 2016, Yahoo! Inc.
* Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/

/**
* Assigns a value to an object via the given path while creating new objects if
* the pathing requires it. If the given path is `foo.bar`, it will create a new object (obj.foo)
* and assign value to obj.foo.bar. If the given object is an Ember.Object, it will create new Ember.Objects.
*/
import Ember from 'ember';

const {
get,
set,
isNone,
defineProperty
} = Ember;

export default function assign(obj, path, value, delimiter = '.') {
let keyPath = path.split(delimiter);
let lastKeyIndex = keyPath.length - 1;
let isEmberObject = obj instanceof Ember.Object;

// Iterate over each key in the path (minus the last one which is the property to be assigned)
for (let i = 0; i < lastKeyIndex; ++ i) {
let key = keyPath[i];
// Create a new object if it doesnt exist
if (isNone(get(obj, key))) {
set(obj, key, isEmberObject ? Ember.Object.create() : {});
}
obj = get(obj, key);
}

if(value instanceof Ember.ComputedProperty) {
defineProperty(obj, keyPath[lastKeyIndex], value);
} else {
set(obj, keyPath[lastKeyIndex], value);
}
}
5 changes: 5 additions & 0 deletions addon/utils/flatten.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* Copyright 2016, Yahoo! Inc.
* Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/

export default function flatten(array = []) {
let result = [];

Expand Down
44 changes: 31 additions & 13 deletions addon/validations/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
import Ember from 'ember';
import getOwner from 'ember-getowner-polyfill';
import flatten from '../utils/flatten';
import assign from '../utils/assign';
import ValidationResult from './result';
import ValidationResultCollection from './result-collection';
import BaseValidator from '../validators/base';
import cycleBreaker from '../utils/cycle-breaker';

const {
get,
set,
run,
RSVP,
merge,
Expand Down Expand Up @@ -182,39 +184,55 @@ function createValidationsObject(validations = {}) {
this._super(...arguments);
let validationRules = validations;
let parentValidations = this.get('_parentValidations');
let attrs = {};
let model = this.get('model');
let attrs = Ember.Object.create();

if(parentValidations) {
validationRules = merge(merge({}, parentValidations.get('_validationRules')), validationRules);
}

let validatableAttributes = Object.keys(validationRules);

// Normalize nested keys into actual objects
validationRules = validatableAttributes.reduce(( obj, key )=> {
assign(obj, key, validationRules[key]);
return obj;
}, {});

validatableAttributes.forEach((attribute) => {
attrs[attribute] = createCPValidationFor(attribute, validationRules[attribute]);
assign(attrs, attribute, createCPValidationFor(attribute, validationRules[attribute]));

// Add a reference to the model in the deepest object
let path = attribute.split('.');
let lastObject = get(attrs, path.slice(0, path.length - 1).join('.'));
if(isNone(get(lastObject, '_model'))) {
set(lastObject, '_model', model);
}
});

this.setProperties({
_validatableAttributes: validatableAttributes,
_validationRules: validationRules,
_validators: {},
_debouncedValidations: {},
attrs: Ember.Object.extend(attrs).create({
_model: this.get('model')
})
attrs
});

createGlobalValidationProps(this);
},

destroy() {
this._super(...arguments);
let validatableAttrs = get(this, '_validatableAttributes');
let debouncedValidations = get(this, `_debouncedValidations`);

// Cancel all debounced timers
Object.keys(debouncedValidations).forEach(attr => {
let attrCache = debouncedValidations[attr];
// Itterate over each attribute and cancel all of its debounced validations
Object.keys(attrCache).forEach(v => run.cancel(attrCache[v]));
validatableAttrs.forEach(attr => {
let attrCache = get(debouncedValidations, attr);
if(!isNone(attrCache)) {
// Itterate over each attribute and cancel all of its debounced validations
Object.keys(attrCache).forEach(v => run.cancel(attrCache[v]));
}
});
}
});
Expand Down Expand Up @@ -425,11 +443,11 @@ function getValidatorsFor(attribute, model) {
function getDebouncedValidationsCacheFor(attribute, model) {
var debouncedValidations = get(model, `validations._debouncedValidations`);

if (isNone(debouncedValidations[attribute])) {
debouncedValidations[attribute] = {};
if (isNone(get(debouncedValidations, attribute))) {
assign(debouncedValidations, attribute, {});
}

return debouncedValidations[attribute];
return get(debouncedValidations, attribute);
}

/**
Expand Down Expand Up @@ -468,7 +486,7 @@ function createValidatorsFor(attribute, model) {
});

// Add validators to model instance cache
validatorCache[attribute] = validators;
assign(validatorCache, attribute, validators);

return validators;
}
Expand Down
90 changes: 80 additions & 10 deletions tests/integration/validations/factory-general-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,18 +286,24 @@ test("debounced validations", function(assert) {
test("debounced validations should cleanup on object destroy", function(assert) {
var done = assert.async();
var initSetup = true;

var debouncedValidator = validator((value, options, model, attr) => {
model.set('foo', 'bar');
return Validators.presence(value, options, model, attr);
}, {
debounce() {
return initSetup ? 0 : 500; // Do not debounce on initial object creation
}
});

var Validations = buildValidations({
firstName: validator(Validators.presence),
lastName: validator((value, options, model, attr) => {
model.set('foo', 'bar');
return Validators.presence(value, options, model, attr);
}, {
debounce() {
return initSetup ? 0 : 500; // Do not debounce on initial object creation
}
}),
lastName: debouncedValidator,
'details.url': debouncedValidator,
});
var object = setupObject(this, Ember.Object.extend(Validations), {
details: {}
});
var object = setupObject(this, Ember.Object.extend(Validations));

assert.equal(object.get('validations.isValid'), false, 'isValid was expected to be FALSE');
assert.equal(object.get('validations.isValidating'), false, 'isValidating was expected to be TRUE');
Expand All @@ -308,8 +314,12 @@ test("debounced validations should cleanup on object destroy", function(assert)
assert.equal(object.get('validations.attrs.lastName.message'), 'lastName should be present');

initSetup = false;
object.set('lastName', 'Golan');
object.setProperties({
'lastName': 'Golan',
'details.url': 'github.com'
});
assert.equal(object.get('validations.attrs.lastName.isValidating'), true);
assert.equal(object.get('validations.attrs.details.url.isValidating'), true);

Ember.run.later(() => {
try {
Expand Down Expand Up @@ -503,3 +513,63 @@ test("validations persist with deep inheritance", function(assert) {
assert.equal(baby.get('validations.errors.length'), 0);
});

test("nested keys - simple", function(assert) {
var Validations = buildValidations({
'user.firstName': validator(Validators.presence),
'user.lastName': validator(Validators.presence)
});
var object = setupObject(this, Ember.Object.extend(Validations), {
user: {}
});

assert.equal(object.get('validations.attrs.user.firstName.isValid'), false);
assert.equal(object.get('validations.attrs.user.lastName.isValid'), false);
assert.equal(object.get('validations.isValid'), false);

object.set('user.firstName', 'Offir');

assert.equal(object.get('validations.attrs.user.firstName.isValid'), true);
assert.equal(object.get('validations.isValid'), false);

object.set('user.lastName', 'Golan');

assert.equal(object.get('validations.attrs.user.lastName.isValid'), true);
assert.ok(!object.get('validations.attrs._model'));
assert.equal(object.get('validations.isValid'), true);
});

test("nested keys - complex", function(assert) {
var Validations = buildValidations({
'firstName': validator(Validators.presence),
'user.foo.bar.baz': validator(Validators.presence),
'user.foo.boop': validator(Validators.presence)
});
var object = setupObject(this, Ember.Object.extend(Validations));

assert.equal(object.get('validations.attrs.firstName.isValid'), false);
assert.equal(object.get('validations.attrs.user.foo.bar.baz.isValid'), false);
assert.equal(object.get('validations.attrs.user.foo.boop.isValid'), false);
assert.equal(object.get('validations.isValid'), false);

object.set('user', { foo: { bar: {} } });

assert.equal(object.get('validations.attrs.firstName.isValid'), false);
assert.equal(object.get('validations.attrs.user.foo.bar.baz.isValid'), false);
assert.equal(object.get('validations.attrs.user.foo.boop.isValid'), false);
assert.equal(object.get('validations.isValid'), false);

object.set('firstName', 'Offir');
object.set('user.foo.bar.baz', 'blah');
object.set('user.foo.boop', 'blah');

assert.equal(object.get('validations.attrs.firstName.isValid'), true);
assert.equal(object.get('validations.attrs.user.foo.bar.baz.isValid'), true);
assert.equal(object.get('validations.attrs.user.foo.boop.isValid'), true);

assert.ok(object.get('validations.attrs._model'));
assert.ok(object.get('validations.attrs.user.foo.bar._model'));
assert.ok(object.get('validations.attrs.user.foo._model'));

assert.equal(object.get('validations.isValid'), true);
});

39 changes: 39 additions & 0 deletions tests/unit/utils/assign-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Ember from 'ember';
import { module, test } from 'qunit';
import assign from 'ember-cp-validations/utils/assign';

module('Unit | Utils | assign');

test('single level', function(assert) {
let obj = {};
assign(obj, 'foo.bar', 1);
assert.deepEqual(obj, { foo: { bar: 1}});
});

test('single level - ember object', function(assert) {
let obj = Ember.Object.create();
assign(obj, 'foo.bar', 1);
assert.ok(obj.foo instanceof Ember.Object);
assert.equal(obj.get('foo.bar'), 1);
});


test('single level - ember object w/ CP', function(assert) {
let obj = Ember.Object.create();
assign(obj, 'foo.bar', Ember.computed(() => 1));
assert.ok(obj.foo instanceof Ember.Object);
assert.equal(obj.get('foo.bar'), 1);
});

test('multi level', function(assert) {
let obj = {};
assign(obj, 'foo.bar.baz.boo', 1);
assert.deepEqual(obj, { foo: { bar: { baz: { boo: 1}}}});
});

test('multi level - ember object', function(assert) {
let obj = Ember.Object.create();
assign(obj, 'foo.bar.baz.boo', 1);
assert.ok(obj.foo.bar.baz instanceof Ember.Object);
assert.equal(obj.get('foo.bar.baz.boo'), 1);
});

0 comments on commit 74e6376

Please sign in to comment.