Skip to content

Commit

Permalink
Merge pull request #24 from centro/subclass-associations
Browse files Browse the repository at this point in the history
Allows parent classes to resolve subclasses when loading data.
  • Loading branch information
burrows committed Mar 16, 2016
2 parents b706c1d + f13b9b2 commit fc662cb
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 2 deletions.
72 changes: 72 additions & 0 deletions spec/model_spec.js
Expand Up @@ -2955,4 +2955,76 @@ describe('Model', function () {
}, 100);
});
});

describe('subclassing', function() {
var Parent = Model.extend('Parent', function() {
this.getSubclass = function(attrs) {
return attrs.type;
};
});

var Child = Parent.extend('Child');
var AnotherChild = Parent.extend('AnotherChild');

var Composition = Model.extend('Composition', function() {
this.hasMany('things', 'Parent');
});

describe('.resolveSubclass', function() {
it('can resolve subclases', function() {
var subclass = Parent.resolveSubclass('Child');
expect(subclass).toBe(Child);
});

it('throws an exception when there are no subclasses', function() {
expect(()=> Child.resolveSubclass('Grandchild'))
.toThrow(new Error('Child.resolveSubclass: could not find subclass Grandchild'));
});

it('throws an exception when the subclasses could not be found', function() {
expect(()=> Parent.resolveSubclass('SomethingElse'))
.toThrow(new Error('Parent.resolveSubclass: could not find subclass SomethingElse'));
});
});

describe('loading subclasses', function() {
it('returns an instance of appropriate subclass', function() {
var loaded = Parent.load({id: 1, type: 'Child'});
expect(loaded instanceof Child).toBe(true);
});

it('loads associated subclasses', function() {
var loaded = Composition.load({
id: 1,
things: [
{
id: 'thing1',
type: 'Child'
},
{
id: 'thing2',
type: 'AnotherChild'
}
]
});

expect(loaded.things[0] instanceof Child).toBe(true);
expect(loaded.things[1] instanceof AnotherChild).toBe(true);
});

it('rejects non subclasses', function() {
expect(()=> {
Composition.load({
id: 2,
things: [
{
id: 'randomThing1',
type: 'SomethingElse'
}
]
});
}).toThrow();
})
});
});
});
25 changes: 23 additions & 2 deletions src/model.js
Expand Up @@ -175,9 +175,22 @@ var Model = TransisObject.extend(function() {

if (typeof f === 'function') { f.call(subclass); }

if(this.displayName !== 'Transis.Model') {
this.subclasses = this.subclasses || [];
this.subclasses.push(name);
}

return subclass;
};

this.resolveSubclass = function(name) {
if(!this.subclasses || this.subclasses.indexOf(name) < 0) {
throw new Error(`${this}.resolveSubclass: could not find subclass ${name}`);
}

return resolve(name);
};

// Public: Returns an empty instance of the model class. An empty instance contains only an id
// and must be retrieved from the mapper before any of its attributes will be available. Since the
// model's data mapper will likely need to perform an async action to retrieve data, this method
Expand Down Expand Up @@ -385,7 +398,7 @@ var Model = TransisObject.extend(function() {
// Returns the loaded model instance.
// Throws `Error` if the given attributes do not contain an `id` attribute.
this.load = function(attrs) {
var id = attrs.id, associations = this.prototype.associations, associated = {}, model;
var id = attrs.id, associations = this.prototype.associations, associated = {}, model, LoadKlass;

if (id == null) {
throw new Error(`${this}.load: an \`id\` attribute is required`);
Expand All @@ -394,7 +407,15 @@ var Model = TransisObject.extend(function() {
loads.push(true);

attrs = Object.assign({}, attrs);
model = IdMap.get(this, id) || new this;
if(typeof this.getSubclass === 'function') {
LoadKlass = this.resolveSubclass(this.getSubclass(attrs));
}
else {
LoadKlass = this;
}

model = IdMap.get(LoadKlass, id) || new LoadKlass;

delete attrs.id;

// extract associated attributes
Expand Down

0 comments on commit fc662cb

Please sign in to comment.