diff --git a/js/fields.js b/js/fields.js index fa235c5..3b7f214 100644 --- a/js/fields.js +++ b/js/fields.js @@ -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) { @@ -168,6 +168,9 @@ define([ if (mimetype) { value = this.serialize(value, mimetype, true); } + if (options && options.validateField) { + options.validateField(null, value); + } return value; }, @@ -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); @@ -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]; diff --git a/js/model.js b/js/model.js index 6dce619..07790be 100644 --- a/js/model.js +++ b/js/model.js @@ -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); } @@ -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, { @@ -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++) { @@ -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}; diff --git a/js/tests/test_model_consistency.js b/js/tests/test_model_consistency.js index e82225f..59bccb2 100644 --- a/js/tests/test_model_consistency.js +++ b/js/tests/test_model_consistency.js @@ -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; @@ -425,7 +427,6 @@ define([ }); }); - // TODO: make this pass asyncTest('failing initial create', function() { setup({noCollection: true}).then(function() { var save1, save2, firstSaveCompleted, @@ -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(); });