summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarlo Zancanaro <carlo@zancanaro.id.au>2014-09-19 12:19:31 +1000
committerCarlo Zancanaro <carlo@zancanaro.id.au>2014-09-19 12:19:31 +1000
commit029e5c1ec39fd35c9edf74c680f7c742e12486f0 (patch)
treee327d847c7f5d3a56fc9b762c9718582bd979cbe
Initial commit - minimal, synchronous, injector
-rw-r--r--.hgignore1
-rw-r--r--Gruntfile.js61
-rw-r--r--bower.json5
-rw-r--r--injector-tests.js200
-rw-r--r--injector.js133
-rw-r--r--injector.min.js1
-rw-r--r--karma.conf.js73
-rw-r--r--package.json19
8 files changed, 493 insertions, 0 deletions
diff --git a/.hgignore b/.hgignore
new file mode 100644
index 0000000..7343bc4
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1 @@
+^node_modules/ \ No newline at end of file
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644
index 0000000..c09bdde
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,61 @@
+module.exports = function (grunt) {
+
+ grunt.initConfig({
+ pkg: grunt.file.readJSON('package.json'),
+ library: grunt.file.readJSON('bower.json'),
+ uglify: {
+ options: {
+ banner: '/*! <%= pkg.name %> <%= pkg.version %> */'
+ },
+ jid: {
+ files: {
+ '<%= library.name %>.min.js': ['<%= library.name %>.js']
+ }
+ }
+ },
+ jshint: {
+ options: {
+ src: [
+ '<%= library.name %>'
+ ],
+ // options here to override JSHint defaults
+ globals: {
+ module: true
+ },
+ globalstrict: false
+ }
+ },
+ watch: {
+ options: {
+ livereload: true
+ },
+ files: [
+ 'Gruntfile.js',
+ 'injector.js',
+ 'injector-tests.js'
+ ],
+ tasks: ['default', 'karma:unit:run']
+ },
+ karma: {
+ options: {
+ configFile: "karma.conf.js"
+ },
+ unit: {
+ background: true
+ },
+ once: {
+ singleRun: true,
+ reporters: ['dots']
+ }
+ }
+ });
+
+ grunt.loadNpmTasks('grunt-contrib-uglify');
+ grunt.loadNpmTasks('grunt-contrib-jshint');
+ grunt.loadNpmTasks('grunt-contrib-watch');
+ grunt.loadNpmTasks('grunt-karma');
+
+ grunt.registerTask('default', ['jshint', 'uglify']);
+ grunt.registerTask('live', ['default', 'karma:unit:start', 'karma:unit:run', 'watch']);
+
+};
diff --git a/bower.json b/bower.json
new file mode 100644
index 0000000..37f58d6
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,5 @@
+{
+ "name": "injector",
+ "version": "0.1.0"
+}
+
diff --git a/injector-tests.js b/injector-tests.js
new file mode 100644
index 0000000..339034f
--- /dev/null
+++ b/injector-tests.js
@@ -0,0 +1,200 @@
+/*global describe,it,expect,beforeEach*/
+describe("injector", function() {
+ var injector;
+ beforeEach(function() {
+ injector = new Injector();
+ });
+
+ 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
+ });
+
+ it("should register an object as a series of dependencies", function() {
+ injector.register({
+ A: function(){ return "A"; },
+ B: function(){ return "B"; }
+ });
+ expect(injector.specs.A).not.toBe(undefined); // IMPLEMENTATION DETAIL
+ expect(injector.specs.B).not.toBe(undefined); // IMPLEMENTATION DETAIL
+ });
+ });
+
+ describe("#get()", function() {
+ it("should throw if the dependency can't be found", function() {
+ expect(function() {
+ injector.get("A");
+ }).toThrow();
+ });
+ });
+
+ 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 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 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 not touch any exceptions thrown by the function", function() {
+ expect(function() {
+ throw new Error("An error");
+ }).toThrow(new Error("An error"));
+ });
+
+ 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 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");
+ });
+ });
+
+ 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");
+ });
+
+ 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 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;});
+
+ var A = injector.get("A");
+ expect(A).toBe("constructorB");
+ });
+
+ 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 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"; }
+ });
+ 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 detect circular dependencies", function() {
+ injector.register("A", ["B", function(B) {return null;}]);
+ injector.register("B", ["A", function(A) {return null;}]);
+
+ var caught = {message: ''};
+ try {
+ injector.get("A");
+ } catch (e) {
+ caught = e;
+ }
+ expect(caught.message.indexOf("Cyclic dependency detected")).toBe(0);
+ });
+
+ 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";
+ }]);
+
+ // make sure the function actually ran
+ expect(injector.get("A")).toBe("returnValueBA");
+ });
+ });
+
+ 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'};});
+
+ var first = injector.get("A");
+ var second = injector.get("A");
+ expect(first).toBe(second);
+ });
+
+ 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'};
+ });
+
+ var first = injector.get("A");
+ var second = injector.get("A");
+ expect(first).toBe(second);
+ });
+
+ it("shouldn't cache values which depend on their requestor", function() {
+ var numTimes = 0;
+ injector.register("A", function() {
+ numTimes++;
+ return "A" + this.requestor();
+ });
+ 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);
+ });
+ });
+});
diff --git a/injector.js b/injector.js
new file mode 100644
index 0000000..4d8b293
--- /dev/null
+++ b/injector.js
@@ -0,0 +1,133 @@
+/*global module*/
+var Injector = (function() {
+
+ var Injector = function() {
+ this.specs = {};
+ this.values = {};
+ this.stack = [];
+ this.usedRequestor = null;
+ };
+
+ 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]);
+ }
+ }
+ return this;
+ };
+
+ 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;
+ } else {
+ delete this.specs[name];
+ return (this.values[name] = result);
+ }
+ } 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.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));
+ };
+
+ Injector.prototype.construct = function() {
+ };
+
+
+ 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(/,/) : [];
+ }
+ };
+
+ 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 stackString = function(stack) {
+ stack.reverse();
+ var result = stack.join(" <- ");
+ stack.reverse();
+ return result;
+ };
+
+ var throwIfCyclic = function(name, stack) {
+ if (stack.indexOf(name) != stack.length - 1) {
+ throw new InjectorError("Cyclic dependency detected " + stackString(stack));
+ }
+ };
+
+
+ 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
new file mode 100644
index 0000000..f1cf96d
--- /dev/null
+++ b/injector.min.js
@@ -0,0 +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
diff --git a/karma.conf.js b/karma.conf.js
new file mode 100644
index 0000000..d8fc2e9
--- /dev/null
+++ b/karma.conf.js
@@ -0,0 +1,73 @@
+module.exports = function(config) {
+ config.set({
+
+ // base path, that will be used to resolve files and exclude
+ basePath: '',
+
+ frameworks: ['jasmine'],
+
+ plugins: [
+ 'karma-jasmine',
+ 'karma-junit-reporter',
+ 'karma-phantomjs-launcher'
+ ],
+
+ // list of files / patterns to load in the browser
+ files: [
+ 'injector.js',
+ 'injector-tests.js'
+ ],
+
+
+ // list of files to exclude
+ exclude: [],
+
+
+ // test results reporter to use
+ // possible values: 'dots', 'progress', 'junit'
+ reporters: ['progress'],
+
+
+ // web server port
+ port: 9876,
+
+
+ // cli runner port
+ runnerPort: 9100,
+
+
+ // enable / disable colors in the output (reporters and logs)
+ colors: true,
+
+
+ // level of logging
+ // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
+ logLevel: config.LOG_DISABLE,
+
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: true,
+
+
+ // 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
new file mode 100644
index 0000000..4aa966b
--- /dev/null
+++ b/package.json
@@ -0,0 +1,19 @@
+{
+ "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"
+ }
+}