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

Fluent interface #73

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
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
70 changes: 59 additions & 11 deletions backbone.geppetto.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
},
Expand Down Expand Up @@ -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);
}
});
Expand All @@ -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)) {
Expand Down Expand Up @@ -236,6 +279,10 @@
});
},

wire: function(subject) {
return new Mapper(this, subject);
},

wireCommand: function wireCommand(eventName, CommandConstructor, wiring) {

var context = this;
Expand Down Expand Up @@ -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
Expand All @@ -293,17 +340,18 @@

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;
},

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
Expand Down Expand Up @@ -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));
Expand All @@ -343,7 +391,7 @@
}, this);
}
this.addPubSub(instance);
this._mapContextEvents(instance);
this._mapContextEvents(instance, contextEvents);
return this;
},

Expand Down
240 changes: 240 additions & 0 deletions specs/src/fluent-api-specs.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});

});
Loading