UNPKG

freight.js

Version:

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

684 lines (529 loc) 16.6 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){ module.exports={ "car": { "constructorFn": ":car_class", "arguments": [":engine", ":wheels"] }, "engine": { "shared": "true", "factoryFn": ":engine_factory", "tags": ["interior"] }, "wheels": { "constructorFn": ":wheels_class", "tags": ["exterior"] } } },{}],4:[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"] } } },{}],5:[function(require,module,exports){ var Freight = require('./../src/freight.js'); var Definition = require('./../src/definition.js'); var carsConfig = require('./examples/cars.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(){ f.should.be.an.instanceOf(Freight); }); }); describe('#setParameter()', function(){ it('should set parameters on the container', function(){ var f2 = f.setParameter('car.class', function(){}); f2.should.equal(f); }); }); describe('#getParameter()', function(){ it('should get a parameter when its set', function(){ var f2 = f.setParameter('car.class', 'hey'); f.getParameter('car.class').should.equal('hey'); f.get('car.class').should.equal('hey'); }); it('should throw when not set', function(){ var f2 = f.setParameter('car.class', 'hey'); (function(){ f.getParameter('car.bogus'); }).should.throw(); }); }); describe('#isParameterId()', function(){ it('should detect first char and type', function(){ (f.isId(false)).should.equal(false); (f.isId('param')).should.equal(false); (f.isId(':param')).should.equal(true); }); }); describe('#register()', function(){ it('should also work with registerAll', function(){ var defs = f.registerAll(carsConfig); defs.length.should.equal(3); f._definitions.length.should.equal(3); }); it('should take correct arguments', function(){ var def = f.register('car', carsConfig.car); f._definitions.length.should.equal(1); def.should.be.an.instanceOf(Definition); }); it('should also work with functions as constructor/factory', function(){ var obj = {}; var factory = function() { arguments[0].should.equal("a"); return obj; }; var Bird = function() { arguments[0].should.equal("b"); this.wings = '2 wings'; }; var def = f.register('truck', { factoryFn: factory, arguments: ["a"] }); var res = f.get('truck'); res.should.equal(obj); var def2 = f.register('bird', { constructorFn: Bird, arguments: ["b"] }); var res2 = f.get('bird'); res2.wings.should.equal('2 wings'); }); }); describe('#getService()', function(){ it('should get a service when its set', function(){ var Engine = function() {}; var Wheels = function() {return {weird: 'constructor'};}; var Car = function(engine, wheels) { this.engine = engine; this.wheels = wheels; }; var defs = f.registerAll(carsConfig); f.setParameter('car_class', Car); f.setParameter('engine_factory', function(){return new Engine();}); f.setParameter('wheels_class', Wheels); var car = f.getService('car'); car.engine.should.be.an.instanceOf(Engine); car.wheels.weird.should.equal('constructor'); }); it('should throw when not set', function(){ var defs = f.registerAll(carsConfig); (function(){ var car = f.getService('bogus'); }).should.throw(); (function(){ var car = f.getService('car'); //constructor param is not set }).should.throw(); (function(){ var car = f.getService('engine'); //constructor param is not set as factory }).should.throw(); f.setParameter('engine_factory', function(){ }); (function(){ f.get('engine'); //engine factory does not factor something }).should.throw(); }); }); describe('#findTaggedServiceIds()', function(){ it("Return tagged service ids", function(){ f.registerAll(castleConfig); var ids = f.findTaggedServiceIds('inanimate'); ids.length.should.equal(3); ids[0].should.equal('castle_fret'); ids = f.findTaggedServiceIds('building'); ids.length.should.equal(2); }); }); describe('#get()', function(){ it("should parameter take priority of service", function(){ f.registerAll(carsConfig); f.setParameter('car_class', function() {}); f.setParameter('engine_factory', function(){return {};}); f.setParameter('wheels_class', function(){}); var car = f.get('car'); (typeof car).should.equal('object'); f.getDefinition('car').service.should.equal(car); f.setParameter('car', 'hey'); f.get('car').should.equal('hey'); }); it("gettign a shared service twice should return the same instance", function(){ f.registerAll(carsConfig); f.setParameter('car_class', function() {}); f.setParameter('engine_factory', function(){return {};}); f.setParameter('wheels_class', function(){}); var engine1 = f.get('engine'); var engine2 = f.get('engine'); engine1.should.equal(engine2); }); }); }); },{"./../src/definition.js":1,"./../src/freight.js":2,"./examples/cars.json":3,"./examples/castle.json":4}]},{},[5]) ;