From c12ef7272abe728615a06853ebb07527b3d6b2eb Mon Sep 17 00:00:00 2001 From: Alex Raginskiy Date: Wed, 16 Mar 2016 01:10:29 -0500 Subject: [PATCH 1/3] Allows parent classes to resolve subclasses when loading data. --- spec/model_spec.js | 77 ++++++++++++++++++++++++++++++++++++++++++++++ src/model.js | 24 +++++++++++++-- 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/spec/model_spec.js b/spec/model_spec.js index d02608b..4974f9d 100644 --- a/spec/model_spec.js +++ b/spec/model_spec.js @@ -2955,4 +2955,81 @@ describe('Model', function () { }, 100); }); }); + + describe('subclassing', function() { + var Parent = Model.extend('Parent', function() { + this.prototype.init = ()=> {}; + + this.getSubclass = function(attrs) { + return attrs.type; + }; + }); + + var Child = Parent.extend('Child', function() { + this.prototype.init = ()=> {}; + }); + + var AnotherChild = Parent.extend('AnotherChild', function() { + this.prototype.init = ()=> {}; + }); + + 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: Child does not have subclasses')); + }); + + 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(); + }) + }); + }); }); diff --git a/src/model.js b/src/model.js index 87437fb..de038cd 100644 --- a/src/model.js +++ b/src/model.js @@ -175,9 +175,21 @@ 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) { throw new Error(`${this}.resolveSubclass: ${this} does not have subclasses`); } + if(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 @@ -385,7 +397,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`); @@ -394,7 +406,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 From 66afe489957b9840f8cd12ad4076365bd120e24c Mon Sep 17 00:00:00 2001 From: Alex Raginskiy Date: Wed, 16 Mar 2016 11:26:15 -0500 Subject: [PATCH 2/3] simplified resolveSubclass error handling --- spec/model_spec.js | 6 ++++-- src/model.js | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/spec/model_spec.js b/spec/model_spec.js index 4974f9d..768aaa7 100644 --- a/spec/model_spec.js +++ b/spec/model_spec.js @@ -2985,11 +2985,13 @@ describe('Model', function () { }); it('throws an exception when there are no subclasses', function() { - expect(()=> Child.resolveSubclass('Grandchild')).toThrow(new Error('Child.resolveSubclass: Child does not have subclasses')); + 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')); + expect(()=> Parent.resolveSubclass('SomethingElse')) + .toThrow(new Error('Parent.resolveSubclass: could not find subclass SomethingElse')); }); }); diff --git a/src/model.js b/src/model.js index de038cd..9973464 100644 --- a/src/model.js +++ b/src/model.js @@ -184,8 +184,9 @@ var Model = TransisObject.extend(function() { }; this.resolveSubclass = function(name) { - if(!this.subclasses) { throw new Error(`${this}.resolveSubclass: ${this} does not have subclasses`); } - if(this.subclasses.indexOf(name) < 0) { throw new Error(`${this}.resolveSubclass: could not find subclass ${name}`); } + if(!this.subclasses || this.subclasses.indexOf(name) < 0) { + throw new Error(`${this}.resolveSubclass: could not find subclass ${name}`); + } return resolve(name); }; From f13b9b28e3af916cb502ef34543f9d74d0a52c95 Mon Sep 17 00:00:00 2001 From: Alex Raginskiy Date: Wed, 16 Mar 2016 13:13:28 -0500 Subject: [PATCH 3/3] removed unnecessary inits from subclass tests --- spec/model_spec.js | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/spec/model_spec.js b/spec/model_spec.js index 768aaa7..b2665c5 100644 --- a/spec/model_spec.js +++ b/spec/model_spec.js @@ -2958,26 +2958,18 @@ describe('Model', function () { describe('subclassing', function() { var Parent = Model.extend('Parent', function() { - this.prototype.init = ()=> {}; - this.getSubclass = function(attrs) { return attrs.type; }; }); - var Child = Parent.extend('Child', function() { - this.prototype.init = ()=> {}; - }); - - var AnotherChild = Parent.extend('AnotherChild', function() { - this.prototype.init = ()=> {}; - }); + 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'); @@ -3015,6 +3007,7 @@ describe('Model', function () { } ] }); + expect(loaded.things[0] instanceof Child).toBe(true); expect(loaded.things[1] instanceof AnotherChild).toBe(true); });