Skip to content

Commit

Permalink
provide overrideable Model#_validateOne()
Browse files Browse the repository at this point in the history
this makes it easy to customize a model's validations

pertains to #24
  • Loading branch information
aaronj1335 committed Feb 11, 2013
1 parent d432ae6 commit 1837759
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 13 deletions.
23 changes: 20 additions & 3 deletions js/fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ define([
return value;
},

validate: function(value, mimetype) {
validate: function(value, mimetype, options) {
value = this._normalizeValue(value);
if (value == null) {
if (this.nonnull) {
Expand All @@ -168,6 +168,9 @@ define([
if (mimetype) {
value = this.serialize(value, mimetype, true);
}
if (options && options.validateField) {
options.validateField(null, value);
}
return value;
},

Expand Down Expand Up @@ -624,7 +627,15 @@ define([
},

validate: function(value, mimetype, options) {
var name, field, structure, error;
var name, field, structure, error, ops,
wrapValidateFieldsFunction = function(f, name, separator) {
separator = separator == null? '.' : separator;
return function(fieldName) {
var args = Array.prototype.slice.call(arguments, 0);
args[0] = fieldName? name + separator + fieldName : name;
return f.apply(this, args);
};
};

this._super.apply(this, arguments);

Expand All @@ -640,7 +651,13 @@ define([
continue;
}
try {
field.validate(value[name], mimetype);
ops = options && options.validateField?
_.extend({}, options, {
validateField: wrapValidateFieldsFunction(
options.validateField, name)
}) :
options;
field.validate(value[name], mimetype, ops);
} catch (e) {
error = error || CompoundError(null, {structure: {}});
error.structure[name] = [e];
Expand Down
27 changes: 19 additions & 8 deletions js/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -412,10 +412,15 @@ define([
},

validate: function() {
var request = this._getRequest(this._loaded? 'update' : 'create'),
var self = this,
request = self._getRequest(self._loaded? 'update' : 'create'),
dfd = $.Deferred();
try {
request.validate(request.extract(this));
request.validate(request.extract(self), null, {
validateField: function(fieldName, value) {
self._validateOne(fieldName, value);
}
});
} catch (e) {
dfd.reject(e);
}
Expand Down Expand Up @@ -455,7 +460,9 @@ define([

_initiateRequest: function(name, params) {
return this._getRequest(name).initiate(this.get('id'), params);
}
},

_validateOne: function(fieldName, value) { }
}, {mixins: [Eventable]});

asSettable.call(Model.prototype, {
Expand Down Expand Up @@ -500,9 +507,9 @@ define([
Model.prototype._setOne = _.wrap(Model.prototype._setOne,
function(f, prop, newValue, currentValue, opts, ctrl) {
var i, l, args = Array.prototype.slice.call(arguments, 1),
inFlight = this._inFlight.save;
self = this, inFlight = self._inFlight.save;
if (opts.noclobber) {
if (this._changes[prop]) {
if (self._changes[prop]) {
ctrl.silent = true;
}
for (i = 0, l = inFlight.length; i < l; i++) {
Expand All @@ -515,15 +522,19 @@ define([
}
}
if (opts.validate) {
var field = this._fieldFromPropName(prop);
var field = self._fieldFromPropName(prop);
try {
field.validate(newValue);
field.validate(newValue, null, {
validateField: function(fieldName, value) {
self._validateOne(prop, value);
}
});
} catch (e) {
ctrl.error = e;
return;
}
}
return f.apply(this, args);
return f.apply(self, args);
});

ret = {Manager: Manager, Model: Model};
Expand Down
146 changes: 144 additions & 2 deletions js/tests/test_model_consistency.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
define([
'vendor/jquery',
'vendor/underscore',
'vendor/uuid',
'mesh/fields',
'./mockedexample',
'./mockednestedpolymorphicexample'
], function($, _, Example, NestedPolymorphicExample) {
], function($, _, uuid, fields, Example, NestedPolymorphicExample) {
var setup = function(options) {
var c, dfd = $.Deferred(),
Resource = options && options.resource? options.resource : Example;
Expand Down Expand Up @@ -425,7 +427,6 @@ define([
});
});

// TODO: make this pass
asyncTest('failing initial create', function() {
setup({noCollection: true}).then(function() {
var save1, save2, firstSaveCompleted,
Expand Down Expand Up @@ -1137,5 +1138,146 @@ define([
});
});

module('custom validations');

var _validateOneBase = {
id: uuid(),
name: 'name val',
required_field: 'required_field val',
structure_field: {
required_field: 123,
structure_field: {required_field: 456}
},
type: 'immutable'
},
_validateOneExpected = _.extend({
'null': _validateOneBase,
'structure_field.required_field': _validateOneBase.structure_field.required_field,
'structure_field.structure_field': _validateOneBase.structure_field.structure_field,
'structure_field.structure_field.required_field': _validateOneBase.structure_field.structure_field.required_field
}, _validateOneBase);

asyncTest('overriding _validateOne provides a hook for validating each field', function() {
setup({noCollection: true}).then(function(c) {
var validated, count = 0,
MyModel = NestedPolymorphicExample.extend({
_validateOne: function(prop, value) {
count++;
(validated = validated || {})[prop] = value;
}
});

MyModel(_validateOneBase).validate().then(function() {
deepEqual(validated, _validateOneExpected);
equal(count, 9);
start();
}, function(e) {
ok(false, 'validate should have succeeded');
console.log(e);
start();
});
});
});

asyncTest('throwing an error in validate one rejects validate call', function() {
setup({noCollection: true}).then(function() {
var MyModel = NestedPolymorphicExample.extend({
_validateOne: function(prop, value) {
if (prop === 'name') {
throw fields.InvalidTypeError('foobar');
}
}
});

MyModel(_validateOneBase).validate().then(function() {
ok(false, 'should have failed');
start();
}, function(e) {
deepEqual(e.serialize(), {
name: [{token: 'invalidtypeerror', message: 'foobar'}]
});
start();
});
});
});

asyncTest('polymorphic values work with _validateOne', function() {
setup({noCollection: true}).then(function() {
var m, MyModel = NestedPolymorphicExample.extend({
_validateOne: function(prop, value) {
if (prop === 'composition.expression') {
throw fields.InvalidTypeError('foobaz');
}
}
});

m = MyModel(_.extend({
composition: {
type: 'attribute-filter',
'expression': 'this " wont [ work AND'
}
}, _validateOneBase))

m.validate().then(function() {
ok(false, 'should have failed');
start();
}, function(e) {
deepEqual(e.serialize(), {
composition: [{
expression: [
{token: 'invalidtypeerror', message: 'foobaz'}
]
}]
});
m.set({
composition: {
type: 'datasource-list',
'datasources': [
{id: uuid(), name: 'some effin data source'}
]
}
});
m.del('composition.expression');

m.validate().then(function() {
start();
}, function(e) {
ok(false, 'should have succeeded');
console.log('second error:',e);
start();
});
});
});
});

asyncTest('validated set', function() {
setup({noCollection: true}).then(function(c) {
var MyModel = NestedPolymorphicExample.extend({
_validateOne: function(prop, value) {
if (prop === 'boolean_field') {
throw fields.InvalidTypeError('three');
}
}
});

MyModel(_validateOneBase).set({
name: 'foobar',
boolean_field: true
}, {validate: true}).then(function() {
ok(false, 'should have failed');
start();
}, function(changes, errors) {
deepEqual(changes, {name: true});
deepEqual(errors.serialize(), {
boolean_field: [
{token: 'invalidtypeerror', message: 'three'}
]
});
start();
});
});

});

start();
});

0 comments on commit 1837759

Please sign in to comment.