summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gruntfile.js47
-rw-r--r--injector-tests.js445
-rw-r--r--injector.js507
-rw-r--r--injector.min.js2
-rw-r--r--karma.conf.js25
-rw-r--r--package.json32
6 files changed, 759 insertions, 299 deletions
diff --git a/Gruntfile.js b/Gruntfile.js
index c09bdde..e2febff 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -27,25 +27,33 @@ module.exports = function (grunt) {
},
watch: {
options: {
- livereload: true
+ spawn: false,
+ dateFormat: function(time){}
},
- files: [
- 'Gruntfile.js',
- 'injector.js',
- 'injector-tests.js'
- ],
- tasks: ['default', 'karma:unit:run']
+ test: {
+ files: [
+ "injector.js",
+ "injector-tests.js"
+ ],
+ tasks: ['test']
+ }
},
- karma: {
- options: {
- configFile: "karma.conf.js"
- },
- unit: {
- background: true
- },
- once: {
- singleRun: true,
- reporters: ['dots']
+ mochaTest: {
+ test: {
+ options: {
+ clearRequireCache: true,
+ reporter: 'min',
+ require: [
+ function(){
+ Injector = require('./injector');
+ expect = require('chai').expect;
+ }
+ ]
+ },
+ src: [
+ "injector.js",
+ "injector-tests.js"
+ ]
}
}
});
@@ -53,9 +61,10 @@ module.exports = function (grunt) {
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-watch');
- grunt.loadNpmTasks('grunt-karma');
+ grunt.loadNpmTasks('grunt-mocha-test');
grunt.registerTask('default', ['jshint', 'uglify']);
- grunt.registerTask('live', ['default', 'karma:unit:start', 'karma:unit:run', 'watch']);
+ grunt.registerTask('test', ['jshint', 'mochaTest']);
+ grunt.registerTask('live', ['test', 'watch:test']);
};
diff --git a/injector-tests.js b/injector-tests.js
index 339034f..e94126f 100644
--- a/injector-tests.js
+++ b/injector-tests.js
@@ -1,200 +1,359 @@
-/*global describe,it,expect,beforeEach*/
-describe("injector", function() {
- var injector;
+/*global describe,it,expect,beforeEach,Injector, setTimeout*/
+
+describe("deferred", function() {
+ var deferred, promise;
beforeEach(function() {
- injector = new Injector();
+ deferred = Injector.defer();
+ promise = deferred.promise;
});
+ this.timeout(100);
- describe("#register()", function() {
- it("should return the injector", function() {
- var result = injector.register("A", function(){ return "A"; });
- expect(result).toBe(injector);
- });
-
- it("should use a function's name if no name provided", function() {
- injector.register(function A(){ return "A"; });
- expect(injector.specs.A).not.toBe(undefined); // IMPLEMENTATION DETAIL
+ describe("promise", function() {
+ it("should have a 'then' method", function() {
+ expect(promise.then).not.to.equal(undefined);
});
- it("should register an object as a series of dependencies", function() {
- injector.register({
- A: function(){ return "A"; },
- B: function(){ return "B"; }
+ it("should be able to chain 'then' methods", function() {
+ var promise1 = promise;
+ var promise2 = promise1.then(function(x) {
+ return x + 1;
});
- expect(injector.specs.A).not.toBe(undefined); // IMPLEMENTATION DETAIL
- expect(injector.specs.B).not.toBe(undefined); // IMPLEMENTATION DETAIL
+ var promise3 = promise1.then(function(x) {
+ return x + 1;
+ });
+ expect(promise1.then).not.to.equal(undefined);
+ expect(promise2.then).not.to.equal(undefined);
+ expect(promise3.then).not.to.equal(undefined);
});
});
- describe("#get()", function() {
- it("should throw if the dependency can't be found", function() {
- expect(function() {
- injector.get("A");
- }).toThrow();
+ describe("resolution", function() {
+
+ it("should resolve asynchronously", function(done) {
+ var hasRun = false;
+ promise.then(function(val) {
+ hasRun = true;
+ return val;
+ });
+ deferred.resolve("value");
+ expect(hasRun).to.equal(false);
+ setTimeout(function() {
+ expect(hasRun).to.equal(true);
+ done();
+ });
});
- });
- describe("#invoke()", function() {
- it("should be able to invoke a function with no dependencies", function() {
- var A = injector.invoke(function() {return "functionResult";});
- expect(A).toBe("functionResult");
+ it("should propagate through multiple promises", function(done) {
+ var promise2 = promise.then(function(value) {
+ return value + 1;
+ });
+ var promise3 = promise2.then(function(value) {
+ return value + 1;
+ });
+ promise3.then(function(value) {
+ expect(value).to.equal(3);
+ done();
+ });
+ deferred.resolve(1);
});
- it("should be able to invoke a function with a dependency in an array", function() {
- injector.register("B", function() {return "constructorB";});
- var A = injector.invoke(["B", function(B) {return B + "A";}]);
- expect(A).toBe("constructorBA");
+ it("should call all registered handlers", function(done) {
+ var called = 0;
+ promise.then(function() {called++;});
+ promise.then(function() {called++;});
+ promise.then(function() {called++;});
+ expect(called).to.equal(0);
+ deferred.resolve(null);
+ setTimeout(function() {
+ expect(called).to.equal(3);
+ done();
+ });
});
- it("should be able to invoke a function with an implicit dependency", function() {
- injector.register("B", function() {return "constructorB";});
- var A = injector.invoke(function(B) {return B + "A";});
- expect(A).toBe("constructorBA");
+ it("should call handlers, even when added after resolution", function(done) {
+ deferred.resolve(null);
+ promise.then(function() {
+ done();
+ });
});
- it("should not touch any exceptions thrown by the function", function() {
+ it("should not allow multiple resolution", function() {
+ deferred.resolve(1);
expect(function() {
- throw new Error("An error");
- }).toThrow(new Error("An error"));
+ deferred.resolve(2);
+ }).to.throw();
});
- it("should invoke the function with 'this' set to the injector", function() {
- var hasRun = false;
- injector.invoke(function() {
- hasRun = true;
- expect(this).toBe(injector);
- });
- expect(hasRun).toBe(true);
+ it("should not allow rejecting after resolving", function() {
+ deferred.resolve(1);
+ expect(function() {
+ deferred.reject(2);
+ }).to.throw();
});
- it("should not provide 'this.requestor' to the invoked function", function() {
- var caught = {};
- try {
- injector.invoke(function() {
- this.requestor();
- });
- } catch (e) {
- caught = e;
- }
- expect(caught.name).toBe("InjectorError");
+ it("should handle a returned promise by 'unwrapping' it", function(done) {
+ promise.then(function(value) {
+ var deferred = Injector.defer();
+ deferred.resolve(value + 1);
+ return deferred.promise;
+ }).then(function(value) {
+ try {
+ expect(value).to.equal(2);
+ done();
+ } catch (e) {
+ done(e);
+ }
+ });
+ deferred.resolve(1);
});
});
- describe("constructor dependency injection", function() {
- it("should be able to register a constructor with no dependencies, and get the value for it", function() {
- injector.register("A", function() {return "constructorA";});
-
- var A = injector.get("A");
- expect(A).toBe("constructorA");
+ describe("rejection", function() {
+ it("should reject asynchronously", function(done) {
+ var hasRun = false;
+ promise.then(null, function(val) {
+ hasRun = true;
+ return val;
+ });
+ deferred.reject("value");
+ expect(hasRun).to.equal(false);
+ setTimeout(function() {
+ expect(hasRun).to.equal(true);
+ done();
+ });
});
- it("should be able to register a constructor with a dependency in an array, and get the value for it", function() {
- injector.register("B", function() {return "constructorB";});
- injector.register("A", ["B", function(B) {return B;}]);
- var A = injector.get("A");
- expect(A).toBe("constructorB");
+ it("should be turned into resolution by a handler", function(done) {
+ var promise2 = promise.then(function(value) {
+ return value + 1;
+ }, function(value) {
+ return value + 100;
+ });
+ promise2.then(function(value) {
+ try {
+ expect(value).to.equal(101);
+ done();
+ } catch (e) {
+ done(e);
+ }
+ });
+ deferred.reject(1);
});
- it("should be able to register a constructor with an implicit dependency, and get the value for it", function() {
- injector.register("B", function() {return "constructorB";});
- injector.register("A", function(B) {return B;});
+ it("should call all registered handlers", function(done) {
+ var called = 0;
+ promise.then(null, function() {called++;});
+ promise.then(null, function() {called++;});
+ promise.then(null, function() {called++;});
+ expect(called).to.equal(0);
+ deferred.reject(null);
+ setTimeout(function() {
+ expect(called).to.equal(3);
+ done();
+ });
+ });
- var A = injector.get("A");
- expect(A).toBe("constructorB");
+ it("should not allow multiple rejection", function() {
+ deferred.reject(1);
+ expect(function() {
+ deferred.reject(2);
+ }).to.throw();
});
- it("should be able to register a constructor with an implicit name", function() {
- injector.register(function A() {return "constructorA";});
- var A = injector.get("A");
- expect(A).toBe("constructorA");
+ it("should not allow resolving after rejecting", function() {
+ deferred.reject(1);
+ expect(function() {
+ deferred.resolve(2);
+ }).to.throw();
});
- it("should be able to register constructors with an object", function() {
- injector.register({
- A: ["B", function (B) {return "constructorA" + B;}],
- B: function(C) { return "B" + C; },
- C: function() { return "C"; }
+ it("should call handlers, even when added after resolution", function(done) {
+ deferred.reject(null);
+ promise.then(null, function() {
+ done();
});
- var A = injector.get("A");
- expect(A).toBe("constructorABC");
});
- it("should wrap any exceptions thrown by constructors", function() {
- var error = new Error("constructorA");
- injector.register("A", function() {throw error;});
- var caught = {};
- try {
- injector.get("A");
- } catch (e) {
- caught = e;
- }
- expect(caught.name).toBe("InjectorError");
- expect(caught.cause).toBe(error);
+ it("should handle a returned promise by 'unwrapping' in the error case", function(done) {
+ promise.then(function(value) {
+ var deferred = Injector.defer();
+ deferred.reject(value + 1);
+ return deferred.promise;
+ }).then(null, function(value) {
+ try {
+ expect(value).to.equal(2);
+ done();
+ } catch (e) {
+ done(e);
+ }
+ });
+ deferred.resolve(1);
});
+ });
- it("should detect circular dependencies", function() {
- injector.register("A", ["B", function(B) {return null;}]);
- injector.register("B", ["A", function(A) {return null;}]);
+ describe("waitForAll", function() {
+ it("should resolve with an array if all successful", function(done) {
+ var d1 = Injector.defer(),
+ d2 = Injector.defer(),
+ d3 = Injector.defer();
+ Injector
+ .waitForAll([d1.promise, d2.promise, d3.promise])
+ .then(function(values) {
+ if (values.length == 3 &&
+ values[0] === 0 &&
+ values[1] === 1 &&
+ values[2] === 2) {
+ done();
+ } else {
+ done(new Error("Error in resolved result: " + values));
+ }
+ }, done);
+ d1.resolve(0);
+ d2.resolve(1);
+ d3.resolve(2);
+ });
- var caught = {message: ''};
- try {
- injector.get("A");
- } catch (e) {
- caught = e;
- }
- expect(caught.message.indexOf("Cyclic dependency detected")).toBe(0);
+ it("should reject with an object of successes/errors if any fail", function(done) {
+ var d1 = Injector.defer(),
+ d2 = Injector.defer(),
+ d3 = Injector.defer();
+ Injector
+ .waitForAll([d1.promise, d2.promise, d3.promise])
+ .then(function(values) {
+ done(new Error("incorrectly resolved promise"));
+ }, function(e) {
+ if (e.errors && e.values &&
+ e.values[0] === 0 &&
+ e.values[1] === 1 &&
+ e.errors[2] === 2) {
+ done();
+ } else {
+ done(new Error("incorrect reject value"));
+ }
+ });
+ d1.resolve(0);
+ d2.resolve(1);
+ d3.reject(2);
});
+ });
+});
- it("should provide access to the requestor through 'this.requestor()'", function() {
- injector.register("B", function() {
- expect(this.requestor()).toBe("A");
- return "returnValueB";
- });
- injector.register("A", ["B", function(B) {
- return B + "A";
- }]);
+describe("injector", function() {
+ var injector, valuePlugin;
+ beforeEach(function() {
+ valuePlugin = new Injector.ValuePlugin();
+ injector = new Injector([valuePlugin]);
+ });
+ this.timeout(100);
- // make sure the function actually ran
- expect(injector.get("A")).toBe("returnValueBA");
+ describe("value registration and retrieval", function() {
+ it("works", function(done) {
+ injector.register("a", function(){return "the value of a";});
+ injector.get("a").then(function(value){
+ if (value == "the value of a")
+ done();
+ else
+ done(new Error("incorrect value for a (" + value + ")"));
+ }, done);
});
- });
- describe("caching", function() {
- it("should return the same exact value for multiple requests of the same dependency", function() {
- injector.register("A", function() {return {an: 'object'};});
+ it("with dependencies works", function(done) {
+ injector
+ .register("a", function(){return "the value of a";})
+ .register("b", ["a", function(a){return a.replace(/a/g, "b");}]);
+ injector.get("b").then(function(value){
+ if (value == "the vblue of b")
+ done();
+ else
+ done(new Error("incorrect value for b (" + value + ")"));
+ }, done);
+ });
+ });
- var first = injector.get("A");
- var second = injector.get("A");
- expect(first).toBe(second);
+ describe("destroy handlers", function() {
+ it("work with single gets", function(done) {
+ injector.register("a", function(){return "a";});
+ expect("a" in valuePlugin.values).to.equal(false);
+ var result = injector.get("a");
+ result.then(function() {
+ if ("a" in valuePlugin.values) {
+ result.destroy().then(function() {
+ if ("a" in valuePlugin.values) {
+ done(new Error("Value found for a after destruction"));
+ } else {
+ done();
+ }
+ });
+ } else {
+ done(new Error("No value found for a"));
+ }
+ }, done);
});
- it("shouldn't re-run constructors for cached values", function() {
- var numTimes = 0;
- injector.register("A", function() {
- numTimes++;
- if (numTimes > 1)
- throw new Error("Shouldn't be run more than once");
- return {an: 'object'};
- });
+ it("works with multiple gets", function(done) {
+ injector.register("a", function(){return "a";});
+ expect("a" in valuePlugin.values).to.equal(false);
+ var result = [injector.get("a"), injector.get("a"), injector.get("a")];
+ result[1].destroy();
+ result[2].destroy();
+ result[0].then(function() {
+ if ("a" in valuePlugin.values) {
+ result[0].destroy().then(function() {
+ if ("a" in valuePlugin.values) {
+ done(new Error("Value found for a after destruction"));
+ } else {
+ done();
+ }
+ });
+ } else {
+ done(new Error("No value found for a"));
+ }
+ }, done);
+ });
- var first = injector.get("A");
- var second = injector.get("A");
- expect(first).toBe(second);
+ it("works through dependencies", function(done) {
+ injector
+ .register("a", function(){return "a";})
+ .register("b", ["a", function(a){return a + "b";}]);
+ expect("a" in valuePlugin.values).to.equal(false);
+ var result = injector.get("b");
+ result.then(function() {
+ if ("a" in valuePlugin.values) {
+ result.destroy().then(function() {
+ if ("a" in valuePlugin.values) {
+ done(new Error("Value found for a after destruction"));
+ } else {
+ done();
+ }
+ });
+ } else {
+ done(new Error("No value found for a"));
+ }
+ }, done);
});
+ });
- it("shouldn't cache values which depend on their requestor", function() {
- var numTimes = 0;
- injector.register("A", function() {
- numTimes++;
- return "A" + this.requestor();
+ describe("cyclic dependency detection", function() {
+ it("detects simple cycles", function(done) {
+ injector.register("a", ["b", function(b) {return b;}]);
+ injector.register("b", ["a", function(a) {return a;}]);
+ injector.get("a").then(function() {
+ done(new Error("Promise should have been rejected"));
+ }, function(e) {
+ done();
});
- injector.register("B", ["A", function(A) {return "B" + A;}]);
- injector.register("C", ["A", function(A) {return "C" + A;}]);
+ });
- expect(injector.get("B")).toBe("BAB");
- expect(injector.get("C")).toBe("CAC");
- expect(numTimes).toBe(2);
+ it("detects cycles with intermediate nodes", function(done) {
+ injector.register("a", ["c", function(c) {return c;}]);
+ injector.register("b", ["a", function(a) {return a;}]);
+ injector.register("c", ["b", function(b) {return b;}]);
+ injector.get("a").then(function() {
+ done(new Error("Promise should have been rejected"));
+ }, function(e) {
+ done();
+ });
});
});
});
diff --git a/injector.js b/injector.js
index 4d8b293..e644539 100644
--- a/injector.js
+++ b/injector.js
@@ -1,133 +1,426 @@
-/*global module*/
+/*global module, setTimeout*/
+
var Injector = (function() {
- var Injector = function() {
- this.specs = {};
- this.values = {};
- this.stack = [];
- this.usedRequestor = null;
- };
+ var Deferred = (function() {
+ var defer = function(fn, value) {setTimeout(function() {fn(value);});};
+ var runFn = function(fn, deferred, action) {
+ return function(value) {
+ try {
+ var result = fn(value);
+ if (result && result.then) {
+ result.then(function(value) {
+ deferred.resolve(value);
+ }, function(value) {
+ deferred.reject(value);
+ });
+ } else {
+ deferred.resolve(result);
+ }
+ } catch (e) {
+ deferred.reject(e);
+ }
+ };
+ };
- Injector.prototype.register = function(name, spec) {
- if (typeof(name) == "function" || name instanceof Function) {
- this.specs[name.name] = name;
- } else if (typeof(name) == "string" || name instanceof String) {
- this.specs[name] = spec;
- } else {
- for (var key in name) {
- this.register(key, name[key]);
+ var Promise = function() {
+ this.resolved = false;
+ this.rejected = false;
+ this.onSuccess = [];
+ this.onError = [];
+ };
+ Promise.prototype.then = function(success, error) {
+ success = success || function(x) {return x;};
+ error = error || function(x) {throw x;};
+ var deferred = new Deferred();
+ var successFn = runFn(success, deferred);
+ var errorFn = runFn(error, deferred);
+ if (this.resolved) {
+ defer(successFn, this.value);
+ } else if (this.rejected) {
+ defer(errorFn, this.value);
+ } else {
+ if (this.onSuccess != null)
+ this.onSuccess.push(successFn);
+ if (this.onError != null)
+ this.onError.push(errorFn);
}
- }
- return this;
- };
+ return deferred.promise;
+ };
- Injector.prototype.get = function(name) {
- if (name in this.specs) {
- var oldUsedRequestor = this.usedRequestor;
- this.usedRequestor = false;
- this.stack.push(name);
- try {
- throwIfCyclic(name, this.stack);
- var result = this.invoke(this.specs[name]);
- if (this.usedRequestor) {
- return result;
+ var Deferred = function() {
+ this.promise = new Promise();
+ };
+ Deferred.prototype.resolve = function(value) {
+ if (this.promise.resolved) {
+ throw new Error("Cannot re-resolve already resolved promise");
+ } else if (this.promise.rejected) {
+ throw new Error("Cannot resolve a rejected promise");
+ } else {
+ this.promise.resolved = true;
+ this.promise.value = value;
+ var handlers = this.promise.onSuccess;
+ this.promise.onSuccess = null;
+ this.promise.onError = null;
+ setTimeout(function() {
+ handlers.forEach(function(handler) {
+ handler(value);
+ });
+ });
+ }
+ };
+ Deferred.prototype.reject = function(value) {
+ if (this.promise.resolved) {
+ throw new Error("Cannot reject an already resolved promise");
+ } else if (this.promise.rejected) {
+ throw new Error("Cannot re-reject a rejected promise");
+ } else {
+ this.promise.rejected = true;
+ this.promise.value = value;
+ var handlers = this.promise.onError;
+ this.promise.onSuccess = null;
+ this.promise.onError = null;
+ setTimeout(function() {
+ handlers.forEach(function(handler) {
+ handler(value);
+ });
+ });
+ }
+ };
+
+ Deferred.waitForAll = function(promises) {
+ var deferred = new Deferred();
+ var successes = 0, errors = 0;
+ var errorResults = {}, successResults = {};
+ var expected = promises.length;
+ promises.forEach(function(promise, i) {
+ promise.then(function(value) {
+ successResults[i] = value;
+ successes++;
+ maybeFinish();
+ }, function(error) {
+ errorResults[i] = error;
+ errors++;
+ maybeFinish();
+ });
+ });
+ var maybeFinish = function() {
+ if (successes == expected) {
+ var array = [];
+ for (var i = 0, l = expected; i < l; ++i)
+ array.push(successResults[i]);
+ deferred.resolve(array);
+ } else if (successes + errors == expected) {
+ deferred.reject({
+ errors: errorResults,
+ values: successResults
+ });
+ }
+ };
+ maybeFinish();
+ return deferred.promise;
+ };
+
+ return Deferred;
+ })();
+
+ var DepsParser = (function() {
+ var fnArgRegex = /^function[^(]*\(([^)]*)\)/;
+ var parseFnArgs = function(fn) {
+ var parts = fnArgRegex.exec(fn.toString().replace(/\s+/g, ""));
+ if (parts == null) {
+ throw new Error("Unable to parse fn definition");
+ } else {
+ return parts[1] ? parts[1].split(/,/) : [];
+ }
+ };
+
+ var parseSpec = function(spec) {
+ var fn, dependencies;
+ if (typeof(spec) == "function" || spec instanceof Function) {
+ dependencies = parseFnArgs(spec);
+ fn = spec;
+ } else {
+ fn = spec[spec.length - 1];
+ dependencies = spec.slice(0, spec.length - 1);
+ }
+ return [fn, dependencies];
+ };
+
+ var parseDep = function(dep) {
+ var parts = dep.split("!");
+ if (parts.length == 0) {
+ throw new Error("Invalid dependency: " + dep);
+ } else if (parts.length == 1) {
+ return {prefix: "", name: parts[0]};
+ } else {
+ return {prefix: parts[0], name: parts.slice(1).join("!")};
+ }
+ };
+
+ return {
+ spec: parseSpec,
+ dep: parseDep
+ };
+ })();
+
+ var Injector = (function(Deferred, DepsParser) {
+
+ var Injector = function(plugins) {
+ // plugins are a list to be executed in order
+ this.plugins = plugins;
+ };
+
+ Injector.rejected = function(message, cause) {
+ var deferred = this.defer();
+ deferred.reject(this.error(message, cause));
+ return deferred.promise;
+ };
+ Injector.prototype.rejected = Injector.rejected;
+
+ Injector.resolved = function(value) {
+ var deferred = this.defer();
+ deferred.resolve(value);
+ return deferred.promise;
+ };
+ Injector.prototype.resolved = Injector.resolved;
+
+ Injector.prototype.get = function(dep, stack) {
+ stack = [dep].concat(stack || []);
+ if (stack && stack.lastIndexOf(dep) != 0) {
+ return this.rejected("Cyclic dependency: " + stack.join(" <- "));
+ }
+ for (var i = 0, l = this.plugins.length; i < l; ++i) {
+ var value = this.plugins[i].get(this, dep, stack);
+ if (value) {
+ if (!value.destroy)
+ value.destroy = function(){return Injector.resolved(true);};
+ return value;
+ }
+ }
+ return this.rejected("Unknown dependency: " + stack.join(" <- "));
+ };
+
+ Injector.prototype.register = function(name, value) {
+ for (var i = 0, l = this.plugins.length; i < l; ++i) {
+ if (this.plugins[i].register(this, name, value))
+ return this;
+ }
+ throw this.error("No plugin handled registration of: " + name);
+ return this;
+ };
+
+ Injector.prototype.invoke = function(spec) {
+ var injector = this;
+ var parsed = this.parseSpec(spec);
+ var fn = parsed[0];
+ var dependencies = parsed[1];
+ var depPromises = dependencies.map(function(dep) {
+ return injector.get(dep, []);
+ });
+ var depPromise = this.waitForAll(depPromises);
+ var result = depPromise.then(function(results) {
+ return fn.apply(injector, results);
+ });
+ result.destroy = function() {
+ return Injector.waitForAll(depPromises.map(function(promise) {
+ return promise.destroy();
+ })).then(function() {
+ return true;
+ });
+ };
+ return result;
+ };
+
+
+
+ /* static utility functions */
+
+ Injector.error = function(message, cause) {return new InjectorError(message, cause);};
+ Injector.prototype.error = Injector.error;
+
+ Injector.defer = function() {return new Deferred();};
+ Injector.prototype.defer = Injector.defer;
+
+ Injector.parseSpec = DepsParser.spec;
+ Injector.prototype.parseSpec = DepsParser.spec;
+
+ Injector.waitForAll = Deferred.waitForAll;
+ Injector.prototype.waitForAll = Deferred.waitForAll;
+
+ /* the injector error type */
+
+ var InjectorError = function(message, cause) {
+ this.name = "InjectorError";
+ this.message = message;
+ this.cause = cause;
+ };
+ InjectorError.prototype = new Error();
+ InjectorError.prototype.constructor = InjectorError;
+ InjectorError.prototype.toString = function() {
+ return "InjectorError: " + this.message + (this.cause ? " [caused by " + this.cause + "]" : "");
+ };
+
+ return Injector;
+
+ })(Deferred, DepsParser);
+
+ Injector.prefixPlugin = function(prefix, plugin) {
+ return {
+ register: function(injector, name, spec) {
+ if (name.indexOf(prefix) == 0) {
+ return plugin.register(injector,
+ name.substr(prefix.length),
+ spec);
+ } else {
+ return false;
+ }
+ },
+ get: function(injector, name, stack) {
+ if (name.indexOf(prefix) == 0) {
+ return plugin.get(injector,
+ name.substr(prefix.length),
+ stack);
} else {
- delete this.specs[name];
- return (this.values[name] = result);
+ return null;
}
- } catch (e) {
- throw (e instanceof InjectorError
- ? e
- : new InjectorError("Error constructing value for " + stackString(this.stack), e));
- } finally {
- this.stack.pop();
- this.usedRequestor = oldUsedRequestor;
}
- }
- if (name in this.values) {
- return this.values[name];
- } else {
- throw new InjectorError("Dependency " + name + " not found");
- }
+ };
};
- Injector.prototype.requestor = function() {
- switch (this.stack.length) {
- case 0:
- throw new InjectorError("Cannot use requestor for invoked function - none exists");
- case 1:
- throw new InjectorError("Cannot use requestor for top-level constructor - none exists");
- default:
- if (this.usedRequestor === false)
- this.usedRequestor = true;
- return this.stack[this.stack.length - 2];
- }
- };
+ Injector.DOMPlugin = (function() {
+ var DOMPlugin = function() {
+ this.aliases = {};
+ };
- Injector.prototype.invoke = function(spec) {
- var parsed = parseSpec(spec);
- var fn = parsed[0];
- var dependencies = parsed[1];
- return fn.apply(this, dependencies.map(function(dependency) {
- return this.get(dependency);
- }, this));
- };
+ DOMPlugin.prototype.register = function(injector, name, spec) {
+ this.aliases[name] = spec;
+ return true;
+ };
- Injector.prototype.construct = function() {
- };
+ DOMPlugin.prototype.get = function(injector, name, stack) {
+ var deferred = injector.defer();
+ var interval = setInterval(function() {
+ var obj = $(this.aliases[name] || name);
+ if (obj.length) {
+ clearInterval(interval);
+ deferred.resolve(obj);
+ }
+ }.bind(this), 100);
+ return deferred.promise;
+ };
+ return DOMPlugin;
+ })();
- var parsingRegex = /^function[^(]*\(([^)]*)\)/;
- var parseFnArgs = function(fn) {
- var parts = parsingRegex.exec(fn.toString().replace(/\s+/g, ""));
- if (parts == null) {
- throw new Error("Unable to parse fn definition");
- } else {
- return parts[1] ? parts[1].split(/,/) : [];
- }
- };
+ Injector.HTTPPlugin = (function() {
+ var HTTPPlugin = function() {
+ this.aliases = {};
+ };
- var parseSpec = function(spec) {
- var fn, dependencies;
- if (typeof(spec) == "function" || spec instanceof Function) {
- dependencies = parseFnArgs(spec);
- fn = spec;
- } else {
- fn = spec[spec.length - 1];
- dependencies = spec.slice(0, spec.length - 1);
- }
- return [fn, dependencies];
- };
+ HTTPPlugin.prototype.register = function(injector, name, spec) {
+ this.aliases[name] = spec;
+ return true;
+ };
+ HTTPPlugin.prototype.get = function(injector, name, stack) {
+ var deferred = injector.defer();
+ $.ajax(this.aliases[name] || name).then(function(result) {
+ deferred.resolve(result);
+ // deferred.resolve.bind(deferred);
+ }, deferred.reject.bind(deferred));
+ deferred.promise.destroy = function() {
+ return Injector.resolved(true);
+ };
+ return deferred.promise;
+ };
- var stackString = function(stack) {
- stack.reverse();
- var result = stack.join(" <- ");
- stack.reverse();
- return result;
- };
+ return HTTPPlugin;
+ })();
- var throwIfCyclic = function(name, stack) {
- if (stack.indexOf(name) != stack.length - 1) {
- throw new InjectorError("Cyclic dependency detected " + stackString(stack));
- }
- };
+ Injector.ValuePlugin = (function() {
+ var ValuePlugin = function() {
+ this.specs = {};
+ this.values = {};
+ };
+
+ ValuePlugin.prototype.register = function(injector, name, spec) {
+ this.specs[name] = spec;
+ return true;
+ };
+ ValuePlugin.prototype.get = function(injector, name, stack) {
+ if (name in this.values) {
+ this.values[name].references++;
+ return this.values[name];
+ } else if (name in this.specs) {
+ var spec = this.specs[name];
+ var parsed = injector.parseSpec(spec);
+ var constructor = parsed[0];
+ var dependencies = parsed[1];
+ var depPromises = dependencies.map(function(dep) {
+ return injector.get(dep, [name].concat(stack));
+ });
+ var depPromise = injector.waitForAll(depPromises);
+
+ var onDestroy = null;
+ var readRequestor = false;
+ var deferred = injector.defer();
+ var result = deferred.promise;
+ depPromise.then(function(results) {
+ var wrappedInjector = Object.create(injector);
+ wrappedInjector.requestor = function() {
+ readRequestor = true;
+ return stack[1];
+ };
+ try {
+ deferred.resolve(constructor.apply(wrappedInjector, results));
+ } catch (e) {
+ deferred.reject(e);
+ } finally {
+ onDestroy = wrappedInjector.onDestroy;
+ if (result.references <= 0 && onDestroy)
+ onDestroy();
+ }
+ }, function(e) {
+ deferred.reject(injector.error("Error constructing " + name));
+ });
+
+ result.references = 1;
+ var values = this.values;
+ result.destroy = function() {
+ this.references--;
+ if (this.references <= 0) {
+ if (onDestroy)
+ onDestroy();
+ delete values[name];
+ if (!result.resolved && !result.rejected)
+ deferred.reject(injector.error("Promise destroyed before value completed construction"));
+ return injector.waitForAll(depPromises.map(function(promise) {
+ return promise.destroy();
+ })).then(function() {
+ return true;
+ });
+ } else {
+ return injector.resolved(true);
+ }
+ };
+ if (readRequestor) {
+ return result;
+ } else {
+ return this.values[name] = result;
+ }
+ } else {
+ return null;
+ }
+ };
+
+ return ValuePlugin;
+ })();
- var InjectorError = function(message, cause) {
- this.name = "InjectorError";
- this.message = message;
- this.cause = cause;
- };
- InjectorError.prototype = new Error();
- InjectorError.prototype.constructor = InjectorError;
- InjectorError.prototype.toString = function() {
- return "InjectorError: " + this.message + (this.cause ? " [caused by " + this.cause + "]" : "");
- };
return Injector;
})();
-
if (typeof(module) !== "undefined")
module.exports = Injector;
diff --git a/injector.min.js b/injector.min.js
index f1cf96d..2f09e0f 100644
--- a/injector.min.js
+++ b/injector.min.js
@@ -1 +1 @@
-/*! injector 0.1.0 */var Injector=function(){var a=function(){this.specs={},this.values={},this.stack=[],this.usedRequestor=null};a.prototype.register=function(a,b){if("function"==typeof a||a instanceof Function)this.specs[a.name]=a;else if("string"==typeof a||a instanceof String)this.specs[a]=b;else for(var c in a)this.register(c,a[c]);return this},a.prototype.get=function(a){if(a in this.specs){var b=this.usedRequestor;this.usedRequestor=!1,this.stack.push(a);try{f(a,this.stack);var c=this.invoke(this.specs[a]);return this.usedRequestor?c:(delete this.specs[a],this.values[a]=c)}catch(d){throw d instanceof g?d:new g("Error constructing value for "+e(this.stack),d)}finally{this.stack.pop(),this.usedRequestor=b}}if(a in this.values)return this.values[a];throw new g("Dependency "+a+" not found")},a.prototype.requestor=function(){switch(this.stack.length){case 0:throw new g("Cannot use requestor for invoked function - none exists");case 1:throw new g("Cannot use requestor for top-level constructor - none exists");default:return this.usedRequestor===!1&&(this.usedRequestor=!0),this.stack[this.stack.length-2]}},a.prototype.invoke=function(a){var b=d(a),c=b[0],e=b[1];return c.apply(this,e.map(function(a){return this.get(a)},this))},a.prototype.construct=function(){};var b=/^function[^(]*\(([^)]*)\)/,c=function(a){var c=b.exec(a.toString().replace(/\s+/g,""));if(null==c)throw new Error("Unable to parse fn definition");return c[1]?c[1].split(/,/):[]},d=function(a){var b,d;return"function"==typeof a||a instanceof Function?(d=c(a),b=a):(b=a[a.length-1],d=a.slice(0,a.length-1)),[b,d]},e=function(a){a.reverse();var b=a.join(" <- ");return a.reverse(),b},f=function(a,b){if(b.indexOf(a)!=b.length-1)throw new g("Cyclic dependency detected "+e(b))},g=function(a,b){this.name="InjectorError",this.message=a,this.cause=b};return g.prototype=new Error,g.prototype.constructor=g,g.prototype.toString=function(){return"InjectorError: "+this.message+(this.cause?" [caused by "+this.cause+"]":"")},a}();"undefined"!=typeof module&&(module.exports=Injector); \ No newline at end of file
+/*! injector 0.1.0 */var Injector=function(){var a=function(){var a=function(a,b){setTimeout(function(){a(b)})},b=function(a,b){return function(c){try{var d=a(c);d&&d.then?d.then(function(a){b.resolve(a)},function(a){b.reject(a)}):b.resolve(d)}catch(e){b.reject(e)}}},c=function(){this.resolved=!1,this.rejected=!1,this.onSuccess=[],this.onError=[]};c.prototype.then=function(c,e){c=c||function(a){return a},e=e||function(a){throw a};var f=new d,g=b(c,f),h=b(e,f);return this.resolved?a(g,this.value):this.rejected?a(h,this.value):(null!=this.onSuccess&&this.onSuccess.push(g),null!=this.onError&&this.onError.push(h)),f.promise};var d=function(){this.promise=new c};return d.prototype.resolve=function(a){if(this.promise.resolved)throw new Error("Cannot re-resolve already resolved promise");if(this.promise.rejected)throw new Error("Cannot resolve a rejected promise");this.promise.resolved=!0,this.promise.value=a;var b=this.promise.onSuccess;this.promise.onSuccess=null,this.promise.onError=null,setTimeout(function(){b.forEach(function(b){b(a)})})},d.prototype.reject=function(a){if(this.promise.resolved)throw new Error("Cannot reject an already resolved promise");if(this.promise.rejected)throw new Error("Cannot re-reject a rejected promise");this.promise.rejected=!0,this.promise.value=a;var b=this.promise.onError;this.promise.onSuccess=null,this.promise.onError=null,setTimeout(function(){b.forEach(function(b){b(a)})})},d.waitForAll=function(a){var b=new d,c=0,e=0,f={},g={},h=a.length;a.forEach(function(a,b){a.then(function(a){g[b]=a,c++,i()},function(a){f[b]=a,e++,i()})});var i=function(){if(c==h){for(var a=[],d=0,i=h;i>d;++d)a.push(g[d]);b.resolve(a)}else c+e==h&&b.reject({errors:f,values:g})};return i(),b.promise},d}(),b=function(){var a=/^function[^(]*\(([^)]*)\)/,b=function(b){var c=a.exec(b.toString().replace(/\s+/g,""));if(null==c)throw new Error("Unable to parse fn definition");return c[1]?c[1].split(/,/):[]},c=function(a){var c,d;return"function"==typeof a||a instanceof Function?(d=b(a),c=a):(c=a[a.length-1],d=a.slice(0,a.length-1)),[c,d]},d=function(a){var b=a.split("!");if(0==b.length)throw new Error("Invalid dependency: "+a);return 1==b.length?{prefix:"",name:b[0]}:{prefix:b[0],name:b.slice(1).join("!")}};return{spec:c,dep:d}}(),c=function(a,b){var c=function(a){this.plugins=a};c.rejected=function(a,b){var c=this.defer();return c.reject(this.error(a,b)),c.promise},c.prototype.rejected=c.rejected,c.resolved=function(a){var b=this.defer();return b.resolve(a),b.promise},c.prototype.resolved=c.resolved,c.prototype.get=function(a,b){if(b=[a].concat(b||[]),b&&0!=b.lastIndexOf(a))return this.rejected("Cyclic dependency: "+b.join(" <- "));for(var d=0,e=this.plugins.length;e>d;++d){var f=this.plugins[d].get(this,a,b);if(f)return f.destroy||(f.destroy=function(){return c.resolved(!0)}),f}return this.rejected("Unknown dependency: "+b.join(" <- "))},c.prototype.register=function(a,b){for(var c=0,d=this.plugins.length;d>c;++c)if(this.plugins[c].register(this,a,b))return this;throw this.error("No plugin handled registration of: "+a)},c.prototype.invoke=function(a){var b=this,d=this.parseSpec(a),e=d[0],f=d[1],g=f.map(function(a){return b.get(a,[])}),h=this.waitForAll(g),i=h.then(function(a){return e.apply(b,a)});return i.destroy=function(){return c.waitForAll(g.map(function(a){return a.destroy()})).then(function(){return!0})},i},c.error=function(a,b){return new d(a,b)},c.prototype.error=c.error,c.defer=function(){return new a},c.prototype.defer=c.defer,c.parseSpec=b.spec,c.prototype.parseSpec=b.spec,c.waitForAll=a.waitForAll,c.prototype.waitForAll=a.waitForAll;var d=function(a,b){this.name="InjectorError",this.message=a,this.cause=b};return d.prototype=new Error,d.prototype.constructor=d,d.prototype.toString=function(){return"InjectorError: "+this.message+(this.cause?" [caused by "+this.cause+"]":"")},c}(a,b);return c.prefixPlugin=function(a,b){return{register:function(c,d,e){return 0==d.indexOf(a)?b.register(c,d.substr(a.length),e):!1},get:function(c,d,e){return 0==d.indexOf(a)?b.get(c,d.substr(a.length),e):null}}},c.DOMPlugin=function(){var a=function(){this.aliases={}};return a.prototype.register=function(a,b,c){return this.aliases[b]=c,!0},a.prototype.get=function(a,b){var c=a.defer(),d=setInterval(function(){var a=$(this.aliases[b]||b);a.length&&(clearInterval(d),c.resolve(a))}.bind(this),100);return c.promise},a}(),c.HTTPPlugin=function(){var a=function(){this.aliases={}};return a.prototype.register=function(a,b,c){return this.aliases[b]=c,!0},a.prototype.get=function(a,b){var d=a.defer();return $.ajax(this.aliases[b]||b).then(function(a){d.resolve(a)},d.reject.bind(d)),d.promise.destroy=function(){return c.resolved(!0)},d.promise},a}(),c.ValuePlugin=function(){var a=function(){this.specs={},this.values={}};return a.prototype.register=function(a,b,c){return this.specs[b]=c,!0},a.prototype.get=function(a,b,c){if(b in this.values)return this.values[b].references++,this.values[b];if(b in this.specs){var d=this.specs[b],e=a.parseSpec(d),f=e[0],g=e[1],h=g.map(function(d){return a.get(d,[b].concat(c))}),i=a.waitForAll(h),j=null,k=!1,l=a.defer(),m=l.promise;i.then(function(b){var d=Object.create(a);d.requestor=function(){return k=!0,c[1]};try{l.resolve(f.apply(d,b))}catch(e){l.reject(e)}finally{j=d.onDestroy,m.references<=0&&j&&j()}},function(){l.reject(a.error("Error constructing "+b))}),m.references=1;var n=this.values;return m.destroy=function(){return this.references--,this.references<=0?(j&&j(),delete n[b],m.resolved||m.rejected||l.reject(a.error("Promise destroyed before value completed construction")),a.waitForAll(h.map(function(a){return a.destroy()})).then(function(){return!0})):a.resolved(!0)},k?m:this.values[b]=m}return null},a}(),c}();"undefined"!=typeof module&&(module.exports=Injector); \ No newline at end of file
diff --git a/karma.conf.js b/karma.conf.js
index d8fc2e9..9dff1c9 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -49,24 +49,25 @@ module.exports = function(config) {
autoWatch: true,
- // Start these browsers, currently available:
- // - Chrome
- // - ChromeCanary
- // - Firefox
- // - Opera
- // - Safari (only Mac)
- // - PhantomJS
- // - IE (only Windows)
- browsers: ['PhantomJS'],
+ // Continuous Integration mode
+ // if true, it capture browsers, run tests and exit
+ singleRun: false,
+
+ // Start these browsers, currently available:
+ // - Chrome
+ // - ChromeCanary
+ // - Firefox
+ // - Opera
+ // - Safari (only Mac)
+ // - PhantomJS
+ // - IE (only Windows)
+ browsers: ['PhantomJS'],
// If browser does not capture in given timeout [ms], kill it
captureTimeout: 60000,
- // Continuous Integration mode
- // if true, it capture browsers, run tests and exit
- singleRun: false
});
};
diff --git a/package.json b/package.json
index 4aa966b..2ff7b5b 100644
--- a/package.json
+++ b/package.json
@@ -1,19 +1,17 @@
{
- "name": "injector",
- "version": "0.1.0",
- "dependencies": {},
- "devDependencies": {
- "grunt": "~0.4.1",
- "grunt-contrib-concat": "~0.3.0",
- "grunt-contrib-uglify": "~0.2.0",
- "grunt-contrib-jshint": "~0.6.0",
- "grunt-contrib-watch": "~0.4.0",
- "grunt-karma": "0.6.2",
- "karma": "~0.10",
- "karma-jasmine": "~0.1.0",
- "karma-junit-reporter": "~0.1.0"
- },
- "engines": {
- "node": ">=0.8.0"
- }
+ "name": "injector",
+ "version": "0.1.0",
+ "dependencies": {},
+ "devDependencies": {
+ "chai": "~1.9",
+ "grunt": "~0.4",
+ "grunt-contrib-jshint": "~0.6",
+ "grunt-contrib-uglify": "~0.2",
+ "grunt-contrib-watch": "~0.6",
+ "grunt-mocha-test": "~0.12",
+ "mocha": "~1.21"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
}