Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #360. Adds support for deep subModelType hierarchies. #362

Merged
merged 3 commits into from
Jul 23, 2013
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 65 additions & 30 deletions backbone-relational.js
Original file line number Diff line number Diff line change
@@ -1569,57 +1569,92 @@
* @return {Backbone.Model}
*/
build: function( attributes, options ) {
var model = this;

// 'build' is a possible entrypoint; it's possible no model hierarchy has been determined yet.
this.initializeModelHierarchy();

// Determine what type of (sub)model should be built if applicable.
// Lookup the proper subModelType in 'this._subModels'.
if ( this._subModels && this.prototype.subModelTypeAttribute in attributes ) {
var subModelTypeAttribute = attributes[ this.prototype.subModelTypeAttribute ];
var subModelType = this._subModels[ subModelTypeAttribute ];
var model = this._findSubModelType(this, attributes) || this;

return new model( attributes, options );
},

/**
* Determines what type of (sub)model should be built if applicable.
* Looks up the proper subModelType in 'this._subModels', recursing into
* types until a match is found. Returns the applicable 'Backbone.Model'
* or null if no match is found.
* @param {Backbone.Model} type
* @param {Object} attributes
* @return {Backbone.Model}
*/
_findSubModelType: function (type, attributes) {
if ( type._subModels && type.prototype.subModelTypeAttribute in attributes ) {
var subModelTypeAttribute = attributes[type.prototype.subModelTypeAttribute];
var subModelType = type._subModels[subModelTypeAttribute];
if ( subModelType ) {
model = subModelType;
return subModelType;
} else {
// Recurse into subModelTypes to find a match
for ( subModelTypeAttribute in type._subModels ) {
subModelType = this._findSubModelType(type._subModels[subModelTypeAttribute], attributes);
if ( subModelType ) {
return subModelType;
}
}
}
}

return new model( attributes, options );
return null;
},

/**
*
*/
initializeModelHierarchy: function() {
// If we're here for the first time, try to determine if this modelType has a 'superModel'.
if ( _.isUndefined( this._superModel ) || _.isNull( this._superModel ) ) {
Backbone.Relational.store.setupSuperModel( this );

// If a superModel has been found, copy relations from the _superModel if they haven't been
// inherited automatically (due to a redefinition of 'relations').
// Otherwise, make sure we don't get here again for this type by making '_superModel' false so we fail
// the isUndefined/isNull check next time.
if ( this._superModel && this._superModel.prototype.relations ) {
// Find relations that exist on the `_superModel`, but not yet on this model.
// Inherit any relations that have been defined in the parent model.
this.inheritRelations();

// If we came here through 'build' for a model that has 'subModelTypes' then try to initialize the ones that
// haven't been resolved yet.
if ( this.prototype.subModelTypes ) {
var resolvedSubModels = _.keys(this._subModels);
var unresolvedSubModels = _.omit(this.prototype.subModelTypes, resolvedSubModels);
_.each( unresolvedSubModels, function( subModelTypeName ) {
var subModelType = Backbone.Relational.store.getObjectByName( subModelTypeName );
subModelType && subModelType.initializeModelHierarchy();
});
}
},

inheritRelations: function() {
// Bail out if we've been here before.
if (!_.isUndefined( this._superModel ) && !_.isNull( this._superModel )) {
return;
}
// Try to initialize the _superModel.
Backbone.Relational.store.setupSuperModel( this );

// If a superModel has been found, copy relations from the _superModel if they haven't been inherited automatically
// (due to a redefinition of 'relations').
if ( this._superModel ) {
// The _superModel needs a chance to initialize its own inherited relations before we attempt to inherit relations
// from the _superModel. You don't want to call 'initializeModelHierarchy' because that could cause sub-models of
// this class to inherit their relations before this class has had chance to inherit it's relations.
this._superModel.inheritRelations();
if ( this._superModel.prototype.relations ) {
// Find relations that exist on the '_superModel', but not yet on this model.
var inheritedRelations = _.select( this._superModel.prototype.relations || [], function( superRel ) {
return !_.any( this.prototype.relations || [], function( rel ) {
return superRel.relatedModel === rel.relatedModel && superRel.key === rel.key;
}, this );
}, this );

this.prototype.relations = inheritedRelations.concat( this.prototype.relations );
}
else {
this._superModel = false;
}
}

// If we came here through 'build' for a model that has 'subModelTypes', and not all of them have been resolved yet, try to resolve each.
if ( this.prototype.subModelTypes && _.keys( this.prototype.subModelTypes ).length !== _.keys( this._subModels ).length ) {
_.each( this.prototype.subModelTypes || [], function( subModelTypeName ) {
var subModelType = Backbone.Relational.store.getObjectByName( subModelTypeName );
subModelType && subModelType.initializeModelHierarchy();
});
// Otherwise, make sure we don't get here again for this type by making '_superModel' false so we fail the
// isUndefined/isNull check next time.
else {
this._superModel = false;
}
},

173 changes: 165 additions & 8 deletions test/tests.js
Original file line number Diff line number Diff line change
@@ -1396,7 +1396,12 @@ $(document).ready(function() {
'carnivore': 'Carnivore'
}
});
scope.Primate = scope.Mammal.extend();
scope.Primate = scope.Mammal.extend({
subModelTypes: {
'human': 'Human'
}
});
scope.Human = scope.Primate.extend();
scope.Carnivore = scope.Mammal.extend();

var MammalCollection = AnimalCollection.extend({
@@ -1405,11 +1410,13 @@ $(document).ready(function() {

var mammals = new MammalCollection( [
{ id: 5, species: 'chimp', type: 'primate' },
{ id: 6, species: 'panther', type: 'carnivore' }
{ id: 6, species: 'panther', type: 'carnivore' },
{ id: 7, species: 'person', type: 'human' }
]);

ok( mammals.at( 0 ) instanceof scope.Primate );
ok( mammals.at( 1 ) instanceof scope.Carnivore );
ok( mammals.at( 2 ) instanceof scope.Human );
});

test( "Object building based on type, when used in relations" , function() {
@@ -1422,8 +1429,13 @@ $(document).ready(function() {
'dog': 'Dog'
}
});
var Dog = scope.Dog = PetAnimal.extend();
var Dog = scope.Dog = PetAnimal.extend({
subModelTypes: {
'poodle': 'Poodle'
}
});
var Cat = scope.Cat = PetAnimal.extend();
var Poodle = scope.Poodle = Dog.extend();

var PetPerson = scope.PetPerson = Backbone.RelationalModel.extend({
relations: [{
@@ -1445,21 +1457,30 @@ $(document).ready(function() {
{
type: 'cat',
name: 'Whiskers'
},
{
type: 'poodle',
name: 'Mitsy'
}
]
});

ok( petPerson.get( 'pets' ).at( 0 ) instanceof Dog );
ok( petPerson.get( 'pets' ).at( 1 ) instanceof Cat );
ok( petPerson.get( 'pets' ).at( 2 ) instanceof Poodle );

petPerson.get( 'pets' ).add({
petPerson.get( 'pets' ).add([{
type: 'dog',
name: 'Spot II'
});
},{
type: 'poodle',
name: 'Mitsy II'
}]);

ok( petPerson.get( 'pets' ).at( 2 ) instanceof Dog );
ok( petPerson.get( 'pets' ).at( 3 ) instanceof Dog );
ok( petPerson.get( 'pets' ).at( 4 ) instanceof Poodle );
});

test( "Automatic sharing of 'superModel' relations" , function() {
var scope = {};
Backbone.Relational.store.addModelScope( scope );
@@ -1484,6 +1505,10 @@ $(document).ready(function() {
scope.Flea = Backbone.RelationalModel.extend({});

scope.Dog = scope.PetAnimal.extend({
subModelTypes: {
'poodle': 'Poodle'
},

relations: [{
type: Backbone.HasMany,
key: 'fleas',
@@ -1493,22 +1518,154 @@ $(document).ready(function() {
}
}]
});
scope.Poodle = scope.Dog.extend();

var dog = new scope.Dog({
name: 'Spot'
});

var poodle = new scope.Poodle({
name: 'Mitsy'
});

var person = new scope.PetPerson({
pets: [ dog ]
pets: [ dog, poodle ]
});

ok( dog.get( 'owner' ) === person, "Dog has a working owner relation." );
ok( poodle.get( 'owner' ) === person, "Poodle has a working owner relation." );

var flea = new scope.Flea({
host: dog
});

var flea2 = new scope.Flea({
host: poodle
});

ok( dog.get( 'fleas' ).at( 0 ) === flea, "Dog has a working fleas relation." );
ok( poodle.get( 'fleas' ).at( 0 ) === flea2, "Poodle has a working fleas relation." );
});

test( "Initialization and sharing of 'superModel' reverse relations from a 'leaf' child model" , function() {
var scope = {};
Backbone.Relational.store.addModelScope( scope );
scope.PetAnimal = Backbone.RelationalModel.extend({
subModelTypes: {
'dog': 'Dog'
}
});

scope.Flea = Backbone.RelationalModel.extend({});
scope.Dog = scope.PetAnimal.extend({
subModelTypes: {
'poodle': 'Poodle'
},
relations: [{
type: Backbone.HasMany,
key: 'fleas',
relatedModel: scope.Flea,
reverseRelation: {
key: 'host'
}
}]
});
scope.Poodle = scope.Dog.extend();

// Define the PetPerson after defining all of the Animal models. Include the 'owner' as a reverse-relation.
scope.PetPerson = Backbone.RelationalModel.extend({
relations: [{
type: Backbone.HasMany,
key: 'pets',
relatedModel: scope.PetAnimal,
reverseRelation: {
type: Backbone.HasOne,
key: 'owner'
}
}]
});

// Initialize the models starting from the deepest descendant and working your way up to the root parent class.
var poodle = new scope.Poodle({
name: 'Mitsy'
});

var dog = new scope.Dog({
name: 'Spot'
});

var person = new scope.PetPerson({
pets: [ dog, poodle ]
});

ok( dog.get( 'owner' ) === person, "Dog has a working owner relation." );
ok( poodle.get( 'owner' ) === person, "Poodle has a working owner relation." );

var flea = new scope.Flea({
host: dog
});

var flea2 = new scope.Flea({
host: poodle
});

ok( dog.get( 'fleas' ).at( 0 ) === flea, "Dog has a working fleas relation." );
ok( poodle.get( 'fleas' ).at( 0 ) === flea2, "Poodle has a working fleas relation." );
});

test( "Initialization and sharing of 'superModel' reverse relations by adding to a polymorphic HasMany" , function() {
var scope = {};
Backbone.Relational.store.addModelScope( scope );
scope.PetAnimal = Backbone.RelationalModel.extend({
// The order in which these are defined matters for this regression test.
subModelTypes: {
'dog': 'Dog',
'fish': 'Fish'
}
});

// This looks unnecessary but it's for this regression test there has to be multiple subModelTypes.
scope.Fish = scope.PetAnimal.extend({});

scope.Flea = Backbone.RelationalModel.extend({});
scope.Dog = scope.PetAnimal.extend({
subModelTypes: {
'poodle': 'Poodle'
},
relations: [{
type: Backbone.HasMany,
key: 'fleas',
relatedModel: scope.Flea,
reverseRelation: {
key: 'host'
}
}]
});
scope.Poodle = scope.Dog.extend({});

// Define the PetPerson after defining all of the Animal models. Include the 'owner' as a reverse-relation.
scope.PetPerson = Backbone.RelationalModel.extend({
relations: [{
type: Backbone.HasMany,
key: 'pets',
relatedModel: scope.PetAnimal,
reverseRelation: {
type: Backbone.HasOne,
key: 'owner'
}
}]
});

// We need to initialize a model through the root-parent-model's build method by adding raw-attributes for a
// leaf-child-class to a polymorphic HasMany.
var person = new scope.PetPerson({
pets: [{
type: 'poodle',
name: 'Mitsy'
}]
});
var poodle = person.get('pets').first();
ok( poodle.get( 'owner' ) === person, "Poodle has a working owner relation." );
});

test( "Overriding of supermodel relations", function() {