diff options
author | Carlo Zancanaro <carlo@zancanaro.id.au> | 2014-09-19 12:19:31 +1000 |
---|---|---|
committer | Carlo Zancanaro <carlo@zancanaro.id.au> | 2014-09-19 12:19:31 +1000 |
commit | 029e5c1ec39fd35c9edf74c680f7c742e12486f0 (patch) | |
tree | e327d847c7f5d3a56fc9b762c9718582bd979cbe |
Initial commit - minimal, synchronous, injector
-rw-r--r-- | .hgignore | 1 | ||||
-rw-r--r-- | Gruntfile.js | 61 | ||||
-rw-r--r-- | bower.json | 5 | ||||
-rw-r--r-- | injector-tests.js | 200 | ||||
-rw-r--r-- | injector.js | 133 | ||||
-rw-r--r-- | injector.min.js | 1 | ||||
-rw-r--r-- | karma.conf.js | 73 | ||||
-rw-r--r-- | package.json | 19 |
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" + } +} |