UNPKG

freight.js

Version:

An simple CommonJS dependency injection container for both node.js and the browser (using browserify)

760 lines (594 loc) 18.8 kB
;(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ var Definition = function Definition(id, conf, container) { var self = this; /** * the id of the definition * @type {string} */ self.id = id; /** * The callable that creates the service * @type {Function} */ self.fn = false; /** * The arguments that will be send to the callable when called, order matters * @type {Array} */ self.args = []; /** * The tags that the service will receive * @type {Array} */ self.tags = []; /** * Contains the last created instance of the service, this is reused when the service is a shared service * @type {Boolean} */ self.service = false; /** * Indicate wether the service behaves like a singleton; returning the same instance on repeated requests * @type {Boolean} */ self.shared = false; /** * Create an instance from this definition * * @method retrieve() * @return {mixed} */ self.retrieve = function retrieve() { if(self.shared === true && self.service !== false) { return self.service; } var service = self.fn(); if(service === undefined) { throw new Error('"'+id+'" service returned "'+undefined+'" on instantiation.'); } self.service = service; return service; }; /** * Add an argument (at the end) to the definition * * @method addArgument * @param {mixed} arg * @chainable */ self.addArgument = function addArgument(arg) { self.args.push(arg); return self; }; /** * Tag the service * * @method addTag() * @param {string} tag * @chainable */ self.addTag = function addTag(tag) { if(self.tags.indexOf(tag) === -1) self.tags.push(tag); return self; }; /** * Resolve arguments that are container ids * @return {Array} the resolved arguments */ self.resolveArguments = function resolveArguments(arr) { arr = typeof arr !== 'undefined' ? arr : self.args; arr.forEach(function(arg, i){ if(container.isId(arg)) { arr[i] = container.get(arg.replace(container.idPrefix, '')); } else if(Array.isArray(arg)) { arr[i] = self.resolveArguments(arg); } else if(arg.constructor == Object) { Object.keys(arg).forEach(function(k){ if(container.isId(arg[k])) arg[k] = container.get(arg[k].replace(container.idPrefix, '')); }); } }); return arr; }; /** * Create an callable that can creates instances for this definitions * * @method * @private * @param {string} fnParam the container parameter that contains the constructor or factory function * @param {string} type either 'c' for constructor or 'f' for factory * @return {Function} */ self._createCallable = function(fnParam, type) { var res = false; var getCheckCallable = function(p) { var fn = false; if(typeof p === 'function') { fn = p; } else { try { fn = container.getParameter(p.replace(container.idPrefix, '')); } catch(e) { throw new Error('Whene trying to instanciate service "'+self.id+'", the parameter specifying the '+((type == 'f') ? ('factory') : ('constructor'))+': "'+fnParam+'" did not exist.'); } } if(typeof fn !== 'function') { throw new Error('Whene trying to instanciate service "'+self.id+'", the parameter specifying the '+((type == 'f') ? ('factory') : ('constructor'))+': "'+fnParam+'" did not return a function, instead received: "'+fn+'"'); } return fn; }; function neu(constructor, args) { var instance = Object.create(constructor.prototype); var result = constructor.apply(instance, args); return typeof result === 'object' ? result : instance; } //constructor type use new if(type === 'c') { res = function() { var _c = new getCheckCallable(fnParam); var _args = self.resolveArguments(); return neu(_c, _args); }; return res; } //factory type just call it return function() { return getCheckCallable(fnParam).apply(container, self.resolveArguments()); //create using factory fn }; }; /** * Apply an defintion configuration * @param {Object} conf * @return {Definition} * @chainable */ self.configure = function(conf) { //shared if(conf.shared !== undefined) { if(String(conf.shared).toLowerCase() === 'true') { self.shared = true; } else if(String(conf.shared).toLowerCase() === 'false') { self.shared = false; } else { throw new Error('Configuration of "'+self.id+'" shared should be an boolean, received: "'+conf+'"'); } } //constructing var fnParam = conf.constructorFn; var type = 'c'; if(fnParam === undefined) { fnParam = conf.factoryFn; type = 'f'; } else if(conf.factoryFn !== undefined) { throw new Error('Configuration of "'+self.id+'" should specify a constructor OR an factory function, not both - received: "'+conf+'"'); } if(fnParam === undefined) throw new Error('Configuration of "'+self.id+'" must either specify an constructor or an factory function - received: "'+conf+'"'); if(container.isId(fnParam) === false && typeof fnParam !== 'function') { throw new Error('Constructor or an factory function of "'+self.id+'" should be specified using a parameter id - received: "'+fnParam+'"'); } self.fn = self._createCallable(fnParam, type); //arguments var args = conf.arguments; if(args !== undefined) { if(!Array.isArray(args)) throw new Error('Instantiation arguments of "'+self.id+'" should be specified as an array - received: "'+args+'"'); args.forEach(function(arg){ self.addArgument(arg); }); } //tags var tags = conf.tags; if(tags !== undefined) { if(!Array.isArray(tags)) throw new Error('Service tags of "'+self.id+'" should be specified as an array - received: "'+tags+'"'); tags.forEach(function(tag){ self.addTag(tag); }); } return self; }; //configure the definition with the provided argument self.configure(conf); }; module.exports = Definition; },{}],2:[function(require,module,exports){ var Definition = require('./definition.js'); var Freight = function() { var self = this; /** * Determin how service id are distinques of normal string arguments in configuration * @type {String} */ self.idPrefix = ':'; /** * the array of definitions this container managers * @type {Array} * @private */ self._definitions = []; /** * the hash with parameters this container holds * @type {Object} * @private */ self._parameters = {}; /** * Get and service of parameter from the container * * @method getService() * @param {string} id * @return {mixed} */ self.get = function get(id) { var res = false; try { res = self.getParameter(id); } catch(e) { res = self.getService(id); } return res; }; /** * Check wether the string an parameter id * @method isId() * @param {string} str * @return {Boolean} */ self.isId = function isId(val) { if(typeof val !== 'string') return false; return ((val.indexOf(self.idPrefix) === 0) ? (true) : (false)); }; /** * Set a parameter on this container * * @method setParameter() * @param {string} id * @param {mixed} val * @chainable */ self.setParameter = function setParameter(id, val) { self._parameters[id] = val; return self; }; /** * Retrieve a parameter from the container * @param {string} id * @return {mixed} */ self.getParameter = function getParameter(id) { var res = self._parameters[id]; if(res === undefined) throw new Error('Parameter "'+id+'" not found.'); return res; }; /** * Return an array of service ids from services with the given tag * * @method findTaggedServiceIds() * @param {string} tag * @return {Array} */ self.findTaggedServiceIds = function findTaggedServiceIds(tag) { var res = []; self._definitions.forEach(function(d){ if(d.tags.indexOf(tag) !== -1) { res.push(d.id); } }); return res; }; /** * Return a service definition from the container * * @method getDefinition() * @param {string} id * @return {Definition} */ self.getDefinition = function getDefinition(id) { var def = false; self._definitions.forEach(function(d){ if(d.id == id) { def = d; } }); return def; }; /** * Return an service by its id * * @method getService() * @param {string} id * @return {mixed} */ self.getService = function getService(id) { var def = self.getDefinition(id); if(def === false) throw new Error('Service "'+id+'" not found.'); var service = def.retrieve(); return service; }; /** * Register a new service as the specified id * * @method register() * @param {string} id * @param {Object} conf the configuration hash for the individual service * @return {Definition} */ self.register = function register(id, conf) { var def = new Definition(id, conf, self); self._definitions.push(def); return def; }; /** * Register several definitions from an configuration hash (e.g. json) * * @method registerAll() * @param {Object} conf the services * @return {Array} a array of registered definitions */ self.registerAll = function build(conf) { var res = []; Object.keys(conf).forEach(function(id){ res.push(self.register(id, conf[id])); }); return res; }; }; module.exports = Freight; },{"./definition.js":1}],3:[function(require,module,exports){ var Freight = require('./../src/freight.js'); var Definition = require('./../src/definition.js'); var carsConfig = require('./examples/cars.json'); var invalidConfig = require('./examples/invalid.json'); var castleConfig = require('./examples/castle.json'); describe('Freight', function(){ var f; beforeEach(function(){ f = new Freight(); }); describe('#construct()', function(){ it('should initialize members', function(){ var def = new Definition('car', carsConfig.car, f); def.id.should.equal('car'); def.shared.should.equal(false); (typeof def.fn).should.equal('function'); var def2 = new Definition('engine', carsConfig.engine, f); def2.shared.should.equal(true); var def3 = new Definition('engine', { "shared": "false", "factoryFn": ":engine_factory" }, f); def3.shared.should.equal(false); }); it('callable should create valid instances as constructor', function(){ var Car = function(a, b){ a.should.equal('a'); b.should.equal('b'); }; f.setParameter('car_class', 'a'); var def = new Definition('car', carsConfig.car, f); def.args = ['a', 'b']; (function(){ var car = def.fn(); //parameter is a string instead of a function }).should.throw(); f.setParameter('car_class', Car); var car = def.fn(); car.should.be.an.instanceOf(Car); }); it('callable should create valid instances as factory', function(){ var Engine = function(){}; f.setParameter('engine_factory', 'a'); var def = new Definition('engine', carsConfig.engine, f); (function(){ var engine = def.fn(); //parameter is a string instead of a function }).should.throw(); f.setParameter('engine_factory', function(){ return new Engine(); }); var engine = def.fn(); engine.should.be.an.instanceOf(Engine); }); }); describe('#configure()', function(){ it('Should throw on invalid configuration', function(){ (function(){ new Definition('invalid1', invalidConfig.invalid1, f); //no factory or constructor }).should.throw(); (function(){ new Definition('invalid2', invalidConfig.invalid2, f); //factory/cls must be a string }).should.throw(); (function(){ new Definition('invalid3', invalidConfig.invalid3, f); //factory/cls should not be both defined }).should.throw(); (function(){ new Definition('invalid4', invalidConfig.invalid4, f); //arguments not as array }).should.throw(); (function(){ new Definition('invalid5', invalidConfig.invalid5, f); //arguments not as array }).should.throw(); (function(){ new Definition('invalid6', invalidConfig.invalid6, f); //tags not as array }).should.throw(); }); it('Should add arguments from configuration', function(){ var def = new Definition('car', carsConfig.car, f); Array.isArray(def.args).should.equal(true); def.args.length.should.equal(2); def.args[0].should.equal(':engine'); def.args[1].should.equal(':wheels'); }); }); describe('#addTag()', function(){ it("should increase length and return definition", function(){ var def = new Definition('engine', carsConfig.engine, f); def.tags.length.should.equal(1); def.addTag('part'); def.addTag('part'); def.addTag('metal'); def.tags.length.should.equal(3); }); }); describe('#addArgument()', function(){ it("should increase length and return definition", function(){ var def = new Definition('engine', carsConfig.engine, f); var def2 = def.addArgument('arg1'); def2.should.equal(def); def.args.length.should.equal(1); def.addArgument('arg2'); def.args.length.should.equal(2); def.args[1].should.equal('arg2'); }); }); describe('#resolveArguments()', function(){ var Engine = function(){}; var engineFactory = function(){ this.should.equal(f); return new Engine(); }; var Car = function(){}; var Wheels = function(){}; var Castle = function(knight, towers) { this.knight = knight; this.towers = towers; }; var Tower = function(height) { this.height = height; return { height: height, }; }; var Knight = function(name, sword, attributes) { this.name = name; this.sword = sword; this.attributes = attributes; }; var Sword = function(name) { this.name = name; this.fastenGrip = function() { this.fastened = true; }; this.sharpenBlade = function() { this.sharpened = true; }; }; it("should increase length and return definition", function(){ var defs = f.registerAll(carsConfig); f.setParameter('engine_factory', engineFactory); f.setParameter('wheels_class', Wheels); var args = defs[0].resolveArguments(); args[0].should.be.an.instanceOf(Engine); args[1].should.be.an.instanceOf(Wheels); args = defs[0].resolveArguments(); //doub resolve should not have any consequences args[0].should.be.an.instanceOf(Engine); args[1].should.be.an.instanceOf(Wheels); }); it("should also resolve array arguments", function(){ var defs = f.registerAll(castleConfig); f.setParameter('castle.class', Castle) .setParameter('tower.class', Tower) .setParameter('tower.height', 30) .setParameter('knight.class', Knight) .setParameter('knight.quest', 'The holy DI grail') .setParameter('sword.factory', function(){ var sword = new Sword('Excalibur'); sword.fastenGrip(); sword.sharpenBlade(); return sword; }); var args = defs[0].resolveArguments(); args[1].length.should.equal(3); args[1][0].height.should.equal(30); args[1][1].height.should.equal(30); args[1][2][1].height.should.equal(30); //test nested }); it("should also resolve hash arguments", function(){ var defs = f.registerAll(castleConfig); f.setParameter('castle.class', Castle) .setParameter('tower.class', Tower) .setParameter('tower.height', 30) .setParameter('knight.class', Knight) .setParameter('knight.quest', 'The holy DI grail') .setParameter('sword.factory', function(){ var sword = new Sword('Excalibur'); sword.fastenGrip(); sword.sharpenBlade(); return sword; }); var args = defs[1].resolveArguments(); args[2].quest.should.equal('The holy DI grail'); args[2].tower.height.should.equal(30); }); }); }); },{"./../src/definition.js":1,"./../src/freight.js":2,"./examples/cars.json":4,"./examples/castle.json":5,"./examples/invalid.json":6}],4:[function(require,module,exports){ module.exports={ "car": { "constructorFn": ":car_class", "arguments": [":engine", ":wheels"] }, "engine": { "shared": "true", "factoryFn": ":engine_factory", "tags": ["interior"] }, "wheels": { "constructorFn": ":wheels_class", "tags": ["exterior"] } } },{}],5:[function(require,module,exports){ module.exports={ "castle_fret": { "shared": true, "constructorFn": ":castle.class", "arguments": [":sir_freight", [":tower", ":tower", [":tower", ":tower"]]], "tags": ["inanimate", "building"] }, "sir_freight": { "shared": true, "constructorFn": ":knight.class", "arguments": ["Sir Freight", ":sword", { "strength": 40, "dexterity": 100, "quest": ":knight.quest", "tower": ":tower" }] }, "sword": { "factoryFn": ":sword.factory", "tags": ["inanimate"] }, "tower": { "constructorFn": ":tower.class", "arguments": [":tower.height"], "tags": ["inanimate", "building"] } } },{}],6:[function(require,module,exports){ module.exports={ "invalid1": { }, "invalid2": { "factoryFn": false }, "invalid3": { "factoryFn": ":car", "constructorFn": ":Car" }, "invalid4": { "constructorFn": ":Car", "arguments": "a" }, "invalid5": { "constructorFn": ":Car", "shared": "a" }, "invalid6": { "constructorFn": ":Car", "tags": "a" } } },{}]},{},[3]) ;