From d60a0539b197ac9c792033db8c735395062f2ff2 Mon Sep 17 00:00:00 2001 From: "Remo H. Jansen" Date: Tue, 21 Mar 2017 21:16:52 +0000 Subject: [PATCH] Support for metadata middleware (#507) * WIP support for metadata middleware * Added unit tests and docs --- src/constants/error_msgs.ts | 2 - src/container/container.ts | 8 + src/container/lookup.ts | 23 ++- src/interfaces/interfaces.ts | 15 ++ src/planning/metadata_reader.ts | 29 ++++ src/planning/planner.ts | 8 +- src/planning/reflection_utils.ts | 34 ++-- test/bugs/bugs.test.ts | 3 +- test/features/metadata_reader.test.ts | 213 ++++++++++++++++++++++++++ test/planning/planner.test.ts | 25 +-- test/resolution/resolver.test.ts | 31 ++-- test/utils/serialization.test.ts | 2 +- wiki/middleware.md | 80 ++++++++++ wiki/module_bundlers.md | 14 +- wiki/purpose.md | 19 ++- 15 files changed, 441 insertions(+), 65 deletions(-) create mode 100644 src/planning/metadata_reader.ts create mode 100644 test/features/metadata_reader.test.ts diff --git a/src/constants/error_msgs.ts b/src/constants/error_msgs.ts index 1686ee4fa..f290270b1 100644 --- a/src/constants/error_msgs.ts +++ b/src/constants/error_msgs.ts @@ -28,5 +28,3 @@ export const CONTAINER_OPTIONS_MUST_BE_AN_OBJECT = "Invalid Container constructo export const CONTAINER_OPTIONS_INVALID_DEFAULT_SCOPE = "Invalid Container option. Default scope must " + "be a string ('singleton' or 'transient')."; - -export const INVALID_BINDING_PROPERTY = "TODO"; diff --git a/src/container/container.ts b/src/container/container.ts index 5dc241bed..cf11d8044 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -10,6 +10,7 @@ import { guid } from "../utils/guid"; import * as ERROR_MSGS from "../constants/error_msgs"; import * as METADATA_KEY from "../constants/metadata_keys"; import { BindingScopeEnum, TargetTypeEnum } from "../constants/literal_types"; +import { MetadataReader } from "../planning/metadata_reader"; class Container implements interfaces.Container { @@ -19,6 +20,7 @@ class Container implements interfaces.Container { private _middleware: interfaces.Next | null; private _bindingDictionary: interfaces.Lookup>; private _snapshots: Array; + private _metadataReader: interfaces.MetadataReader; public static merge(container1: interfaces.Container, container2: interfaces.Container): interfaces.Container { @@ -77,6 +79,7 @@ class Container implements interfaces.Container { this._snapshots = []; this._middleware = null; this.parent = null; + this._metadataReader = new MetadataReader(); } public load(...modules: interfaces.ContainerModule[]): void { @@ -218,6 +221,10 @@ class Container implements interfaces.Container { }, initial); } + public applyCustomMetadataReader(metadataReader: interfaces.MetadataReader) { + this._metadataReader = metadataReader; + } + // Resolves a dependency by its runtime identifier // The runtime identifier must be associated with only one binding // use getAll when the runtime identifier is associated with multiple bindings @@ -291,6 +298,7 @@ class Container implements interfaces.Container { // create a plan let context = plan( + this._metadataReader, this, args.isMultiInject, args.targetType, diff --git a/src/container/lookup.ts b/src/container/lookup.ts index a16d73cc0..7d9f489ae 100644 --- a/src/container/lookup.ts +++ b/src/container/lookup.ts @@ -17,8 +17,13 @@ class Lookup> implements interfaces.Lookup { // adds a new entry to _map public add(serviceIdentifier: interfaces.ServiceIdentifier, value: T): void { - if (serviceIdentifier === null || serviceIdentifier === undefined) { throw new Error(ERROR_MSGS.NULL_ARGUMENT); }; - if (value === null || value === undefined) { throw new Error(ERROR_MSGS.NULL_ARGUMENT); }; + if (serviceIdentifier === null || serviceIdentifier === undefined) { + throw new Error(ERROR_MSGS.NULL_ARGUMENT); + }; + + if (value === null || value === undefined) { + throw new Error(ERROR_MSGS.NULL_ARGUMENT); + }; let entry = this._map.get(serviceIdentifier); if (entry !== undefined) { @@ -32,7 +37,9 @@ class Lookup> implements interfaces.Lookup { // gets the value of a entry by its key (serviceIdentifier) public get(serviceIdentifier: interfaces.ServiceIdentifier): T[] { - if (serviceIdentifier === null || serviceIdentifier === undefined) { throw new Error(ERROR_MSGS.NULL_ARGUMENT); } + if (serviceIdentifier === null || serviceIdentifier === undefined) { + throw new Error(ERROR_MSGS.NULL_ARGUMENT); + } let entry = this._map.get(serviceIdentifier); @@ -46,7 +53,9 @@ class Lookup> implements interfaces.Lookup { // removes a entry from _map by its key (serviceIdentifier) public remove(serviceIdentifier: interfaces.ServiceIdentifier): void { - if (serviceIdentifier === null || serviceIdentifier === undefined) { throw new Error(ERROR_MSGS.NULL_ARGUMENT); } + if (serviceIdentifier === null || serviceIdentifier === undefined) { + throw new Error(ERROR_MSGS.NULL_ARGUMENT); + } if (!this._map.delete(serviceIdentifier)) { throw new Error(ERROR_MSGS.KEY_NOT_FOUND); @@ -67,7 +76,11 @@ class Lookup> implements interfaces.Lookup { // returns true if _map contains a key (serviceIdentifier) public hasKey(serviceIdentifier: interfaces.ServiceIdentifier): boolean { - if (serviceIdentifier === null || serviceIdentifier === undefined) { throw new Error(ERROR_MSGS.NULL_ARGUMENT); } + + if (serviceIdentifier === null || serviceIdentifier === undefined) { + throw new Error(ERROR_MSGS.NULL_ARGUMENT); + } + return this._map.has(serviceIdentifier); } diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index b32c1e9bc..ccd62abb2 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -176,6 +176,7 @@ namespace interfaces { getAll(serviceIdentifier: ServiceIdentifier): T[]; load(...modules: ContainerModule[]): void; unload(...modules: ContainerModule[]): void; + applyCustomMetadataReader(metadataReader: MetadataReader): void; applyMiddleware(...middleware: Middleware[]): void; snapshot(): void; restore(): void; @@ -280,6 +281,20 @@ namespace interfaces { (request: Request | null): boolean; } + export interface MetadataReader { + getConstrucotorMetadata(constructorFunc: Function): ConstructorMetadata; + getPropertiesMetadata(constructorFunc: Function): MetadataMap; + } + + export interface MetadataMap { + [propertyNameOrArgumentIndex: string]: Metadata[]; + } + + export interface ConstructorMetadata { + compilerGeneratedMetadata: Function[]|undefined; + userGeneratedMetadata: MetadataMap; + } + } export { interfaces }; diff --git a/src/planning/metadata_reader.ts b/src/planning/metadata_reader.ts new file mode 100644 index 000000000..b8dc1f02b --- /dev/null +++ b/src/planning/metadata_reader.ts @@ -0,0 +1,29 @@ +import { interfaces } from "../interfaces/interfaces"; +import * as METADATA_KEY from "../constants/metadata_keys"; + +class MetadataReader implements interfaces.MetadataReader { + + public getConstrucotorMetadata(constructorFunc: Function): interfaces.ConstructorMetadata { + + // TypeScript compiler generated annotations + let compilerGeneratedMetadata = Reflect.getMetadata(METADATA_KEY.PARAM_TYPES, constructorFunc); + + // User generated constructor annotations + let userGeneratedMetadata = Reflect.getMetadata(METADATA_KEY.TAGGED, constructorFunc); + + return { + compilerGeneratedMetadata: compilerGeneratedMetadata, + userGeneratedMetadata: userGeneratedMetadata || {} + }; + + } + + public getPropertiesMetadata(constructorFunc: Function): interfaces.MetadataMap { + // User generated properties annotations + let userGeneratedMetadata = Reflect.getMetadata(METADATA_KEY.TAGGED_PROP, constructorFunc) || []; + return userGeneratedMetadata; + } + +} + +export { MetadataReader }; diff --git a/src/planning/planner.ts b/src/planning/planner.ts index 2dab24248..163c70fee 100644 --- a/src/planning/planner.ts +++ b/src/planning/planner.ts @@ -121,6 +121,7 @@ function _validateActiveBindingCount( } function _createSubRequests( + metadataReader: interfaces.MetadataReader, avoidConstraints: boolean, serviceIdentifier: interfaces.ServiceIdentifier, context: interfaces.Context, @@ -165,10 +166,10 @@ function _createSubRequests( if (binding.type === BindingTypeEnum.Instance && binding.implementationType !== null) { - let dependencies = getDependencies(binding.implementationType); + let dependencies = getDependencies(metadataReader, binding.implementationType); dependencies.forEach((dependency: interfaces.Target) => { - _createSubRequests(false, dependency.serviceIdentifier, context, subChildRequest, dependency); + _createSubRequests(metadataReader, false, dependency.serviceIdentifier, context, subChildRequest, dependency); }); } @@ -207,6 +208,7 @@ function getBindings( } function plan( + metadataReader: interfaces.MetadataReader, container: interfaces.Container, isMultiInject: boolean, targetType: interfaces.TargetType, @@ -218,7 +220,7 @@ function plan( let context = new Context(container); let target = _createTarget(isMultiInject, targetType, serviceIdentifier, "", key, value); - _createSubRequests(avoidConstraints, serviceIdentifier, context, null, target); + _createSubRequests(metadataReader, avoidConstraints, serviceIdentifier, context, null, target); return context; } diff --git a/src/planning/reflection_utils.ts b/src/planning/reflection_utils.ts index 346d5bd2a..87e520df4 100644 --- a/src/planning/reflection_utils.ts +++ b/src/planning/reflection_utils.ts @@ -5,16 +5,22 @@ import * as ERROR_MSGS from "../constants/error_msgs"; import * as METADATA_KEY from "../constants/metadata_keys"; import { TargetTypeEnum } from "../constants/literal_types"; -function getDependencies(func: Function): interfaces.Target[] { +function getDependencies( + metadataReader: interfaces.MetadataReader, func: Function +): interfaces.Target[] { let constructorName = getFunctionName(func); - let targets: interfaces.Target[] = getTargets(constructorName, func, false); + let targets: interfaces.Target[] = getTargets(metadataReader, constructorName, func, false); return targets; } -function getTargets(constructorName: string, func: Function, isBaseClass: boolean): interfaces.Target[] { +function getTargets( + metadataReader: interfaces.MetadataReader, constructorName: string, func: Function, isBaseClass: boolean +): interfaces.Target[] { + + let metadata = metadataReader.getConstrucotorMetadata(func); // TypeScript compiler generated annotations - let serviceIdentifiers = Reflect.getMetadata(METADATA_KEY.PARAM_TYPES, func); + let serviceIdentifiers = metadata.compilerGeneratedMetadata; // All types resolved must be annotated with @injectable if (serviceIdentifiers === undefined) { @@ -23,7 +29,7 @@ function getTargets(constructorName: string, func: Function, isBaseClass: boolea } // User generated annotations - let constructorArgsMetadata = Reflect.getMetadata(METADATA_KEY.TAGGED, func) || []; + let constructorArgsMetadata = metadata.userGeneratedMetadata; let keys = Object.keys(constructorArgsMetadata); let hasUserDeclaredUnknownInjections = (func.length === 0 && keys.length > 0); @@ -39,7 +45,7 @@ function getTargets(constructorName: string, func: Function, isBaseClass: boolea ); // Target instances that represent properties to be injected - let propertyTargets = getClassPropsAsTargets(func); + let propertyTargets = getClassPropsAsTargets(metadataReader, func); let targets = [ ...constructorTargets, @@ -49,7 +55,7 @@ function getTargets(constructorName: string, func: Function, isBaseClass: boolea // Throw if a derived class does not implement its constructor explicitly // We do this to prevent errors when a base class (parent) has dependencies // and one of the derived classes (children) has no dependencies - let baseClassDepencencyCount = getBaseClassDepencencyCount(func); + let baseClassDepencencyCount = getBaseClassDepencencyCount(metadataReader, func); if (targets.length < baseClassDepencencyCount) { let error = ERROR_MSGS.ARGUMENTS_LENGTH_MISMATCH_1 + @@ -127,9 +133,9 @@ function getConstructorArgsAsTargets( return targets; } -function getClassPropsAsTargets(func: Function) { +function getClassPropsAsTargets(metadataReader: interfaces.MetadataReader, constructorFunc: Function) { - let classPropsMetadata = Reflect.getMetadata(METADATA_KEY.TAGGED_PROP, func) || []; + let classPropsMetadata = metadataReader.getPropertiesMetadata(constructorFunc); let targets: interfaces.Target[] = []; let keys = Object.keys(classPropsMetadata); @@ -157,11 +163,11 @@ function getClassPropsAsTargets(func: Function) { } // Check if base class has injected properties - let baseConstructor = Object.getPrototypeOf(func.prototype).constructor; + let baseConstructor = Object.getPrototypeOf(constructorFunc.prototype).constructor; if (baseConstructor !== Object) { - let baseTargets = getClassPropsAsTargets(baseConstructor); + let baseTargets = getClassPropsAsTargets(metadataReader, baseConstructor); targets = [ ...targets, @@ -173,7 +179,7 @@ function getClassPropsAsTargets(func: Function) { return targets; } -function getBaseClassDepencencyCount(func: Function): number { +function getBaseClassDepencencyCount(metadataReader: interfaces.MetadataReader, func: Function): number { let baseConstructor = Object.getPrototypeOf(func.prototype).constructor; @@ -181,7 +187,7 @@ function getBaseClassDepencencyCount(func: Function): number { // get targets for base class let baseConstructorName = getFunctionName(func); - let targets = getTargets(baseConstructorName, baseConstructor, true); + let targets = getTargets(metadataReader, baseConstructorName, baseConstructor, true); // get unmanaged metadata let metadata: any[] = targets.map((t: interfaces.Target) => { @@ -198,7 +204,7 @@ function getBaseClassDepencencyCount(func: Function): number { if (dependencyCount > 0 ) { return dependencyCount; } else { - return getBaseClassDepencencyCount(baseConstructor); + return getBaseClassDepencencyCount(metadataReader, baseConstructor); } } else { diff --git a/test/bugs/bugs.test.ts b/test/bugs/bugs.test.ts index fe8e31d3f..1746d59d6 100644 --- a/test/bugs/bugs.test.ts +++ b/test/bugs/bugs.test.ts @@ -4,6 +4,7 @@ import { getFunctionName } from "../../src/utils/serialization"; import * as ERROR_MSGS from "../../src/constants/error_msgs"; import * as METADATA_KEY from "../../src/constants/metadata_keys"; import { getDependencies } from "../../src/planning/reflection_utils"; +import { MetadataReader } from "../../src/planning/metadata_reader"; import { Container, injectable, @@ -552,7 +553,7 @@ describe("Bugs", () => { expect(serviceIdentifiers["0"][0].value.toString()).to.be.eql("Symbol(BAR)"); // is the plan correct? - let dependencies = getDependencies(Foo); + let dependencies = getDependencies(new MetadataReader(), Foo); expect(dependencies.length).to.be.eql(1); expect(dependencies[0].serviceIdentifier.toString()).to.be.eql("Symbol(BAR)"); diff --git a/test/features/metadata_reader.test.ts b/test/features/metadata_reader.test.ts new file mode 100644 index 000000000..604ddcdbb --- /dev/null +++ b/test/features/metadata_reader.test.ts @@ -0,0 +1,213 @@ +import { expect } from "chai"; +import { Container } from "../../src/inversify"; +import { interfaces } from "../../src/interfaces/interfaces"; +import { Metadata } from "../../src/planning/metadata"; +import * as METADATA_KEY from "../../src/constants/metadata_keys"; + +describe("Custom Metadata Reader", () => { + + interface FunctionWithMetadata extends Function { + constructorInjections: interfaces.ServiceIdentifier[]; + propertyInjections: PropertyInjectionMetadata[]; + } + + interface PropertyInjectionMetadata { + propName: string; + injection: interfaces.ServiceIdentifier; + } + + class StaticPropsMetadataReader implements interfaces.MetadataReader { + + public getConstrucotorMetadata(constructorFunc: FunctionWithMetadata): interfaces.ConstructorMetadata { + + const formatMetadata = (injections: interfaces.ServiceIdentifier[]) => { + let userGeneratedMetadata: interfaces.MetadataMap = {}; + injections.forEach((injection, index) => { + let metadata = new Metadata(METADATA_KEY.INJECT_TAG, injection); + if (Array.isArray(userGeneratedMetadata[index])) { + userGeneratedMetadata[index].push(metadata); + } else { + userGeneratedMetadata[index] = [metadata]; + } + }); + return userGeneratedMetadata; + }; + + let constructorInjections = constructorFunc.constructorInjections; + + if (Array.isArray(constructorInjections) === false) { + throw new Error("Missing constructorInjections annotation!"); + } + + let userGeneratedConsturctorMetadata = formatMetadata(constructorInjections); + + return { + // compilerGeneratedMetadata lenght must match userGeneratedMetadata + // we expose compilerGeneratedMetadata because if your custom annotation + // system is powered by decorators. The TypeScript compiler could generate + // some metadata when the emitDecoratorMetadata flag is enabled. + compilerGeneratedMetadata: new Array(constructorInjections.length), + userGeneratedMetadata: userGeneratedConsturctorMetadata + }; + + } + + public getPropertiesMetadata(constructorFunc: FunctionWithMetadata): interfaces.MetadataMap { + + const formatMetadata = (injections: PropertyInjectionMetadata[]) => { + let userGeneratedMetadata: interfaces.MetadataMap = {}; + injections.forEach((propInjection, index) => { + let metadata = new Metadata(METADATA_KEY.INJECT_TAG, propInjection.injection); + if (Array.isArray(userGeneratedMetadata[propInjection.propName])) { + userGeneratedMetadata[propInjection.propName].push(metadata); + } else { + userGeneratedMetadata[propInjection.propName] = [metadata]; + } + }); + return userGeneratedMetadata; + }; + + let propertyInjections = constructorFunc.propertyInjections; + + if (Array.isArray(propertyInjections) === false) { + throw new Error("Missing propertyInjections annotation!"); + } + + let userGeneratedPropertyMetadata = formatMetadata(propertyInjections); + return userGeneratedPropertyMetadata; + + } + + } + + it("Should be able to use custom constructor injection metadata", () => { + + interface Ninja { + fight(): string; + sneak(): string; + } + + interface Katana { + hit(): string; + } + + interface Shuriken { + throw(): string; + } + + class Katana implements Katana { + public static readonly constructorInjections = []; + public static readonly propertyInjections = []; + public hit() { + return "cut!"; + } + } + + class Shuriken implements Shuriken { + public static readonly constructorInjections = []; + public static readonly propertyInjections = []; + public throw() { + return "hit!"; + } + } + + class Ninja implements Ninja { + + public static readonly constructorInjections = ["Katana", "Shuriken"]; + public static readonly propertyInjections = []; + + private _katana: Katana; + private _shuriken: Shuriken; + + public constructor( + katana: Katana, + shuriken: Shuriken + ) { + this._katana = katana; + this._shuriken = shuriken; + } + + public fight() { return this._katana.hit(); }; + public sneak() { return this._shuriken.throw(); }; + + } + + let container = new Container(); + container.applyCustomMetadataReader(new StaticPropsMetadataReader()); + + container.bind("Ninja").to(Ninja); + container.bind("Katana").to(Katana); + container.bind("Shuriken").to(Shuriken); + + let ninja = container.get("Ninja"); + + expect(ninja.fight()).eql("cut!"); + expect(ninja.sneak()).eql("hit!"); + + }); + + it("Should be able to use custom prop injection metadata", () => { + + interface Ninja { + fight(): string; + sneak(): string; + } + + interface Katana { + hit(): string; + } + + interface Shuriken { + throw(): string; + } + + class Katana implements Katana { + public static readonly constructorInjections = []; + public static readonly propertyInjections = []; + public static readonly __brk = 1; // TEMP + public hit() { + return "cut!"; + } + } + + class Shuriken implements Shuriken { + public static readonly constructorInjections = []; + public static readonly propertyInjections = []; + public static readonly __brk = 1; // TEMP + public throw() { + return "hit!"; + } + } + + class Ninja implements Ninja { + + public static readonly constructorInjections = []; + + public static readonly propertyInjections = [ + { propName: "_katana", injection: "Katana" }, + { propName: "_shuriken", injection: "Shuriken" } + ]; + + public static readonly __brk = 1; // TEMP + + private _katana: Katana; + private _shuriken: Shuriken; + public fight() { return this._katana.hit(); }; + public sneak() { return this._shuriken.throw(); }; + + } + + let container = new Container(); + container.applyCustomMetadataReader(new StaticPropsMetadataReader()); + container.bind("Ninja").to(Ninja); + container.bind("Katana").to(Katana); + container.bind("Shuriken").to(Shuriken); + + let ninja = container.get("Ninja"); + + expect(ninja.fight()).eql("cut!"); + expect(ninja.sneak()).eql("hit!"); + + }); + +}); diff --git a/test/planning/planner.test.ts b/test/planning/planner.test.ts index b29bff0d9..4b7852ab5 100644 --- a/test/planning/planner.test.ts +++ b/test/planning/planner.test.ts @@ -10,6 +10,7 @@ import { inject } from "../../src/annotation/inject"; import { multiInject } from "../../src/annotation/multi_inject"; import * as sinon from "sinon"; import * as ERROR_MSGS from "../../src/constants/error_msgs"; +import { MetadataReader } from "../../src/planning/metadata_reader"; describe("Planner", () => { @@ -84,7 +85,7 @@ describe("Planner", () => { container.bind(katanaHandlerId).to(KatanaHandler); // Actual - let actualPlan = plan(container, false, TargetTypeEnum.Variable, ninjaId).plan; + let actualPlan = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId).plan; let actualNinjaRequest = actualPlan.rootRequest; let actualKatanaRequest = actualNinjaRequest.childRequests[0]; let actualKatanaHandlerRequest = actualKatanaRequest.childRequests[0]; @@ -245,7 +246,7 @@ describe("Planner", () => { }; }); - let actualPlan = plan(container, false, TargetTypeEnum.Variable, ninjaId).plan; + let actualPlan = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId).plan; expect(actualPlan.rootRequest.serviceIdentifier).eql(ninjaId); expect(actualPlan.rootRequest.childRequests[0].serviceIdentifier).eql(katanaFactoryId); @@ -288,7 +289,7 @@ describe("Planner", () => { container.bind(weaponId).to(Shuriken); container.bind(weaponId).to(Katana); - let actualPlan = plan(container, false, TargetTypeEnum.Variable, ninjaId).plan; + let actualPlan = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId).plan; // root request has no target expect(actualPlan.rootRequest.serviceIdentifier).eql(ninjaId); @@ -357,7 +358,10 @@ describe("Planner", () => { container.bind(ninjaId).to(Ninja); container.bind(shurikenId).to(Shuriken); - let throwFunction = () => { plan(container, false, TargetTypeEnum.Variable, ninjaId); }; + let throwFunction = () => { + plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + }; + expect(throwFunction).to.throw(`${ERROR_MSGS.NOT_REGISTERED} Katana`); }); @@ -400,7 +404,10 @@ describe("Planner", () => { container.bind(katanaId).to(SharpKatana); container.bind(shurikenId).to(Shuriken); - let throwFunction = () => { plan(container, false, TargetTypeEnum.Variable, ninjaId); }; + let throwFunction = () => { + plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + }; + expect(throwFunction).to.throw(`${ERROR_MSGS.AMBIGUOUS_MATCH} Katana`); }); @@ -438,7 +445,7 @@ describe("Planner", () => { container.bind(weaponId).to(Katana).whenTargetTagged("canThrow", false); container.bind(weaponId).to(Shuriken).whenTargetTagged("canThrow", true); - let actualPlan = plan(container, false, TargetTypeEnum.Variable, ninjaId).plan; + let actualPlan = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId).plan; // root request has no target expect(actualPlan.rootRequest.serviceIdentifier).eql(ninjaId); @@ -468,7 +475,7 @@ describe("Planner", () => { container.bind("Weapon").to(Katana); let throwFunction = () => { - plan(container, false, TargetTypeEnum.Variable, "Weapon"); + plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, "Weapon"); }; expect(throwFunction).to.throw(`${ERROR_MSGS.MISSING_INJECTABLE_ANNOTATION} Katana.`); @@ -501,7 +508,7 @@ describe("Planner", () => { container.bind("Sword").to(Katana); let throwFunction = () => { - plan(container, false, TargetTypeEnum.Variable, "Warrior"); + plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, "Warrior"); }; expect(throwFunction).to.throw(`${ERROR_MSGS.MISSING_INJECT_ANNOTATION} argument 0 in class Ninja.`); @@ -535,7 +542,7 @@ describe("Planner", () => { container.bind("Factory").to(Katana); let throwFunction = () => { - plan(container, false, TargetTypeEnum.Variable, "Ninja"); + plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, "Ninja"); }; expect(throwFunction).to.throw(`${ERROR_MSGS.MISSING_INJECT_ANNOTATION} argument 0 in class Ninja.`); diff --git a/test/resolution/resolver.test.ts b/test/resolution/resolver.test.ts index 5fbb03ee5..65b8eb4d9 100644 --- a/test/resolution/resolver.test.ts +++ b/test/resolution/resolver.test.ts @@ -13,6 +13,7 @@ import { targetName } from "../../src/annotation/target_name"; import * as Proxy from "harmony-proxy"; import * as ERROR_MSGS from "../../src/constants/error_msgs"; import * as sinon from "sinon"; +import { MetadataReader } from "../../src/planning/metadata_reader"; describe("Resolve", () => { @@ -92,7 +93,7 @@ describe("Resolve", () => { container.bind(katanaBladeId).to(KatanaBlade); container.bind(katanaHandlerId).to(KatanaHandler); - let context = plan(container, false, TargetTypeEnum.Variable, ninjaId); + let context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); let ninja = resolve(context); expect(ninja instanceof Ninja).eql(true); @@ -170,7 +171,7 @@ describe("Resolve", () => { container.bind(katanaHandlerId).to(KatanaHandler).inSingletonScope(); // SINGLETON! let bindingDictionary = getBindingDictionary(container); - let context = plan(container, false, TargetTypeEnum.Variable, ninjaId); + let context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); expect(bindingDictionary.get(katanaId)[0].cache === null).eql(true); let ninja = resolve(context); @@ -219,7 +220,7 @@ describe("Resolve", () => { container.bind(ninjaId); // IMPORTAN! (Invalid binding) // context and plan - let context = plan(container, false, TargetTypeEnum.Variable, ninjaId); + let context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); let throwFunction = () => { resolve(context); @@ -289,7 +290,7 @@ describe("Resolve", () => { container.bind(shurikenId).to(Shuriken); container.bind(katanaId).toConstantValue(new Katana(new KatanaHandler(), new KatanaBlade())); // IMPORTANT! - let context = plan(container, false, TargetTypeEnum.Variable, ninjaId); + let context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); let ninja = resolve(context); @@ -401,7 +402,7 @@ describe("Resolve", () => { container.bind(katanaId).to(Katana); container.bind>(newableKatanaId).toConstructor(Katana); // IMPORTANT! - let context = plan(container, false, TargetTypeEnum.Variable, ninjaId); + let context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); let ninja = resolve(context); expect(ninja instanceof Ninja).eql(true); @@ -489,7 +490,7 @@ describe("Resolve", () => { }; }); - let context = plan(container, false, TargetTypeEnum.Variable, ninjaId); + let context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); let ninja = resolve(context); @@ -573,7 +574,7 @@ describe("Resolve", () => { container.bind(katanaHandlerId).to(KatanaHandler); container.bind>(katanaFactoryId).toAutoFactory(katanaId); - let context = plan(container, false, TargetTypeEnum.Variable, ninjaId); + let context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); let ninja = resolve(context); expect(ninja instanceof Ninja).eql(true); @@ -665,7 +666,7 @@ describe("Resolve", () => { }; }); - let context = plan(container, false, TargetTypeEnum.Variable, ninjaId); + let context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); let ninja = resolve(context); @@ -717,7 +718,7 @@ describe("Resolve", () => { container.bind(weaponId).to(Katana).whenTargetTagged("canThrow", false); container.bind(weaponId).to(Shuriken).whenTargetTagged("canThrow", true); - let context = plan(container, false, TargetTypeEnum.Variable, ninjaId); + let context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); let ninja = resolve(context); @@ -763,7 +764,7 @@ describe("Resolve", () => { container.bind(weaponId).to(Katana).whenTargetNamed("strong"); container.bind(weaponId).to(Shuriken).whenTargetNamed("weak"); - let context = plan(container, false, TargetTypeEnum.Variable, ninjaId); + let context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); let ninja = resolve(context); @@ -815,7 +816,7 @@ describe("Resolve", () => { return request.target.name.equals("shuriken"); }); - let context = plan(container, false, TargetTypeEnum.Variable, ninjaId); + let context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); let ninja = resolve(context); @@ -865,7 +866,7 @@ describe("Resolve", () => { container.bind(weaponId).to(Katana); container.bind(weaponId).to(Shuriken); - let context = plan(container, false, TargetTypeEnum.Variable, ninjaId); + let context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); let ninja = resolve(context); @@ -878,7 +879,7 @@ describe("Resolve", () => { container2.bind(ninjaId).to(Ninja); container2.bind(weaponId).to(Katana); - let context2 = plan(container2, false, TargetTypeEnum.Variable, ninjaId); + let context2 = plan(new MetadataReader(), container2, false, TargetTypeEnum.Variable, ninjaId); let ninja2 = resolve(context2); @@ -938,7 +939,7 @@ describe("Resolve", () => { return katana; }); - let context = plan(container, false, TargetTypeEnum.Variable, ninjaId); + let context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); let ninja = resolve(context); @@ -1016,7 +1017,7 @@ describe("Resolve", () => { container.bind(katanaFactoryId).toFunction(katanaFactory); - let context = plan(container, false, TargetTypeEnum.Variable, ninjaId); + let context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); let ninja = resolve(context); diff --git a/test/utils/serialization.test.ts b/test/utils/serialization.test.ts index 8f5bd5b5d..0e90eda63 100644 --- a/test/utils/serialization.test.ts +++ b/test/utils/serialization.test.ts @@ -3,7 +3,7 @@ import { getFunctionName, listMetadataForTarget } from "../../src/utils/serializ import { Target } from "../../src/planning/target"; import { TargetTypeEnum } from "../../src/constants/literal_types"; -describe("serialization", () => { +describe("Serialization", () => { it("Should return a good function name", () => { diff --git a/wiki/middleware.md b/wiki/middleware.md index dac702e74..00540ba82 100644 --- a/wiki/middleware.md +++ b/wiki/middleware.md @@ -98,3 +98,83 @@ function middleware1(planAndResolve: PlanAndResolve): PlanAndResolve { }; } ``` + +## Custom metadata reader + +> :warning: Please note that it is not recommended to create your own custom +> metadata reader. We have included this feature two allow library / framework creators +> to have a higher level of customization but the average user should not use a custom +> metadata reader. In general, a custom metadata reader should only be used when +> developing a framework in order to provide users with an annotation APIs +> less explicit than the default anotation API. +> +> If you are developing a framework or library and you create a custom metadata reader, +> Please remember to provide your framework with support for an alternative for all the +> decorators in the default API: `@injectable, ``@inject`, `@multiInject`, `@tagged`, +> `@named`, `@optional`, `@targetName` & `@unmanaged`. + +Middleware allows you to intercept a plan and resolve it but you are not allowed to change the way the annotation phase behaves. + +There is a second extension point that allows you to decide what kind of annotation +system you would like to use. The default annotation system is powered by decorators and +reflect-metadata: + +```ts +@injectable() +class Ninja implements Ninja { + + private _katana: Katana; + private _shuriken: Shuriken; + + public constructor( + @inject("Katana") katana: Katana, + @inject("Shuriken") shuriken: Shuriken + ) { + this._katana = katana; + this._shuriken = shuriken; + } + + public fight() { return this._katana.hit(); }; + public sneak() { return this._shuriken.throw(); }; + +} +``` + +You can use a custom metadata reader to implement a custom anotation system. + +For example, you could implement an annotation system based on static properties: + +```ts +class Ninja implements Ninja { + + public static constructorInjections = [ + "Katana", "Shuriken" + ]; + + private _katana: Katana; + private _shuriken: Shuriken; + + public constructor( + katana: Katana, + shuriken: Shuriken + ) { + this._katana = katana; + this._shuriken = shuriken; + } + + public fight() { return this._katana.hit(); }; + public sneak() { return this._shuriken.throw(); }; + +} +``` + +A custom metadata reader must implement the `interfaces.MetadataReader` interface. + +A full example [can be found in our unit tests](https://github.com/inversify/InversifyJS/blob/master/test/features/metadata_reader.test.ts). + +One you have a custom metadata reader you will be ready to apply it: + +```ts +let container = new Container(); +container.applyCustomMetadataReader(new StaticPropsMetadataReader()); +``` diff --git a/wiki/module_bundlers.md b/wiki/module_bundlers.md index 9aeeebb6e..f632d420f 100644 --- a/wiki/module_bundlers.md +++ b/wiki/module_bundlers.md @@ -3,27 +3,28 @@ ## Browserify ### Reflect-Metadata + The source from the reflect-metadata module contains a require('crypto') statement, which will pull additional node-builtin dependencies into your final bundle. This is about 0.6MB, way too much for a **browser build**. To workaround this issue add an init task in your gulp or grunt file to modify the library directly. Then browserify-shim the modified library. Below is an example for using gulp, you can execute multiple tasks within one tasks by using event-stream. - + gulp.task("init", function(done){ var tasks = []; - + tasks.push( gulp.src(["node_modules/reflect-metadata/Reflect.js"]) .pipe(replace(/var nodeCrypto = isNode && require\("crypto"\);/g, "var nodeCrypto = false;")) .pipe(gulp.dest("src/ts/libs")) ); - + return es.merge(tasks).on("end", done); }); - + Then in your package.json file, add a browserify-shim to the modified library: - + dependencies:{ "browserify-shim": "latest" } @@ -36,6 +37,3 @@ Then in your package.json file, add a browserify-shim to the modified library: Make the init task a dependency to run before your main task. You can then **require("reflect-metadata")** or **import "reflect-metadata"** in your inversify.config.ts/js file as usual. - -## Webpack -TODO (acceptiong PRs) diff --git a/wiki/purpose.md b/wiki/purpose.md index 4eb920332..3f03bb25c 100644 --- a/wiki/purpose.md +++ b/wiki/purpose.md @@ -1,8 +1,8 @@ -# Why InversifyJS? +# Why InversifyJS There are many good reasons to use InversifyJS but we would like to highlight some of them: -### 1. Real decoupling: +## 1. Real decoupling InversifyJS offers you real decoupling. Consider the following class: @@ -84,7 +84,8 @@ The good news is that in the future the symbols or string literals [could end up being generated by the TS compiler](https://github.com/Microsoft/TypeScript/issues/2577), but that is in the hands of the TC39 committee for the moment. -### 2. Solves competitors issues +## 2. Solves competitors issues + Some "old" JavaScript IoC container like the angular 1.x `$injector` have some problems: ![](http://i.imgur.com/Y2lRw4N.png) @@ -97,13 +98,15 @@ InversifyJS solves these problems: - There are no namespace collisions thanks to tagged, named and contextual bindings. - It is a stand alone library. -### 3. All the features that you may need +## 3. All the features that you may need + As far as I know it is the only IoC container for JavaScript that features complex dependency resolution (e.g. contextual bindings), multiple scopes (transient, singleton) and many other features. On top of that there is room for growth with features like interception or web worker scope. We also have plans for the development of dev-tools like browser extensions and middleware (logging, caching...). -### 4. Object composition is a pain! +## 4. Object composition is a pain + You may think that you don't need an IoC container. ![](https://raw.githubusercontent.com/inversify/inversify.github.io/master/img/so.png) @@ -113,12 +116,14 @@ If the [preceding argument](http://stackoverflow.com/questions/871405/why-do-i-n - [The current state of dependency inversion in JavaScript](http://blog.wolksoftware.com/the-current-state-of-dependency-inversion-in-javascript) - [About object-oriented design and the “class” & “extends” keywords in TypeScript / ES6](http://blog.wolksoftware.com/about-classes-inheritance-and-object-oriented-design-in-typescript-and-es6) -### 5. Type safety +## 5. Type safety + The library has been developed using TypeScript so type safety comes out of the box if you work with TypeScript but it is nice to mention that if you try to inject a Katana into a class that expects an implementation of `Shuriken` you will get a compilation error. -### 6. Great development experience +## 6. Great development experience + We are working hard to provide you with a great IoC container for your JavaScript apps but also a great development experience. We have spend a lot of time trying to make the InversifyJS as user friendly as possible and are working on development tools for chrome and we have already developed a logger middleware to help you to debug in Node.js.