diff --git a/.travis.yml b/.travis.yml index 1589455..593bda9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: node_js node_js: - 0.10.18 before_install: + - npm install -g buster@0.7.8 - npm install -g grunt-cli - npm install -g bower after_script: diff --git a/examples/demo.js b/examples/demo.js new file mode 100644 index 0000000..8c76bb1 --- /dev/null +++ b/examples/demo.js @@ -0,0 +1,13 @@ +var sirenApi = new Backbone.Siren('root.url', options); + +sirenApi.resolve('team/123'); + +// How can I subscribe to changes to this model? +// Do you subscribe to changes to a model or changes to a "channel" +// Does each push notification resolve to a url endpoint? + +// Are push notifications fundamentally different from http requests (e.g., is there a 1 to 1 relationship between a notification and a url endpoint) + +// would this be a "batch" operation? + // If subscribed to a "loans" channel this is easy because you can get a collection of loans + // If listening to changes in general, for all models this would require some concept of Batch (how would we implement the long polling fallback?) \ No newline at end of file diff --git a/package.json b/package.json index 202de40..1fe83eb 100644 --- a/package.json +++ b/package.json @@ -24,13 +24,13 @@ "license": "BSD", "readmeFilename": "readme.md", "devDependencies": { - "buster": "~0.7.8", - "buster-coverage": "~0.1.0", - "coveralls": "~2.6.1", - "grunt": "~0.4.1", - "grunt-buster": "~0.3.1", - "grunt-contrib-jshint": "~0.7.2", - "grunt-contrib-uglify": "~0.2.7", - "grunt-rigger": "~0.5.0" + "buster": "0.7.8", + "buster-coverage": "0.1.0", + "coveralls": "2.6.1", + "grunt": "0.4.1", + "grunt-buster": "0.3.1", + "grunt-contrib-jshint": "0.7.2", + "grunt-contrib-uglify": "0.2.7", + "grunt-rigger": "0.5.0" } } diff --git a/src/backbone.siren.action.js b/src/backbone.siren.action.js index f8437a4..c581d54 100644 --- a/src/backbone.siren.action.js +++ b/src/backbone.siren.action.js @@ -212,7 +212,7 @@ Action.prototype = { }); } - options = _.extend(presets, options); + options = _.extend(presets, parent.siren.ajaxOptions || {}, options); attributes = _.extend(parent.toJSON({actionName: this.name}), attributes); // Note that .save() can return false in the case of failed validation. diff --git a/src/backbone.siren.js b/src/backbone.siren.js index db6691d..a478e56 100644 --- a/src/backbone.siren.js +++ b/src/backbone.siren.js @@ -227,6 +227,7 @@ function match(filters) { * @todo This only works with rel links that are requests to the API. * There will be times when a rel points to a resource outside of the API and that needs to be thought through * @todo This method leaves much to be desired and should be refactored. + * @todo might be more useful if it checks if the link is to a sirenEntity? If so, resolves it? (still need to put some thought into this) * * @param {String} rel * @returns {Promise} @@ -237,11 +238,9 @@ function request(rel) { return _.indexOf(link.rel, rel) > -1; }); - if (! link) { - return; + if (link) { + return BbSiren.resolveOne(link.href); } - - return BbSiren.resolveOne(link.href); } @@ -256,7 +255,7 @@ function request(rel) { * @returns {Promise} */ function resolve(options) { - options = options || {}; + options = $.extend(this.siren.ajaxOptions || {}, options); var deferred = new $.Deferred(); @@ -270,6 +269,7 @@ function resolve(options) { // Its already been hydrated deferred.resolve(this); } else if (options.url) { + // This option allows us to defer hydration of our model or collection with the url provided // Very much like .fetch() only it adds support for chaining nested entities @@ -342,6 +342,7 @@ function resolveNextInChain(chain, options) { /** * Is called in the Model or Collection's constructor. * It creates a Backbone.Siren.Action instance from a raw action and attaches it to the Model or Collection (aka "parent"). + * @todo, This should probably be a public, static method on BbSiren. * * @returns {Array|undefined} */ @@ -438,21 +439,20 @@ _.extend(BbSiren, { * Creates a Backbone.Siren model, collection, or error from a Siren object * * @param {Object} rawEntity + * @param {Object} options * @returns {Backbone.Siren.Model|Backbone.Siren.Collection|Backbone.Siren.Error} */ - , parse: function (rawEntity, store) { - var bbSiren; + , parse: function (rawEntity, options) { + options = options || {}; if (BbSiren.isRawCollection(rawEntity)) { - bbSiren = new Backbone.Siren.Collection(rawEntity, {store: store}); + return new Backbone.Siren.Collection(rawEntity, options); } else if (BbSiren.isRawError(rawEntity)) { // @todo how should we represent errors? For now, treat them as regular Models... - bbSiren = new Backbone.Siren.Model(rawEntity, {store: store}); + return new Backbone.Siren.Model(rawEntity, options); } else { - bbSiren = new Backbone.Siren.Model(rawEntity, {store: store}); + return new Backbone.Siren.Model(rawEntity, options); } - - return bbSiren; } @@ -542,6 +542,7 @@ _.extend(BbSiren, { * @param {String} url * @param {Object} options * @param {Object} options.store - store instance @todo remove the need to have this parameter + * @todo - add an options.ajaxOptions parameter. */ , resolveOne: function (url, options) { options = options || {}; @@ -605,7 +606,7 @@ _.extend(BbSiren, { BbSiren.ajax(rootUrl, options) .done(function (rawEntity) { - var bbSiren = BbSiren.parse(rawEntity, store); + var bbSiren = BbSiren.parse(rawEntity, options); deferred.resolve(bbSiren); options.deferred = chainedDeferred; @@ -620,7 +621,7 @@ _.extend(BbSiren, { entity = {}; } - bbSiren = BbSiren.parse(entity, store); + bbSiren = BbSiren.parse(entity, options); deferred.reject(bbSiren, jqXhr); chainedDeferred.reject(bbSiren, jqXhr); }); @@ -686,7 +687,7 @@ _.extend(BbSiren, { deferred.resolve(self.setEntity(bbSiren, rawEntity.rel, getRawEntityName(rawEntity))); }); } else { - bbSiren = BbSiren.parse(rawEntity, options.store); + bbSiren = BbSiren.parse(rawEntity, options); bbSirenPromise = deferred.resolve(this.setEntity(bbSiren, rawEntity.rel, getRawEntityName(rawEntity))); } @@ -718,9 +719,6 @@ _.extend(BbSiren, { } - - - /** * http://backbonejs.org/#Model-parse * @@ -820,6 +818,23 @@ _.extend(BbSiren, { options.parse = true; // Force "parse" to be called on instantiation: http://stackoverflow.com/questions/11068989/backbone-js-using-parse-without-calling-fetch/14950519#14950519 Backbone.Model.call(this, sirenObj, options); + + this.siren = {}; + + // the store + if (options.store) { + this.siren.store = options.store; + } + + // entity options + if (options.ajaxOptions) { + this.siren.ajaxOptions = options.ajaxOptions; + } + + if (options.apiRoot) { + this.siren.apiRoot = options.apiRoot; + } + this.parseActions(); } @@ -944,6 +959,17 @@ _.extend(BbSiren, { options.parse = true; // Force "parse" to be called on instantiation: http://stackoverflow.com/questions/11068989/backbone-js-using-parse-without-calling-fetch/14950519#14950519 Backbone.Collection.call(this, sirenObj, options); + + this.siren = {}; + + if (options.store) { + this.siren.store = options.store; + } + + if (options.ajaxOptions) { + this.siren.ajaxOptions = options.ajaxOptions; + } + this.parseActions(); } }) @@ -983,6 +1009,7 @@ BbSiren.prototype = { , resolve: function (entityPaths, options) { options = $.extend({}, this.options, options); options.store = this.store; + options.apiRoot = this.apiRoot; var self = this , urls = []; diff --git a/test/spec/backbone.siren.action.js b/test/spec/backbone.siren.action.js index cfcc511..bc87643 100644 --- a/test/spec/backbone.siren.action.js +++ b/test/spec/backbone.siren.action.js @@ -7,11 +7,11 @@ describe('Siren Action: ', function () { // @todo quick fix for upgrade to buster 0.7 var expect = buster.expect; - var sirenAction = {name: 'add-item', 'class': ['fuzzy', 'fluffy'], title: 'Add Item', method: 'FANCY', href: 'http://api.x.io/orders/42/items', type: 'application/x-fancy-stuff', fields: [{name: 'orderNumber', type: 'hidden', value: '42'}, {name: 'productCode', type: 'text'}, {name: 'quantity', type: 'number' }]} - , bbSirenAction; + var sirenAction, bbSirenAction; beforeEach(function () { + sirenAction = {name: 'add-item', 'class': ['fuzzy', 'fluffy'], title: 'Add Item', method: 'FANCY', href: 'http://api.x.io/orders/42/items', type: 'application/x-fancy-stuff', fields: [{name: 'orderNumber', type: 'hidden', value: '42'}, {name: 'productCode', type: 'text'}, {name: 'quantity', type: 'number' }]}; bbSirenAction = new Backbone.Siren.Action(sirenAction); }); @@ -96,6 +96,21 @@ describe('Siren Action: ', function () { }); + it('merges the siren.ajaxOptions onto each call', function () { + var mySirenModel = {href: 'test', actions: [sirenAction]} + , myBbSirenModel = new Backbone.Siren.Model(mySirenModel, {ajaxOptions: {type: 'FANCIER', contentType: 'application/x-aaah-shite'}}); + + myBbSirenModel.getActionByName('add-item').execute(); + expect($.ajax).toHaveBeenCalledWith(sinon.match({url: 'http://api.x.io/orders/42/items', type: 'FANCIER', contentType: 'application/x-aaah-shite'})); + + $.ajax.reset(); + + // Override + myBbSirenModel.getActionByName('add-item').execute({type: 'FANCIEST'}); + expect($.ajax).toHaveBeenCalledWith(sinon.match({url: 'http://api.x.io/orders/42/items', type: 'FANCIEST', contentType: 'application/x-aaah-shite'})); + }); + + it('returns undefined if there is no parent to the action', function () { expect(bbSirenAction.execute()).not.toBeDefined(); }); diff --git a/test/spec/backbone.siren.collection.js b/test/spec/backbone.siren.collection.js index 07c064e..a8c8f4e 100644 --- a/test/spec/backbone.siren.collection.js +++ b/test/spec/backbone.siren.collection.js @@ -115,6 +115,19 @@ describe('Siren Collection: ', function () { }); + describe('.resolve()', function () { + it('merges siren.ajaxOptions onto each each call', function () { + var options = {forceFetch: true, type: 'blah'}; + + this.stub(sirenCollection, 'fetch'); + sirenCollection.siren.ajaxOptions = {dataType: 'json'}; + sirenCollection.resolve(options); + + expect(sirenCollection.fetch).toHaveBeenCalledWith(sinon.match({forceFetch: true, type: 'blah', dataType: 'json'})); + }); + }); + + describe('.hasClass()', function () { it('returns whether a collection has a given class', function () { expect(sirenCollection.hasClass('wtf')).toBe(false); @@ -443,6 +456,31 @@ describe('Siren Collection: ', function () { }); + + + describe('.siren', function () { + it('is an object that is set on each BbSiren Collection upon instantiation', function () { + var myCollection = new Backbone.Siren.Model(); + expect(myCollection.siren).toBeObject(); + }); + + + it('has a store if provided via the options', function () { + var myCollection = new Backbone.Siren.Collection({href: 'blah'}, {store: new Backbone.Siren.Store()}); + expect(myCollection.siren.store).toBeObject(); + }); + + + it('has a ajaxOptions if provided via the options', function () { + var ajaxOptions = {data: {blah: true}, type: 'json'} + , myCollection = new Backbone.Siren.Collection({href: 'blah'}, {ajaxOptions: ajaxOptions}); + + expect(myCollection.siren.ajaxOptions).toBeObject(); + expect(myCollection.siren.ajaxOptions).toEqual(ajaxOptions); + }); + }); + + it('Adds siren sub-entities as models to a Backbone Collection\'s models property', function () { expect(sirenCollection.models).toBeDefined(); expect(sirenCollection.models.length).toBe(3); diff --git a/test/spec/backbone.siren.model.js b/test/spec/backbone.siren.model.js index 48fdb3b..ed344c2 100644 --- a/test/spec/backbone.siren.model.js +++ b/test/spec/backbone.siren.model.js @@ -122,6 +122,19 @@ describe('Siren Model: ', function () { }); + describe('.resolve()', function () { + it('merges siren.ajaxOptions onto each each call', function () { + var options = {forceFetch: true, type: 'blah'}; + + this.stub(sirenModel, 'fetch'); + sirenModel.siren.ajaxOptions = {dataType: 'json'}; + sirenModel.resolve(options); + + expect(sirenModel.fetch).toHaveBeenCalledWith(sinon.match({forceFetch: true, type: 'blah', dataType: 'json'})); + }); + }); + + describe('.hasClass()', function () { it('returns whether a model has a given class', function () { expect(sirenModel.hasClass('wtf')).toBeFalse(); @@ -530,6 +543,29 @@ describe('Siren Model: ', function () { }); + describe('.siren', function () { + it('is an object that is set each BbSiren Model upon instantiation', function () { + var myModel = new Backbone.Siren.Model(); + expect(myModel.siren).toBeObject(); + }); + + + it('has a store if provided via the options', function () { + var myModel = new Backbone.Siren.Model({href: 'blah'}, {store: new Backbone.Siren.Store()}); + expect(myModel.siren.store).toBeObject(); + }); + + + it('has a ajaxOptions if provided via the options', function () { + var ajaxOptions = {data: {blah: true}, type: 'json'} + , myModel = new Backbone.Siren.Model({href: 'blah'}, {ajaxOptions: ajaxOptions}); + + expect(myModel.siren.ajaxOptions).toBeObject(); + expect(myModel.siren.ajaxOptions).toEqual(ajaxOptions); + }); + }); + + it('sets a Backbone Model\'s "attributes" hash to the siren "properties"', function () { expect(sirenModel.attributes).toMatch(settingsModelSiren.properties); });