Skip to content

Commit

Permalink
#56 - Prevent errors when serializing objects with circular references
Browse files Browse the repository at this point in the history
  • Loading branch information
uglymunky committed May 6, 2014
1 parent b11f9a1 commit 2bcd646
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 12 deletions.
43 changes: 34 additions & 9 deletions src/backbone.siren.js
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,26 @@ _.extend(BbSiren, {
}


/**
* Wrapper for .toJSON()
*
* @param {*} val
* @param {Object} options
* @returns {*} A serialized version of the given val.
*/
, serializeData: function (val, options) {
options = options || {};

if (BbSiren.isHydratedObject(val)) {
if (_.indexOf(options.renderedEntities, val.url()) < 0 ) {
return val.toJSON(options);
}
} else {
return val;
}
}


/**
* Parses a raw siren model into a Backbone.Siren.Model and maintains its representation in the store.
*
Expand Down Expand Up @@ -875,10 +895,15 @@ _.extend(BbSiren, {
/**
* http://backbonejs.org/#Model-toJSON
*
* If passed an actionName, .toJSON() will only serialize the properties from the action's field's
*
* @param {Object} options
* @returns {Object}
*/
, toJSON: function (options) {
options = options || {};
options.renderedEntities = options.renderedEntities || [];

var action
, json = {}
, self = this;
Expand All @@ -887,19 +912,16 @@ _.extend(BbSiren, {
action = this.getActionByName(options.actionName);
}

options.renderedEntities.push(this.url());

if (action) {
_.each(action.fields, function (field) {
var val = self.get(field.name);

json[field.name] = BbSiren.isHydratedObject(val)
? val.toJSON({actionName: field.action})
: val;
options.actionName = field.action;
json[field.name] = BbSiren.serializeData(self.get(field.name), options);
});
} else {
_.each(this.attributes, function (val, name) {
json[name] = (val instanceof Backbone.Siren.Model) || (val instanceof Backbone.Siren.Collection)
? val.toJSON(options)
: val;
json[name] = BbSiren.serializeData(val, options);
});
}

Expand Down Expand Up @@ -1038,7 +1060,10 @@ _.extend(BbSiren, {
* @returns {Object}
*/
, toJSON: function (options) {
options = options || {};
options = options || {};
options.renderedEntities = options.renderedEntities || [];

options.renderedEntities.push(this.url());

// if (! options.isNestedBatch) { // @todo WIP
// delete options.actionName;
Expand Down
9 changes: 8 additions & 1 deletion test/spec/backbone.siren.collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,14 @@ describe('Siren Collection: ', function () {


it('returns an empty array if the collection does not have any matching attributes for the given action', function () {
var mySirenCollection = new Backbone.Siren.Collection({actions: [{name: 'do-test'}]});
var mySirenCollection = new Backbone.Siren.Collection({
actions: [{
name: 'do-test'
}]
, links: [
{rel: ['test'], href:'x.io'}
]
});

expect(mySirenCollection.toJSON({actionName: 'do-test'})).toEqual([]);
});
Expand Down
27 changes: 27 additions & 0 deletions test/spec/backbone.siren.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,33 @@ describe('Backbone.Siren: ', function () {
});


describe('.serializeData()', function () {
it('returns a serialized version of the model or collection', function () {
this.spy(Backbone.Siren.Model.prototype, 'toJSON');
this.spy(Backbone.Siren.Collection.prototype, 'toJSON');

var settingsModel = new Backbone.Siren.Model(rawSettingsModel);
Backbone.Siren.serializeData(settingsModel);
expect(settingsModel.toJSON).toHaveBeenCalled();

var collection = new Backbone.Siren.Collection(rawCollection);
Backbone.Siren.serializeData(collection);
expect(collection.toJSON).toHaveBeenCalled();
});


it('returns the value if it\'s not a Model or Collection', function () {
expect(Backbone.Siren.serializeData('two')).toBe('two');
});


it('returns `undefined` if val\'s already been rendered', function () {
var settingsModel = new Backbone.Siren.Model(rawSettingsModel);
expect(Backbone.Siren.serializeData(settingsModel, {renderedEntities: [settingsModel.url()]})).not.toBeDefined();
});
});


describe('.parseCollection()', function () {
it('creates a new collection from a raw collection', function () {
var collection = Backbone.Siren.parseCollection(rawCurrentCollection);
Expand Down
93 changes: 91 additions & 2 deletions test/spec/backbone.siren.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,65 @@ describe('Siren Model: ', function () {
, {"rel":["next"],"href": "http://api.x.io/orders/43"}
]
}
, sirenModel, store;

, rawCircularModel = {
// Level one
name: 'l1'
, properties: {
level: 'one'
}
, entities: [
// Level two
{
name: 'l2'
, properties: {
level: 'two'
}
, entities: [
// Level three - back to "one"
{
name: 'l3'
, properties: {
level: 'one'
}
, entities: [
// Level four - back to "two"
{
name: 'l4'
, properties: {
level: 'two'
}
, entities: []
, links: [
{rel: ['self'], href: 'x.io/two'}
]
}
]
, links: [
{rel: ['self'], href: 'x.io/one'}
]
}
, {
name: 'random'
, properties: {
random: 'yes'
}
, entities: []
, links: [
{rel: ['self'], href: 'x.io/random'}
]
}
]
, links: [
{rel: ['self'], href: 'x.io/two'}
]
}
]
, links: [
{rel: ['self'], href: 'x.io/one'}
]
}
, sirenModel, store;


beforeEach(function () {
Expand Down Expand Up @@ -342,7 +400,13 @@ describe('Siren Model: ', function () {
it('returns an empty object if the model does not have any matching attributes for the given action', function () {
// @todo should probably throw instead.

var mySirenModel = new Backbone.Siren.Model({actions: [{name: 'do-test'}]});
var mySirenModel = new Backbone.Siren.Model({actions: [{
name: 'do-test'
}]
, links: [
{rel: ['test'], href:'x.io'}
]
});

expect(mySirenModel.toJSON({actionName: 'do-test'})).toEqual({});
});
Expand All @@ -354,6 +418,31 @@ describe('Siren Model: ', function () {

expect(model.toJSON()).toEqual(props);
});


it('will only parse an entity once', function () {
var mySirenModel = new Backbone.Siren.Model(rawCircularModel);
var serializedData = mySirenModel.toJSON();

expect(serializedData.l2.random).toBeDefined();
expect(serializedData.l2.l3).not.toBeDefined();

// Nest the circular model one level deeper
mySirenModel = new Backbone.Siren.Model({
properties: {
level: 'top'
}
, entities: [rawCircularModel]
, links: [
{rel: ['self'], href: 'x.io/top'}
]
});

serializedData = mySirenModel.toJSON();

expect(serializedData.l1.l2.random).toBeDefined();
expect(serializedData.l1.l2.l3).not.toBeDefined();
});
});


Expand Down

0 comments on commit 2bcd646

Please sign in to comment.