├── lib ├── Config.ts ├── annotations │ ├── Dependency.ts │ ├── SingletonAnnotation.ts │ └── Inject.ts ├── PrototypeProvidedDependency.ts ├── ProvidedDependency.ts ├── InjectionRequest.ts ├── NamedProvidedDependency.ts ├── Deps.ts ├── PrototypeInjectionRequest.ts ├── Singleton.ts ├── BaseInjectionRequest.ts ├── NamedInjectionRequest.ts ├── Injector.ts └── Context.ts ├── .gitignore ├── typings.d.ts ├── .travis.yml ├── index.ts ├── tsconfig.json ├── test-resources └── UsingSingleton.ts ├── tsd.json ├── typings.json ├── package.json ├── test ├── ContextTest.ts ├── SingletonTest.ts ├── PrototypeInjectionTest.ts └── NamedInjectionTest.ts └── README.md /lib/Config.ts: -------------------------------------------------------------------------------- 1 | 2 | var Config = { 3 | useGetters: true 4 | }; 5 | 6 | export = Config; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | **.js 3 | **.js.map 4 | 5 | .idea 6 | node_modules 7 | typings 8 | build 9 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ts-dependency-injection' { 2 | import main = require('index'); 3 | export = main; 4 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "0.11" 5 | - "0.10" 6 | - "iojs" 7 | - "iojs-v1.0.4" -------------------------------------------------------------------------------- /lib/annotations/Dependency.ts: -------------------------------------------------------------------------------- 1 | function Dependency(constructor) { 2 | throw new Error("Deprecated"); 3 | } 4 | 5 | export = Dependency; -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * typescript-dependency-injection 3 | * Copyright(c) 2015 Tom Guillermin 4 | * MIT Licensed 5 | */ 6 | 'use strict'; 7 | 8 | import Deps = require('./lib/Deps'); 9 | export = Deps; 10 | -------------------------------------------------------------------------------- /lib/PrototypeProvidedDependency.ts: -------------------------------------------------------------------------------- 1 | 2 | import ProvidedDependency = require('./ProvidedDependency'); 3 | 4 | class PrototypeProvidedDependency extends ProvidedDependency { 5 | 6 | } 7 | 8 | export = PrototypeProvidedDependency; -------------------------------------------------------------------------------- /lib/annotations/SingletonAnnotation.ts: -------------------------------------------------------------------------------- 1 | 2 | import SingletonClass = require('../Singleton'); 3 | 4 | function Singleton(constructor) { 5 | // Register it in the singleton registry 6 | SingletonClass.addSingleton(constructor); 7 | } 8 | 9 | export = Singleton; -------------------------------------------------------------------------------- /lib/ProvidedDependency.ts: -------------------------------------------------------------------------------- 1 | 2 | class ProvidedDependency { 3 | 4 | protected instance: any; 5 | 6 | constructor(instance: any) { 7 | this.instance = instance; 8 | } 9 | 10 | getInstance(): any { 11 | return this.instance; 12 | } 13 | 14 | } 15 | 16 | export = ProvidedDependency; -------------------------------------------------------------------------------- /lib/InjectionRequest.ts: -------------------------------------------------------------------------------- 1 | 2 | import ProvidedDependency = require('./ProvidedDependency'); 3 | 4 | interface InjectionRequest { 5 | 6 | matches(value: ProvidedDependency): boolean; 7 | load(target: ProvidedDependency, value: ProvidedDependency): void; 8 | 9 | } 10 | 11 | export = InjectionRequest; -------------------------------------------------------------------------------- /lib/NamedProvidedDependency.ts: -------------------------------------------------------------------------------- 1 | 2 | import ProvidedDependency = require('./ProvidedDependency'); 3 | 4 | class NamedProvidedDependency extends ProvidedDependency { 5 | 6 | private name: string; 7 | 8 | constructor(instance: any, name: string) { 9 | super(instance); 10 | this.name = name; 11 | } 12 | 13 | getName(): string { 14 | return this.name; 15 | } 16 | 17 | } 18 | 19 | export = NamedProvidedDependency; -------------------------------------------------------------------------------- /lib/Deps.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Tom on 02/07/2015. 3 | */ 4 | 5 | export import Context = require('./Context'); 6 | 7 | export import Dependency = require('./annotations/Dependency'); 8 | import Inject = require('./annotations/Inject'); 9 | export var Injection = Inject.Injection; 10 | export var NamedInjection = Inject.NamedInjection; 11 | export var AutoInject = Inject.AutoInject; 12 | export var DirectLoad = Inject.DirectLoad; 13 | export import Singleton = require('./annotations/SingletonAnnotation'); 14 | 15 | export import Config = require('./Config'); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitAny": false, 5 | "outDir": "./build", 6 | "sourceMap": true, 7 | "target": "ES5", 8 | "declaration": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true 11 | }, 12 | "files": [ 13 | "index.ts", 14 | "test/ContextTest.ts", 15 | "test/NamedInjectionTest.ts", 16 | "test/PrototypeInjectionTest.ts", 17 | "test/SingletonTest.ts", 18 | "test-resources/UsingSingleton.ts", 19 | "typings/main.d.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /test-resources/UsingSingleton.ts: -------------------------------------------------------------------------------- 1 | import Deps = require('../index'); 2 | 3 | export import Config = Deps.Config; 4 | 5 | @Deps.Singleton 6 | export class MySingleton { 7 | public singletonMethod(): void { 8 | console.log("Hello!"); 9 | } 10 | } 11 | 12 | export class MyClass { 13 | 14 | @Deps.AutoInject(MySingleton) 15 | public attr: MySingleton; 16 | 17 | public myMethod(): void { 18 | console.log("This is my method!"); 19 | } 20 | 21 | } 22 | 23 | @Deps.DirectLoad 24 | export class MyClassWithDirectLoad { 25 | 26 | @Deps.AutoInject(MySingleton) 27 | public attr: MySingleton; // = null; 28 | 29 | public myMethod(): void { 30 | console.log("This is another method!"); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /tsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v4", 3 | "repo": "borisyankov/DefinitelyTyped", 4 | "ref": "master", 5 | "path": "typings", 6 | "bundle": "typings/tsd.d.ts", 7 | "installed": { 8 | "mocha/mocha.d.ts": { 9 | "commit": "625cd68fed45e3230393af71bcde1d36706f3d56" 10 | }, 11 | "chai/chai.d.ts": { 12 | "commit": "625cd68fed45e3230393af71bcde1d36706f3d56" 13 | }, 14 | "sinon/sinon.d.ts": { 15 | "commit": "625cd68fed45e3230393af71bcde1d36706f3d56" 16 | }, 17 | "log4js/log4js.d.ts": { 18 | "commit": "a213223e1bdf40b280ebf765b42e7ef3da9394dc" 19 | }, 20 | "express/express.d.ts": { 21 | "commit": "a213223e1bdf40b280ebf765b42e7ef3da9394dc" 22 | }, 23 | "serve-static/serve-static.d.ts": { 24 | "commit": "a213223e1bdf40b280ebf765b42e7ef3da9394dc" 25 | }, 26 | "mime/mime.d.ts": { 27 | "commit": "a213223e1bdf40b280ebf765b42e7ef3da9394dc" 28 | }, 29 | "node/node.d.ts": { 30 | "commit": "a213223e1bdf40b280ebf765b42e7ef3da9394dc" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/PrototypeInjectionRequest.ts: -------------------------------------------------------------------------------- 1 | 2 | import InjectionRequest = require('./InjectionRequest'); 3 | import BaseInjectionRequest = require('./BaseInjectionRequest'); 4 | 5 | import ProvidedDependency = require('./ProvidedDependency'); 6 | 7 | /** 8 | * This class represents an injection request based on the prototype. 9 | * This means that any provided instance that is using the prototype in the request 10 | * will be matched. 11 | */ 12 | class PrototypeInjectionRequest extends BaseInjectionRequest implements InjectionRequest { 13 | 14 | public valuePrototype; 15 | 16 | constructor(propertyKey: string, targetPrototype, valuePrototype) { 17 | if (typeof(valuePrototype) == "function") { 18 | throw new Error("Should pass the prototype for the value '"+valuePrototype.name+"', not its constructor!"); 19 | } 20 | super(propertyKey, targetPrototype); 21 | this.valuePrototype = valuePrototype; 22 | } 23 | 24 | public matches(value: ProvidedDependency): boolean { 25 | return this.valuePrototype.isPrototypeOf(value.getInstance()); 26 | } 27 | } 28 | 29 | export = PrototypeInjectionRequest; 30 | -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-dependency-injection", 3 | "ambientDependencies": { 4 | "mocha": "github:DefinitelyTyped/DefinitelyTyped/mocha/mocha.d.ts#625cd68fed45e3230393af71bcde1d36706f3d56", 5 | "chai": "github:DefinitelyTyped/DefinitelyTyped/chai/chai.d.ts#625cd68fed45e3230393af71bcde1d36706f3d56", 6 | "sinon": "github:DefinitelyTyped/DefinitelyTyped/sinon/sinon.d.ts#625cd68fed45e3230393af71bcde1d36706f3d56", 7 | "log4js": "github:DefinitelyTyped/DefinitelyTyped/log4js/log4js.d.ts#a213223e1bdf40b280ebf765b42e7ef3da9394dc", 8 | "express": "github:DefinitelyTyped/DefinitelyTyped/express/express.d.ts#a213223e1bdf40b280ebf765b42e7ef3da9394dc", 9 | "serve-static": "github:DefinitelyTyped/DefinitelyTyped/serve-static/serve-static.d.ts#a213223e1bdf40b280ebf765b42e7ef3da9394dc", 10 | "mime": "github:DefinitelyTyped/DefinitelyTyped/mime/mime.d.ts#a213223e1bdf40b280ebf765b42e7ef3da9394dc", 11 | "node": "github:DefinitelyTyped/DefinitelyTyped/node/node.d.ts#a213223e1bdf40b280ebf765b42e7ef3da9394dc", 12 | "reflect-metadata":"github:rbuckton/ReflectDecorators/reflect-metadata.d.ts" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-dependency-injection", 3 | "version": "1.2.2", 4 | "author": "Tom Guillermin ", 5 | "description": "This TypeScript library allows you to easily declare and resolve dependencies, injecting them inyour classes attributes, using eye-candy TypeScript annotations.", 6 | "main": "build/index.js", 7 | "typings": "build/index.d.ts", 8 | "dependencies": { 9 | "log4js": "^0.6.26", 10 | "reflect-metadata": "^0.1.0" 11 | }, 12 | "devDependencies": { 13 | "chai": "^3.0.0", 14 | "mocha": "^2.2.5", 15 | "sinon": "^1.15.4", 16 | "typescript": "^1.7.5", 17 | "typings": "^0.6.8" 18 | }, 19 | "scripts": { 20 | "test": "mocha build/test", 21 | "make-dev": "typings install; tsc;", 22 | "prepublish": "typings install; tsc;" 23 | }, 24 | "engines": { 25 | "node": ">= 0.10.0" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://www.github.com/toms-dev/TypeScriptDependencyInjection.git" 30 | }, 31 | "keywords": [ 32 | "dependency", 33 | "injection", 34 | "typescript", 35 | "annotation", 36 | "decorator", 37 | "framework" 38 | ], 39 | "files": [ 40 | "README.md", 41 | "build/" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /lib/Singleton.ts: -------------------------------------------------------------------------------- 1 | import log4js = require('log4js'); 2 | var log = log4js.getLogger(); 3 | 4 | class Singleton { 5 | 6 | private static classes: any[] = []; 7 | 8 | private construct: new () => T; 9 | private instance: T; 10 | 11 | constructor(classConstructor: new () => T) { 12 | this.construct = classConstructor; 13 | } 14 | 15 | public get(): T { 16 | log.debug("Getting singleton!"); 17 | if (! this.instance) { 18 | log.debug("Creating new instance!"); 19 | this.instance = new (this.construct)(); 20 | } 21 | 22 | return this.instance; 23 | } 24 | 25 | public static addSingleton(constructor): void { 26 | if (Singleton.classes.indexOf(constructor) == -1) { 27 | Singleton.classes.push(new Singleton(constructor)); 28 | } 29 | } 30 | 31 | public static getSingleton(construct): Singleton { 32 | var matching = Singleton.classes.filter(function(currentSingleton) { 33 | return currentSingleton.construct == construct; 34 | }); 35 | 36 | if (matching.length == 0) { 37 | throw new Error("No such singleton '"+construct.name+"'"); 38 | } 39 | if (matching.length > 1) { 40 | throw new Error("Same singleton was declared twice. How is that possible?"); 41 | } 42 | 43 | return matching[0]; 44 | } 45 | 46 | } 47 | 48 | export = Singleton; 49 | -------------------------------------------------------------------------------- /lib/BaseInjectionRequest.ts: -------------------------------------------------------------------------------- 1 | import ProvidedDependency = require('./ProvidedDependency'); 2 | 3 | /** 4 | * This class represents an injection request based on the prototype. 5 | * This means that any provided instance that is using the prototype in the request 6 | * will be matched. 7 | */ 8 | class BaseInjectionRequest { 9 | 10 | protected targetPrototype: Object; 11 | private propertyKey: string; 12 | private loadingCallback: Function; 13 | 14 | constructor(propertyKey: string, targetPrototype) { 15 | if (typeof(targetPrototype) == "function") { 16 | throw new Error("Should pass the prototype for the target '"+targetPrototype.name+"', not its constructor!"); 17 | } 18 | 19 | this.targetPrototype = targetPrototype; 20 | this.propertyKey = propertyKey; 21 | 22 | this.loadingCallback = function(value) { 23 | this[propertyKey] = value; 24 | } 25 | } 26 | 27 | 28 | public matches(value): boolean { 29 | throw "Not overriden"; 30 | } 31 | 32 | public load(target: ProvidedDependency, value: ProvidedDependency): void { 33 | this.loadingCallback.apply(target.getInstance(), [value.getInstance()]); 34 | } 35 | 36 | public toString(): string { 37 | var requestClassName = ( this.constructor).name; 38 | var targetClassName = ( this.targetPrototype).constructor.name; 39 | return requestClassName+"@"+targetClassName+"."+this.propertyKey; 40 | } 41 | } 42 | 43 | export = BaseInjectionRequest; 44 | -------------------------------------------------------------------------------- /test/ContextTest.ts: -------------------------------------------------------------------------------- 1 | import chai = require('chai'); 2 | import sinon = require('sinon'); 3 | 4 | import Deps = require('../index'); 5 | 6 | var assert = chai.assert; 7 | 8 | class Dependency1 { 9 | } 10 | class Dependency2 { 11 | } 12 | 13 | class MyClass { 14 | 15 | @Deps.Injection(Dependency1) 16 | public protoDep1: Dependency1; 17 | 18 | @Deps.Injection(Dependency2) 19 | public protoDep2: Dependency2; 20 | 21 | @Deps.NamedInjection("my_dep") 22 | public namedDep: any; 23 | } 24 | 25 | describe("Context testing", () => { 26 | var c1: Deps.Context, 27 | c2: Deps.Context, 28 | instance: MyClass, 29 | dep1: Dependency1, 30 | dep2: Dependency2; 31 | 32 | beforeEach(() => { 33 | c1 = new Deps.Context(); 34 | c2 = new Deps.Context(); 35 | instance = new MyClass(); 36 | dep1 = new Dependency1(); 37 | dep2 = new Dependency2(); 38 | }); 39 | 40 | it("should not inject in another context", () => { 41 | c1.addValue(instance); 42 | c1.addValue(dep1); 43 | c2.addValue(dep2); 44 | 45 | c1.resolve(); 46 | c2.resolve(); 47 | 48 | assert.equal(instance.protoDep1, dep1, "protoDep1 has the right value"); 49 | assert.isUndefined(instance.protoDep2, "protoDep2 is undefined"); 50 | }); 51 | 52 | it("should throw an error if a dependency is not met, only in strict mode", () => { 53 | c1.addValue(instance); 54 | 55 | chai.expect(() => { 56 | c1.resolve(); 57 | }).to.not.throw(Error); 58 | 59 | chai.expect(() => { 60 | c1.resolveStrict(); 61 | }).to.throw(Error); 62 | }); 63 | 64 | }); 65 | -------------------------------------------------------------------------------- /lib/NamedInjectionRequest.ts: -------------------------------------------------------------------------------- 1 | import ProvidedDependency = require('./ProvidedDependency'); 2 | import NamedProvidedDependency = require('./NamedProvidedDependency'); 3 | 4 | import InjectionRequest = require('./InjectionRequest'); 5 | import BaseInjectionRequest = require('./BaseInjectionRequest'); 6 | 7 | /** 8 | * This class represents an injection request based on the prototype. 9 | * This means that any provided instance that is using the prototype in the request 10 | * will be matched. 11 | */ 12 | class NamedInjectionRequest extends BaseInjectionRequest implements InjectionRequest { 13 | 14 | public valueName: string; 15 | public valuePrototype; 16 | 17 | constructor(propertyKey: string, targetPrototype, name, valuePrototype?) { 18 | if (valuePrototype && typeof(valuePrototype) == "function") { 19 | throw new Error("Should pass the prototype for the value '"+valuePrototype.name+"', not its constructor!"); 20 | } 21 | 22 | super(propertyKey, targetPrototype); 23 | 24 | this.valueName = name; 25 | this.valuePrototype = valuePrototype; 26 | } 27 | 28 | public matches(value: ProvidedDependency): boolean { 29 | if (! (value instanceof NamedProvidedDependency)) { 30 | return false; 31 | } 32 | var namedDep = value; 33 | 34 | var nameMatch = this.valueName == namedDep.getName(); 35 | var prototypeMatch = this.valuePrototype ? this.valuePrototype.isPrototypeOf(value.getInstance()) : true; 36 | 37 | return nameMatch && prototypeMatch; 38 | } 39 | 40 | 41 | public toString():string { 42 | var suffix = ""; 43 | if (this.valuePrototype) { 44 | suffix += this.valuePrototype.constructor.name+"@"; 45 | } 46 | suffix += this.valueName; 47 | return super.toString()+"="+suffix; 48 | } 49 | } 50 | 51 | export = NamedInjectionRequest; 52 | -------------------------------------------------------------------------------- /lib/Injector.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import InjectionRequest = require('./InjectionRequest'); 3 | import ProvidedDependency = require('./ProvidedDependency'); 4 | 5 | import log4js = require('log4js'); 6 | var log = log4js.getLogger(); 7 | 8 | class DependencyInjector { 9 | 10 | public static getRequests(target:ProvidedDependency):InjectionRequest[] { 11 | log.debug("Target:", target.getInstance()); 12 | var instance = target.getInstance(); 13 | 14 | // Skip primitive objects 15 | var isPrimitive = typeof(instance) != "object"; 16 | if (isPrimitive) return []; 17 | 18 | var proto = Object.getPrototypeOf(instance); 19 | 20 | var DEP_INJ_REQUESTS_KEY = "dependencyInjection.requests"; 21 | var requests:InjectionRequest[] = Reflect.getMetadata(DEP_INJ_REQUESTS_KEY, proto); //proto.__injectionRequests || []; 22 | if (requests) { 23 | log.debug("Requests of " + proto.constructor.name + ":", requests.length); 24 | } 25 | return requests ? requests : []; 26 | } 27 | 28 | public resolveRequest(r:InjectionRequest, providedInstance:ProvidedDependency, deps:ProvidedDependency[], 29 | strict:boolean):void { 30 | var matchingDeps:ProvidedDependency[] = []; 31 | for (var i in deps) { 32 | if (!deps.hasOwnProperty(i)) continue; 33 | var d = deps[i]; 34 | 35 | // Skip self-injection 36 | if (d.getInstance() == providedInstance.getInstance()) continue; 37 | 38 | // Check if the dependency matches the request 39 | if (r.matches(d)) { 40 | matchingDeps.push(d); 41 | } 42 | } 43 | log.debug(matchingDeps.length + " matching requests for ", providedInstance.getInstance().constructor.name); 44 | // Throw an error if more than one dependency matches the request, as the context is ambiguous. 45 | if (matchingDeps.length > 1) { 46 | throw new Error("Ambiguous context with " + matchingDeps.length + " matching dependencies."); 47 | } 48 | // Throw an error or warn if no provided dependency fulfills the request 49 | else if (matchingDeps.length == 0) { 50 | var message = r.toString() + " was not resolved."; 51 | if (strict) { 52 | throw new Error(message); 53 | } else { 54 | log.warn("WARN: " + message); 55 | } 56 | return; 57 | } 58 | 59 | var injectedDep = matchingDeps[0]; 60 | r.load(providedInstance, injectedDep); 61 | } 62 | 63 | } 64 | 65 | export = DependencyInjector; 66 | -------------------------------------------------------------------------------- /lib/Context.ts: -------------------------------------------------------------------------------- 1 | import DependencyInjector = require('./Injector'); 2 | 3 | import ProvidedDependency = require('./ProvidedDependency'); 4 | import PrototypeProvidedDependency = require('./PrototypeProvidedDependency'); 5 | import NamedProvidedDependency = require('./NamedProvidedDependency'); 6 | 7 | import InjectionRequest = require('./InjectionRequest'); 8 | 9 | import log4js = require('log4js'); 10 | var logger = log4js.getLogger("Context"); 11 | 12 | /** 13 | * Data class to hold the link between request and instance 14 | */ 15 | class InjectionRequestInstance { 16 | 17 | request: InjectionRequest; 18 | instance: ProvidedDependency; 19 | 20 | public constructor(request:InjectionRequest, instance:any) { 21 | this.request = request; 22 | this.instance = instance; 23 | } 24 | } 25 | 26 | 27 | class DependencyInjectionContext { 28 | 29 | private providedDependencies: ProvidedDependency[]; 30 | 31 | private injector:DependencyInjector; 32 | 33 | private requests: InjectionRequestInstance[]; 34 | 35 | constructor() { 36 | this.providedDependencies = []; 37 | this.injector = new DependencyInjector(); 38 | } 39 | 40 | private loadRequests(dep: ProvidedDependency): void { 41 | var self = this; 42 | DependencyInjector.getRequests(dep).map(function(r) { 43 | self.requests.push(new InjectionRequestInstance(r, dep)); 44 | }); 45 | } 46 | 47 | public addValue(instance:any, name?: string):void { 48 | if (name) { 49 | logger.debug("Adding named dep: {}", name); 50 | this.addNamedValue(instance, name); 51 | //this.providedDependencies.push(new NamedProvidedDependency(instance, name)); 52 | } else { 53 | this.providedDependencies.push(new PrototypeProvidedDependency(instance)); 54 | } 55 | } 56 | 57 | public addNamedValue(instance:any, name:string):void { 58 | // Store the dependency 59 | this.providedDependencies.push(new NamedProvidedDependency(instance, name)); 60 | } 61 | 62 | public resolve(strict = false):void { 63 | // Build the request list 64 | this.requests = []; 65 | for (var i in this.providedDependencies) { 66 | if (! this.providedDependencies.hasOwnProperty(i)) continue; 67 | var dep = this.providedDependencies[i]; 68 | this.loadRequests(dep); 69 | } 70 | 71 | // Resolve the requests 72 | for (var i in this.requests) { 73 | if (! this.requests.hasOwnProperty(i)) continue; 74 | var r = this.requests[i]; 75 | this.injector.resolveRequest(r.request, r.instance, this.providedDependencies, strict); 76 | } 77 | } 78 | 79 | public resolveStrict(): void { 80 | this.resolve(true); 81 | } 82 | 83 | } 84 | 85 | export = DependencyInjectionContext; 86 | -------------------------------------------------------------------------------- /test/SingletonTest.ts: -------------------------------------------------------------------------------- 1 | import chai = require('chai'); 2 | import sinon = require('sinon'); 3 | 4 | import Deps = require('../index'); 5 | 6 | var assert = chai.assert; 7 | 8 | describe("Singleton unit test", () => { 9 | it("should have the value available", () => { 10 | var test = require('../test-resources/UsingSingleton'); 11 | 12 | var a = new test.MyClass(); 13 | assert.isDefined(a.attr); 14 | assert.isNotNull(a.attr); 15 | assert.instanceOf(a.attr, test.MySingleton); 16 | 17 | var b = new test.MyClassWithDirectLoad(); 18 | assert.equal(Object.keys(b).length, 1, "Object that are "); 19 | }); 20 | it("should share the same singleton instance accross multiple values", () => { 21 | var test = require('../test-resources/UsingSingleton'); 22 | 23 | var a = new test.MyClass(); 24 | var b = new test.MyClass(); 25 | 26 | assert.equal(a.attr, b.attr, "instance are shared"); 27 | }); 28 | 29 | describe("should use the Config", () => { 30 | it("should load the same config object", () => { 31 | var test = require('../test-resources/UsingSingleton'); 32 | assert.equal(test.Config, Deps.Config, "Configs should be the same"); 33 | }); 34 | describe("useGetters", () => { 35 | describe("useGetters = false", () => { 36 | var test; 37 | var warningSpy; 38 | before(() => { 39 | Deps.Config.useGetters = false; 40 | test = require('../test-resources/UsingSingleton'); 41 | var log4js = require('log4js'); 42 | var loggerInjector = log4js.getLogger(); 43 | warningSpy = sinon.spy(loggerInjector, "warn"); 44 | }); 45 | 46 | it("should contains the injected attributes in the keys after a getter call", () => { 47 | var a = new test.MyClass(); 48 | assert.isTrue(Object.keys(a).indexOf("attr") == -1, "The key is not set yet"); 49 | a.attr; // trigger the loading 50 | assert.isTrue(Object.keys(a).indexOf("attr") != -1, "The key is set"); 51 | }); 52 | 53 | it("should warn that not using @DirectLoad is a perf killer...", () => { 54 | var a = new test.MyClass(); 55 | a.attr; 56 | assert.isTrue(warningSpy.calledOnce); 57 | }); 58 | it("should not warn when using @DirectLoad", () => { 59 | warningSpy.reset(); 60 | var b = new test.MyClassWithDirectLoad(); 61 | b.attr; 62 | assert.isFalse(warningSpy.called); 63 | }); 64 | }); 65 | 66 | describe("useGetter = true", () => { 67 | var test; 68 | before(() => { 69 | Deps.Config.useGetters = true; 70 | test = require('../test-resources/UsingSingleton'); 71 | }); 72 | 73 | it("is not expected to have the object in the keys because we're using an internal getter", () => { 74 | var a = new test.MyClass(); 75 | a.attr; // just to trigger the getter 76 | assert.isTrue(Object.keys(a).indexOf("attr") == -1, 77 | "Object.keys(a) does not contain the key 'attr'"); 78 | }); 79 | }) 80 | }) 81 | }); 82 | 83 | it("should have the value set directly if using @DirectLoad", () => { 84 | var test = require('../test-resources/UsingSingleton'); 85 | 86 | var a = new test.MyClassWithDirectLoad(); 87 | assert.isTrue(Object.keys(a).indexOf("attr") != -1, "value is set!"); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/PrototypeInjectionTest.ts: -------------------------------------------------------------------------------- 1 | import chai = require('chai'); 2 | import sinon = require('sinon'); 3 | 4 | import Deps = require('../index'); 5 | 6 | var assert = chai.assert; 7 | 8 | class Dependency1 { 9 | } 10 | class Dependency2 { 11 | } 12 | class SubDependency1 extends Dependency1 { 13 | } 14 | 15 | class MyClass { 16 | @Deps.Injection(Dependency1) 17 | public dep:Dependency1; 18 | } 19 | 20 | class MyChildClass extends MyClass { 21 | 22 | } 23 | 24 | class SelfInjectingClass { 25 | @Deps.Injection(SelfInjectingClass) 26 | public dep:SelfInjectingClass; 27 | } 28 | 29 | describe("PrototypeInjection unit test", () => { 30 | var context:Deps.Context, 31 | instance:MyClass, 32 | dep1:Dependency1, 33 | dep2:Dependency2; 34 | beforeEach(() => { 35 | context = new Deps.Context(); 36 | instance = new MyClass(); 37 | dep1 = new Dependency1(); 38 | dep2 = new Dependency2(); 39 | }); 40 | it("should be injected when the prototype matches", () => { 41 | context.addValue(instance); 42 | context.addValue(dep1); 43 | context.resolve(); 44 | 45 | assert.equal(instance.dep, dep1, "The dependence is injected"); 46 | }); 47 | 48 | it("should not be injected when the prototype is different", () => { 49 | context.addValue(instance); 50 | context.addValue(dep2); 51 | 52 | context.resolve(); 53 | assert.isUndefined(instance.dep, "The dependence is undefined"); 54 | }); 55 | 56 | it("should only inject the prototypes that matches", () => { 57 | context.addValue(instance); 58 | context.addValue(dep2); // add a non-matching dependency to the context 59 | context.addValue(dep1); 60 | context.addValue(dep2); // add a non-matching dependency to the context 61 | context.resolve(); 62 | 63 | assert.equal(instance.dep, dep1, "The right dependence is injected"); 64 | }); 65 | 66 | it("should also inject if the target instance is a child of the annotated class", () => { 67 | instance = new MyChildClass(); 68 | context.addValue(instance); 69 | context.addValue(dep1); 70 | context.resolve(); 71 | 72 | assert.equal(instance.dep, dep1, "The dependence is injected"); 73 | }); 74 | 75 | it("should throw an exception if the context is ambiguous", () => { 76 | context.addValue(instance); 77 | context.addValue(dep1); 78 | context.addValue(new Dependency1()); 79 | 80 | chai.expect(() => { 81 | context.resolve(); 82 | }).to.throw(); 83 | }); 84 | it("should throw an exception if the context is ambiguous because of inheritance", () => { 85 | context.addValue(instance); 86 | context.addValue(dep1); 87 | context.addValue(new SubDependency1()); 88 | 89 | chai.expect(() => { 90 | context.resolve(); 91 | }).to.throw(); 92 | }); 93 | 94 | it("should not inject itself", () => { 95 | var self1 = new SelfInjectingClass(); 96 | context.addValue(self1); 97 | context.resolve(); 98 | 99 | assert.isUndefined(self1.dep); 100 | }); 101 | 102 | it("should not create an ambiguous context thanks to self-injection prevention", () => { 103 | var self1 = new SelfInjectingClass(); 104 | var self2 = new SelfInjectingClass(); 105 | context.addValue(self1); 106 | context.addValue(self2); 107 | context.resolve(); 108 | assert.equal(self1.dep, self2); 109 | assert.equal(self2.dep, self1); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /lib/annotations/Inject.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import InjectionRequest = require('../InjectionRequest'); 3 | import PrototypeInjectionRequest = require('../PrototypeInjectionRequest'); 4 | import NamedInjectionRequest = require('../NamedInjectionRequest'); 5 | 6 | import Singleton = require('../Singleton'); 7 | 8 | import Config = require('../Config'); 9 | 10 | import log4js = require('log4js'); 11 | var log = log4js.getLogger(); 12 | 13 | export function Injection(typeToInject) { 14 | return function (target:Object, propertyKey:string | symbol) { 15 | addPrototypeInjectionRequest(target, typeToInject.prototype, propertyKey); 16 | } 17 | } 18 | 19 | export function NamedInjection(name, typeToInject?) { 20 | var proto = typeToInject ? typeToInject.prototype : null; 21 | return function (target:Object, propertyKey:string | symbol) { 22 | addNamedInjectionRequest(target, name, propertyKey, proto); 23 | } 24 | } 25 | 26 | export function AutoInject(dependencyClass) { 27 | log.debug("Auto inject"); 28 | if (!dependencyClass) throw new Error("Missing parameter!"); 29 | return function (prototype, propertyKey) { 30 | log.debug("\tOn "+propertyKey+" as "+dependencyClass.name); 31 | 32 | // We define this as a safeguard if the user doesn't call @DirectLoad 33 | Object.defineProperty(prototype, propertyKey, { 34 | get: function () { 35 | // If we don't have to use getters, we'll use the first call to inject 36 | // all the values 37 | log.debug("Calling getter! ", Config.useGetters); 38 | if (! Config.useGetters) { 39 | var isInjected = Reflect.getOwnMetadata("isInjected", prototype) ? true : false; 40 | if (! isInjected) { 41 | log.warn("Config.useGetters=false but the @"+(DirectLoad).name+" decorator" + 42 | " was not put on the class " +prototype.constructor.name+"."); 43 | removeGettersFromPrototype(prototype); 44 | autoLoadInjectors(this, prototype); 45 | } 46 | } 47 | return Singleton.getSingleton(dependencyClass).get() 48 | }, 49 | enumerable: true, 50 | configurable: true // to be able to remove it from the prototype later 51 | }); 52 | 53 | // We define this if the user wants to use the @DirectLoad annotation 54 | var singletons:{[propertyKey: string]: any} = Reflect.getOwnMetadata("singletonInjectors", prototype) || {}; 55 | singletons[propertyKey] = function () { 56 | this[propertyKey] = Singleton.getSingleton(dependencyClass).get(); 57 | }; 58 | Reflect.defineMetadata("singletonInjectors", singletons, prototype); 59 | 60 | return prototype; 61 | } 62 | } 63 | 64 | function autoLoadInjectors(target, proto) { 65 | Reflect.defineMetadata("isInjected", true, proto); 66 | var singletonInjectors = Reflect.getOwnMetadata("singletonInjectors", proto) || []; 67 | loadSingletonInjectors(singletonInjectors, target); 68 | } 69 | 70 | function loadSingletonInjectors(singletonInjectors, target) { 71 | for (var propertyKey in singletonInjectors) { 72 | if (!singletonInjectors.hasOwnProperty(propertyKey)) continue; 73 | var s = singletonInjectors[propertyKey]; 74 | s.apply(target); 75 | } 76 | } 77 | 78 | function removeGettersFromPrototype(proto) { 79 | var singletonInjectors:{[propertyKey: string]: any} = Reflect.getOwnMetadata("singletonInjectors", proto) || {}; 80 | for (var propertyKey in singletonInjectors) { 81 | if (!singletonInjectors.hasOwnProperty(propertyKey)) continue; 82 | log.debug("\tDeleting proto: "+propertyKey); 83 | if (! delete proto[propertyKey]) { 84 | throw new Error("Failed to delete property getter!"); 85 | } 86 | } 87 | } 88 | 89 | export function DirectLoad(constructor):any { 90 | log.trace("Direct load of "+constructor.name); 91 | var proto = constructor.prototype; 92 | 93 | // Remove the getter override 94 | removeGettersFromPrototype(proto); 95 | 96 | var wrapper = function () { 97 | autoLoadInjectors(this, proto); 98 | constructor.apply(this, arguments) 99 | }; 100 | 101 | wrapper.prototype = proto; 102 | 103 | return wrapper; 104 | } 105 | 106 | function addInjectionRequest(targetPrototype, injectionPrototype, request:InjectionRequest) { 107 | var protoName = targetPrototype.constructor.name; 108 | var injectionName = injectionPrototype ? injectionPrototype.constructor.name : null; 109 | 110 | // Check that the names are valid 111 | // 112 | if (!protoName) throw new Error("Incorrect prototype! No name found!"); 113 | // Throw error if the 'injectionPrototype' is provided but the name is a false-value 114 | if (injectionPrototype && !injectionName) throw new Error("Incorrect prototype! No name found!"); 115 | 116 | // Register the injection request 117 | var protoInjectionRequests:InjectionRequest[]; 118 | var DEP_INJ_REQUESTS_KEY = "dependencyInjection.requests"; 119 | if (!Reflect.hasMetadata(DEP_INJ_REQUESTS_KEY, targetPrototype)) { 120 | protoInjectionRequests = []; 121 | Reflect.defineMetadata(DEP_INJ_REQUESTS_KEY, [], targetPrototype); 122 | } else { 123 | protoInjectionRequests = Reflect.getMetadata(DEP_INJ_REQUESTS_KEY, targetPrototype) 124 | } 125 | protoInjectionRequests.push(request); 126 | Reflect.defineMetadata(DEP_INJ_REQUESTS_KEY, protoInjectionRequests, targetPrototype); 127 | } 128 | 129 | function addPrototypeInjectionRequest(targetPrototype, injectionPrototype, propertyKey) { 130 | var request = new PrototypeInjectionRequest(propertyKey, targetPrototype, injectionPrototype); 131 | addInjectionRequest(targetPrototype, injectionPrototype, request); 132 | } 133 | 134 | function addNamedInjectionRequest(targetPrototype, name:string, propertyKey:string | symbol, injectionPrototype) { 135 | var request = new NamedInjectionRequest( propertyKey, targetPrototype, name, injectionPrototype); 136 | addInjectionRequest(targetPrototype, injectionPrototype, request); 137 | } 138 | -------------------------------------------------------------------------------- /test/NamedInjectionTest.ts: -------------------------------------------------------------------------------- 1 | import chai = require('chai'); 2 | import sinon = require('sinon'); 3 | 4 | import Deps = require('../index'); 5 | 6 | var assert = chai.assert; 7 | 8 | class Dependency1 { 9 | } 10 | class Dependency2 { 11 | } 12 | 13 | class MyClass { 14 | @Deps.NamedInjection("my_dep1", Dependency1) // with explicit prototype 15 | public dep1:Dependency1; 16 | 17 | @Deps.NamedInjection("my_dep2") // no explicit prototype 18 | public dep2: Dependency2; 19 | } 20 | 21 | class MyChildClass extends MyClass { 22 | 23 | } 24 | 25 | class MyClassUsingPrototypeInjection { 26 | @Deps.Injection(Dependency1) // no name here 27 | public dep: Dependency1; 28 | } 29 | 30 | class SelfInjectingClass { 31 | @Deps.NamedInjection("a_friend", SelfInjectingClass) 32 | public dep: SelfInjectingClass; 33 | } 34 | 35 | class AnyClass { 36 | 37 | @Deps.NamedInjection("attr1") 38 | public attr1: any; 39 | 40 | @Deps.NamedInjection("attr2") 41 | public attr2: any; 42 | 43 | @Deps.NamedInjection("attr3") 44 | public attr3: any; 45 | 46 | @Deps.NamedInjection("attr4") 47 | public attr4: any; 48 | 49 | } 50 | 51 | 52 | describe("NamedInjection unit test", () => { 53 | var context: Deps.Context, 54 | instance:MyClass, 55 | dep1:Dependency1, 56 | dep2:Dependency2; 57 | beforeEach(() => { 58 | context = new Deps.Context(); 59 | instance = new MyClass(); 60 | dep1 = new Dependency1(); 61 | dep2 = new Dependency2(); 62 | }); 63 | it("should be injected when the prototype matches", () => { 64 | context.addValue(instance); 65 | context.addNamedValue(dep1, "my_dep1"); 66 | context.addNamedValue(dep2, "my_dep2"); 67 | context.resolve(); 68 | 69 | assert.equal(instance.dep1, dep1, "The dependence is injected"); 70 | }); 71 | 72 | it("should not be injected when the name does not match", () => { 73 | context.addValue(instance); 74 | context.addValue(dep1, "derp"); 75 | context.addValue(dep2, "hello"); 76 | 77 | context.resolve(); 78 | assert.isUndefined(instance.dep1, "The dependence is undefined"); 79 | }); 80 | 81 | it("should not be injected when the prototype is different, even with matching name", () => { 82 | context.addValue(instance); 83 | context.addValue(dep2, "my_dep1"); 84 | 85 | context.resolve(); 86 | assert.isUndefined(instance.dep1, "The dependence is undefined"); 87 | }); 88 | 89 | it("should inject a named provided dependency into an PrototypeInjectionRequest", () => { 90 | var instanceProto = new MyClassUsingPrototypeInjection(); 91 | context.addValue(instanceProto); 92 | context.addValue(dep1, "some_random_name"); 93 | context.resolve(); 94 | 95 | assert.equal(instanceProto.dep, dep1); 96 | }); 97 | 98 | it("should only inject for the name that matches", () => { 99 | context.addValue(instance); 100 | context.addValue(new Dependency1(), "some_name"); // add a non-matching dependency to the context 101 | context.addValue(dep1, "my_dep1"); 102 | context.addValue(new Dependency1(), "some_other_name"); // add a non-matching dependency to the context 103 | context.resolve(); 104 | 105 | assert.equal(instance.dep1, dep1, "The right dependence is injected"); 106 | }); 107 | 108 | it("should also inject if the target instance is a child of the annotated class", () => { 109 | instance = new MyChildClass(); 110 | context.addValue(instance); 111 | context.addValue(dep1, "my_dep1"); 112 | context.resolve(); 113 | 114 | assert.equal(instance.dep1, dep1, "The dependence is injected"); 115 | }); 116 | 117 | it("should be able to inject basic types", () => { 118 | var anyInstance = new AnyClass(); 119 | context.addValue(anyInstance); 120 | context.addValue(1, "attr1"); 121 | context.addValue("message", "attr2"); 122 | 123 | // The spy **replaces** the functions, it does not wrap it 124 | var fakeCallback = sinon.spy(function() { 125 | }); 126 | 127 | context.addValue(fakeCallback, "attr3"); 128 | context.addValue(true, "attr4"); 129 | 130 | context.resolve(); 131 | 132 | assert.isNumber(anyInstance.attr1, "attr1 is number"); 133 | assert.isString(anyInstance.attr2, "attr2 is string"); 134 | assert.isDefined(anyInstance.attr3, "attr3 is defined"); 135 | assert.isFunction(anyInstance.attr3, "attr3 is a function"); 136 | anyInstance.attr3(); 137 | 138 | assert.isTrue(fakeCallback.calledOnce, "fakeCallback is called through attr3"); 139 | }); 140 | 141 | it("should throw an exception if the context is ambiguous because of the names", () => { 142 | context.addValue(instance); 143 | context.addValue(dep1, "my_dep1"); 144 | context.addValue(new Dependency1(), "my_dep1"); 145 | chai.expect(() => { 146 | context.resolve(); 147 | }).to.throw(); 148 | }); 149 | 150 | it("should not inject itself", () => { 151 | var self1 = new SelfInjectingClass(); 152 | context.addValue(self1, "a_friend"); 153 | context.resolve(); 154 | 155 | assert.isUndefined(self1.dep); 156 | }); 157 | 158 | it("should not create an ambiguous context thanks to self-injection mecanism", () => { 159 | var self1 = new SelfInjectingClass(); 160 | var self2 = new SelfInjectingClass(); 161 | context.addValue(self1, "a_friend"); 162 | context.addValue(self2, "a_friend"); 163 | context.resolve(); 164 | assert.equal(self1.dep, self2); 165 | assert.equal(self2.dep, self1); 166 | }); 167 | 168 | it("should allow the resolution of dependencies that has the same names but different prototypes", () => { 169 | context.addValue(instance); 170 | context.addValue(new Dependency1(), "TEST"); 171 | context.addNamedValue(dep2, "my_dep1"); 172 | context.addValue(new Dependency1(), "TEST"); 173 | context.addValue(dep1, "my_dep1"); // this one should be injected 174 | context.addValue(new Dependency1(), "TEST"); 175 | context.addNamedValue(dep2, "my_dep1"); 176 | context.addValue(new Dependency1(), "TEST"); 177 | context.resolve(); 178 | 179 | assert.equal(instance.dep1, dep1, "The right one should be injected"); 180 | }); 181 | }); 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeScript dependency injection library 2 | [![Build Status](https://travis-ci.org/toms-dev/TypeScriptDependencyInjection.svg?branch=master)](https://travis-ci.org/toms-dev/TypeScriptDependencyInjection) 3 | 4 | This TypeScript library allows you to easily declare and resolve dependencies, injecting them in your classes attributes, using eye-candy TypeScript annotations. 5 | 6 | *Author:* Tom Guillermin - [http://www.tomsdev.com](http://www.tomsdev.com) 7 | 8 | [(Shortcut to "Getting started" section)](#getting-started) 9 | 10 | ## Requirements 11 | 12 | - TypeScript compiler 1.5 or higher 13 | - EcmaScript5-compliant engine (nodejs versions >= .0.10 will do fine) 14 | 15 | ## Who is it for? 16 | 17 | This library is primarily aimed at framework developers but any programmer that want clean and concise code will surely enjoy it too! It's a great way to reduce redundant boilerplate code. 18 | 19 | 20 | **Side-note about the terminology:** The official term of the `@Something` syntax in TypeScript is "decorator", but I might inadvertently call it "annotation" quite frequently. 21 | 22 | ## Features 23 | 24 | - **Powerful**. Resolves dependencies by prototype and/or name. 25 | - **Concise**. Using TypeScript annotations will be a real pleasure for your eyes. I promise. 26 | - **Expressive**. By declaring multiple contexts, you have fine control of the resolution process. 27 | - **Safe**. The solver automatically detects ambiguous contexts and prevent unexpected behaviors. 28 | - **Forgiving.** Even if you forget an annotation (eg. @DirectLoad), the framework will warn you and find a way around to make things work. 29 | 30 | ## umh... dependency injection? 31 | 32 | Dependency injection allows you to reduce coupling by **dynamically setting ("injecting") variables** where they need to be. 33 | 34 | For example, let's say I'm writing some controllers and I want to be able to send emails from them. I can write an `EmailService` class and provide dynamically its instance to any controller that requests it. 35 | Dependency injection will allow me to have a clean and unified syntax for both requesting and providing the `EmailService` (see below for an example). 36 | 37 | 38 | 39 | ## Getting started 40 | 41 | First you have to install the library with NPM: 42 | ```bash 43 | $ npm install ts-dependency-injection --save 44 | ``` 45 | 46 | Then import the library in your TypeScript code using: 47 | 48 | ```TypeScript 49 | import Deps = require('ts-dependency-injection''); 50 | ``` 51 | 52 | 53 | ### Manual context resolution 54 | 55 | Then, you can declare a dependency using the following annotation: 56 | ```TypeScript 57 | class MyClass { 58 | 59 | @Deps.Injection(MyDependency) 60 | public dep: MyDependency; 61 | 62 | } 63 | ``` 64 | Here, you are declaring that instances of *MyClass* needs an instance of *MyDependency* to work properly. 65 | 66 | To provide an instance of the dependency to an instance of *MyClass*, you have to 67 | create a **dependency context**. 68 | A context is a way to explicitly define which values are available during the resolution. 69 | 70 | All you have to do is add all the values that participates in the context and run the resolution. 71 | To create the context and resolve the dependencies: 72 | ```TypeScript 73 | // Instantiate everything that has to 74 | var dep = new MyDependency(); 75 | var instance = new MyClass(); 76 | 77 | var context = new Deps.Context(); 78 | 79 | // Provide the values to the context 80 | context.addValue(dep); 81 | context.addValue(instance); 82 | 83 | // Resolve all the dependencies 84 | context.resolve(); 85 | ``` 86 | The dependency matching is performed here on the prototypes, but it can also be performed on names. 87 | 88 | ### Named dependencies 89 | You have the ability to give names to dependencies to avoid collisions. You have to use another annotation, `NamedInjection`: 90 | ```TypeScript 91 | class MyClass { 92 | @Deps.NamedInjection("some_name", MyDependency) 93 | private attr: MyDependency; 94 | } 95 | ``` 96 | Then, you can add the values to the context by specifying their name: 97 | ```TypeScript 98 | context.addNamedValue(new MyDependency(), "some_name"); 99 | // or an equivalent syntax: 100 | context.addValue(new MyDependency(), "some_name"); 101 | ``` 102 | 103 | ```TypeScript 104 | class MyClass { 105 | @Deps.NamedInjection("my dep", MyDependency) 106 | public dep: MyDependency; 107 | } 108 | 109 | // [...] later in the code: 110 | context.addValue(dep, "my dep"); 111 | context.addValue(instance, "an instance"); 112 | ``` 113 | 114 | ## Inheritance 115 | Of course, the resolution support inheritance in the dependencies. 116 | 117 | *Example* 118 | ```TypeScript 119 | class Dep2 extends MyDependency { 120 | // empty class 121 | } 122 | ``` 123 | *Dep2* instances will be successfully matched as a *MyDependency* during the resolution. 124 | 125 | 126 | ### Injecting primitives 127 | You can inject primitive types by name the same way you do with class instances. 128 | The only thing you have to do is adding them to the context: 129 | ```TypeScript 130 | context.addValue(1, "attr1"); // number 131 | context.addValue("message", "attr2"); // string 132 | context.addValue(true, "attr3"); // boolean 133 | context.addValue(function() { // function 134 | console.log("Hello, I was injected !"); 135 | }, "attr4"); 136 | ``` 137 | **Note**: As primitive types do not have a prototype, there is currently no way of directly specifying its type in the annotation. I'm currently working on a solution using *strings* parameters (like this: `@Deps.NamedInjection("attr1", "number")`) but this is experimental. 138 | 139 | ### Automatic injection for singletons 140 | 141 | If you have some singletons classes, you may want to expose them at various places in your code. 142 | This library allows you to automatically instantiate and inject singleton without having anything to do except annotating your class! 143 | 144 | First, declare your singleton: 145 | ```TypeScript 146 | @Deps.Singleton 147 | class MySingleton { 148 | public singletonMethod(): void { 149 | console.log("Hello!"); 150 | } 151 | } 152 | ``` 153 | 154 | Then, request it: 155 | ```TypeScript 156 | class MyClass { 157 | @Deps.AutoInject(MySingleton) 158 | public attr: MySingleton; 159 | } 160 | ``` 161 | And that's it! The singleton is available on every instance of `MyClass`: 162 | ```TypeScript 163 | var a = new MyClass(); 164 | a.attr.singletonMethod(); // prints "Hello!" in the console 165 | ``` 166 | 167 | ### Strict resolution 168 | By default, when you call `context.resolve()`, if a dependency is not found in the context, nothing happens and the class attribute is `undefined` (or whatever default value you provided). 169 | You may want to ensure that all the dependencies were met. To do so, you can use `context.resolveStrict()` or `context.resolve(true)`. The injection system will throw an exception if something's missing. 170 | 171 | ### Ambiguous context 172 | It might happen that when resolving a context, you get an error saying that the context is ambiguous. It means that there are *many possible values* for a *single* injection request, and the injection system can't guess which one has to be used. 173 | 174 | There are two probable causes of ambiguous context error : 175 | 176 | - the context contains **multiple instances** of the **same class** (or of some _inherited classes_) that are **not named** 177 | - the context contains **multiple instances** of the **same class** with the **same name** 178 | 179 | ### Self-injection & same-name injection requests 180 | The injection system will **prevent an instance from injecting into itself** (_"why ?"_). 181 | The benefit of this is that it will **allow** you to have **two instances with the same name and same type in the same context**, to make them cross-inject into one another, 182 | ```TypeScript 183 | class SelfInjectingClass { 184 | @Deps.NamedInjection("a_friend", SelfInjectingClass) 185 | public dep: SelfInjectingClass; 186 | } 187 | 188 | var self1 = new SelfInjectingClass(); 189 | var self2 = new SelfInjectingClass(); 190 | 191 | context.addValue(self1, "a_friend"); 192 | context.addValue(self2, "a_friend"); 193 | context.resolve(); // no error! :) 194 | ``` 195 | 196 | **Note.** It is also possible to use an non-named injection annotation in the class declaration: 197 | ```TypeScript 198 | @Deps.Injection(SelfInjectingClass) 199 | public dep: SelfInjectingClass; 200 | ``` 201 | 202 | 203 | ## todo list 204 | 205 | - ~~named dependencies~~ 206 | - ~~strict context resolution (optional)~~ 207 | - ~~unit tests~~ 208 | - primitive injection by type declaration ("number"/"string"/"boolean") 209 | - ~~singleton dependency magic injection 210 | - context extension : be able to "copy" a context, and add values into this "child" context, without reaffecting 211 | values from the parent context. Example: server context -> match context -> player context 212 | - Documentation : example of Annotation wrapping for framework developpers. 213 | - register user-created singletons 214 | --------------------------------------------------------------------------------