From ad3280ae1785cb15e319212bb421be15bc7c9dc4 Mon Sep 17 00:00:00 2001 From: Camille Reynders Date: Wed, 24 Sep 2014 21:07:19 +0200 Subject: [PATCH] Fluent API first sketch --- backbone.geppetto.js | 70 ++++++++-- specs/src/fluent-api-specs.js | 240 ++++++++++++++++++++++++++++++++++ specs/src/resolver-specs.js | 14 +- specs/test-main.js | 3 +- 4 files changed, 309 insertions(+), 18 deletions(-) create mode 100644 specs/src/fluent-api-specs.js diff --git a/backbone.geppetto.js b/backbone.geppetto.js index fe89ee1..4f279c6 100755 --- a/backbone.geppetto.js +++ b/backbone.geppetto.js @@ -72,6 +72,48 @@ } }; + function Configurator(mapping) { + this.mapping = mapping; + } + _.extend(Configurator.prototype, { + withWiring: function(wiring) { + this.mapping.wiring = wiring; + return this; + }, + withContextEvents: function(contextEvents) { + this.mapping.contextEvents = contextEvents; + return this; + }, + withParameters: function() { + this.mapping.params = _.toArray(arguments); + return this; + } + }); + + function Mapper(context, subject) { + this.context = context; + this.subject = subject; + } + + _.extend(Mapper.prototype, { + asSingleton: function(key) { + this.context.wireSingleton(key, this.subject); + return new Configurator(this.context._mappings[key]); + }, + asValue: function(key) { + this.context.wireValue(key, this.subject); + return new Configurator(this.context._mappings[key]); + }, + asClass: function(key) { + this.context.wireClass(key, this.subject); + return new Configurator(this.context._mappings[key]); + }, + asView: function(key) { + this.context.wireView(key, this.subject); + return new Configurator(this.context._mappings[key]); + } + }); + var Geppetto = {}; Geppetto.version = '0.7.1'; @@ -134,7 +176,7 @@ instance = new config.clazz(); } if (!instance.initialize) { - this.resolve(instance, config.wiring); + this.resolve(instance, config.wiring, config.contextEvents); } return instance; }, @@ -163,12 +205,12 @@ return output; }, - _wrapConstructor: function(clazz, wiring) { + _wrapConstructor: function(key, clazz) { var context = this; if (clazz.extend) { return clazz.extend({ constructor: function() { - context.resolve(this, wiring); + context.resolve(this, context._mappings[key].wiring, context._mappings[key].contextEvents); clazz.prototype.constructor.apply(this, arguments); } }); @@ -177,8 +219,9 @@ } }, - _mapContextEvents: function(obj) { - _.each(obj.contextEvents, function(callback, eventName) { + _mapContextEvents: function(obj, contextEvents) { + var actualEvents = contextEvents || obj.contextEvents; + _.each(actualEvents, function(callback, eventName) { if (_.isFunction(callback)) { this.listen(obj, eventName, callback); } else if (_.isString(callback)) { @@ -236,6 +279,10 @@ }); }, + wire: function(subject) { + return new Mapper(this, subject); + }, + wireCommand: function wireCommand(eventName, CommandConstructor, wiring) { var context = this; @@ -283,7 +330,7 @@ wireClass: function(key, clazz, wiring) { this._mappings[key] = { - clazz: this._wrapConstructor(clazz, wiring), + clazz: this._wrapConstructor(key, clazz), object: null, type: TYPES.OTHER, wiring: wiring @@ -293,9 +340,10 @@ wireView: function(key, clazz, wiring) { this._mappings[key] = { - clazz: createFactory(this._wrapConstructor(clazz, wiring)), + clazz: createFactory(this._wrapConstructor(key, clazz)), object: null, - type: TYPES.VIEW + type: TYPES.VIEW, + wiring: wiring }; return this; }, @@ -303,7 +351,7 @@ wireSingleton: function(key, clazz, wiring) { this._mappings[key] = { - clazz: this._wrapConstructor(clazz, wiring), + clazz: this._wrapConstructor(key, clazz), object: null, type: TYPES.SINGLETON, wiring: wiring @@ -334,7 +382,7 @@ return this._retrieveFromCacheOrCreate(key, true); }, - resolve: function(instance, wiring) { + resolve: function(instance, wiring, contextEvents) { wiring = wiring || instance.wiring; if (wiring) { var propNameArgIndex = Number(!_.isArray(wiring)); @@ -343,7 +391,7 @@ }, this); } this.addPubSub(instance); - this._mapContextEvents(instance); + this._mapContextEvents(instance, contextEvents); return this; }, diff --git a/specs/src/fluent-api-specs.js b/specs/src/fluent-api-specs.js new file mode 100644 index 0000000..b56a601 --- /dev/null +++ b/specs/src/fluent-api-specs.js @@ -0,0 +1,240 @@ +/* suppress jshint warnings for chai syntax - https://github.com/chaijs/chai/issues/41#issuecomment-14904150 */ +/* jshint -W024 */ +/* jshint expr:true */ +define([ + "underscore", "backbone", "geppetto" +], function(_, Backbone, Geppetto) { + var expect = chai.expect; + + describe("Backbone.Geppetto fluent API", function() { + var context; + beforeEach(function() { + context = new Geppetto.Context(); + }); + afterEach(function() { + context.destroy(); + context = undefined; + }); + describe("when retrieving objects", function() { + it("should poll the parent if no corresponding mapping was found", function(){ + var value = {}; + var child = new Geppetto.Context({ + parentContext : context + }); + context.wire(value).asValue('value'); + var actual = child.getObject('value'); + expect(actual).to.equal(value); + }); + }); + + describe("when mapping a singleton", function() { + var key = 'a singleton'; + var foo = {}; + var contextEventSpy = sinon.spy(); + var SingletonClass = function() {}; + _.extend(SingletonClass.prototype, Backbone.Events); + SingletonClass.prototype.contextEvents = { + }; + beforeEach(function() { + context.wire(foo).asValue('foo'); + context.wire(SingletonClass) + .asSingleton(key) + .withWiring({ + foo: 'foo' + }) + .withContextEvents({ + "event:foo" : function(){ + contextEventSpy(); + } + }); + }); + it('should be determinable', function() { + expect(context.hasWiring(key)).to.be.true; + }); + it('should produce an instance of the mapped class', function() { + var actual = context.getObject(key); + expect(actual).to.be.an.instanceOf(SingletonClass); + }); + it('should produce a single, unique instance', function() { + var first = context.getObject(key); + var second = context.getObject(key); + expect(second).to.equal(first); + }); + it("should be instantiatable by brute force", function() { + var first = context.getObject(key); + var second = context.instantiate(key); + expect(second).to.not.equal(first); + }); + it("should be injected with its dependencies when instantiated", function() { + var actual = context.getObject(key); + expect(actual.foo).to.equal(foo); + }); + it("should optionally allow wiring configuration", function() { + var dependerClass = function() {}; + context.wire(dependerClass) + .asSingleton('depender') + .withWiring( { + dependency: key + }); + var depender = context.getObject('depender'); + expect(depender.dependency).to.equal(context.getObject(key)); + }); + it("should map context events when configured", function(){ + var actual = context.getObject(key); + context.dispatch('event:foo'); + expect(contextEventSpy).to.have.been.called; + }); + }); + describe("when mapping a value", function() { + var key = 'a value'; + var value = {}; + beforeEach(function() { + context.wire(value ).asValue(key); + }); + it('should be determinable', function() { + expect(context.hasWiring(key)).to.be.true; + }); + it("should be retrievable", function() { + expect(context.getObject(key)).to.equal(value); + }); + it("it should always return the same value", function() { + var first = context.getObject(key); + var second = context.getObject(key); + expect(second).to.equal(first); + }); + }); + describe("when mapping a class", function() { + var key = 'a class'; + var clazz = function() {}; + clazz.prototype.wiring = ['foo']; + var foo = {}; + var contextEventSpy = sinon.spy(); + _.extend(clazz.prototype, Backbone.Events); + beforeEach(function() { + context.wire(foo ).asValue('foo'); + context.wire(clazz) + .asClass(key) + .withWiring({ + foo : "foo" + }) + .withContextEvents({ + "event:foo" : function(){ + contextEventSpy(); + } + }); + }); + it('should be determinable', function() { + expect(context.hasWiring(key)).to.be.true; + }); + it('should produce an instance of the mapped class', function() { + var actual = context.getObject(key); + expect(actual).to.be.an.instanceOf(clazz); + }); + it('should produce a new instance every time', function() { + var first = context.getObject(key); + var second = context.getObject(key); + expect(second).to.not.equal(first); + }); + it("should be injected with its dependencies when instantiated", function() { + var actual = context.getObject(key); + expect(actual.foo).to.equal(foo); + }); + it("should optionally allow wiring configuration", function() { + var dependerClass = function() {}; + context.wire(dependerClass) + .asClass('depender') + .withWiring({ + dependency: key + }); + var depender = context.getObject('depender'); + expect(depender.dependency).to.be.an.instanceOf(clazz); + }); + it("should map context events when instantiated", function(){ + var actual = context.getObject(key); + context.dispatch('event:foo'); + expect(contextEventSpy).to.have.been.called; + }); + }); + describe("when mapping a view", function() { + var key = 'a class'; + var clazz; + var foo = {}; + var contextEventSpy = sinon.spy(); + beforeEach(function() { + contextEventSpy.reset(); + clazz = Backbone.View.extend(); + + context.wire(clazz) + .asView(key) + .withWiring({ + foo: 'foo' + }) + .withContextEvents({ + "event:foo" : function(){ + contextEventSpy(); + } + }); + context.wire(foo).asValue('foo'); + }); + it('should be determinable', function() { + expect(context.hasWiring(key)).to.be.true; + }); + it('should extend the view constructor', function() { + var actual = context.getObject(key); + expect(actual).to.be.a("function"); + }); + it('should retrieve the same class every time', function() { + var first = context.getObject(key); + var second = context.getObject(key); + expect(second).to.equal(first); + }); + it("should call the view's original 'initialize' function when instantiated", function() { + var initializeSpy = sinon.spy(); + expect(initializeSpy).not.to.have.been.called; + clazz.prototype.initialize = function() { + initializeSpy(); + }; + var ViewConstructor = context.getObject(key); + var viewInstance = new ViewConstructor(); + expect(initializeSpy).to.have.been.calledOnce; + }); + it("should be injected with its dependencies when instantiated", function() { + var ViewConstructor = context.getObject(key); + var viewInstance = new ViewConstructor(); + expect(viewInstance.foo).to.equal(foo); + }); + it("should map context events when instantiated", function(){ + var ViewCtor = context.getObject(key); + var view = new ViewCtor(); + context.dispatch('event:foo'); + expect(contextEventSpy).to.have.been.called; + }); + }); + describe('when configuring wirings', function(){ + var key = "key"; + var passed; + var ctor = function(){ + passed = _.toArray(arguments); + }; + var payload = {}; + var a = {}; + var b = {}; + beforeEach(function(){ + passed = null; + context.wire(ctor) + .asClass(key) + .withParameters(payload, a, b) + }); + afterEach(function(){ + context.destroy(); + }); + it('should pass all arguments as payload to the constructor function', function(){ + context.getObject(key); + expect(passed[0]).to.equal(payload); + expect(passed[1]).to.equal(a); + expect(passed[2]).to.equal(b); + }); + }); + }); + +}); diff --git a/specs/src/resolver-specs.js b/specs/src/resolver-specs.js index e7dbbae..b5c7bd0 100644 --- a/specs/src/resolver-specs.js +++ b/specs/src/resolver-specs.js @@ -1,3 +1,5 @@ + + /* suppress jshint warnings for chai syntax - https://github.com/chaijs/chai/issues/41#issuecomment-14904150 */ /* jshint -W024 */ /* jshint expr:true */ @@ -356,17 +358,17 @@ define([ it("should allow wrapped constructor to handle initialization parameters in similar fashion as unwrapped constructor)", function() { var obj1 = {value: 'foo'}; var obj2 = {value: 'bar'}; - var clazz = Backbone.Model.extend({ + var clazz = Backbone.View.extend({ initialize: function (obj1, obj2) { this.obj1 = obj1; this.obj2 = obj2; } }); - var originalModel = new clazz(obj1, obj2); - var wrappedClazz = context._wrapConstructor(clazz, null); - var wrappedModel = new wrappedClazz(obj1, obj2); - expect(originalModel.obj1).to.eql(wrappedModel.obj1); - expect(originalModel.obj2).to.eql(wrappedModel.obj2) + context.wireView('View', clazz); + var View = context.getObject('View'); + var view = new View(obj1, obj2); + expect(view.obj1).to.equal(obj1); + expect(view.obj2).to.equal(obj2); }); }); diff --git a/specs/test-main.js b/specs/test-main.js index 8f27a22..0ab18f9 100755 --- a/specs/test-main.js +++ b/specs/test-main.js @@ -26,7 +26,8 @@ require([ "backbone", "geppetto", "src/geppetto-specs", - "src/resolver-specs" + "src/resolver-specs", + "src/fluent-api-specs" ], function() { chai.Assertion.includeStack = true;