diff --git a/CHANGELOG.md b/CHANGELOG.md index 15786d9f..b74b3cbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgrade information for v4.x to v5.x ### Fixed +- Fix `Target.isTagged()` to exclude `optional` from tag injections #1190. +- Update `toConstructor`, `toFactory`, `toFunction`, `toAutoFactory`, `toProvider` and `toConstantValue` to have singleton scope #1297. - Fix injection on optional properties when targeting ES6 #928 ## [5.0.1] - 2018-10-17 diff --git a/src/constants/metadata_keys.ts b/src/constants/metadata_keys.ts index a0419536..0c795fac 100644 --- a/src/constants/metadata_keys.ts +++ b/src/constants/metadata_keys.ts @@ -30,3 +30,16 @@ export const DESIGN_PARAM_TYPES = "design:paramtypes"; // used to identify postConstruct functions export const POST_CONSTRUCT = "post_construct"; + +function getNonCustomTagKeys(): string[] { + return [ + INJECT_TAG, + MULTI_INJECT_TAG, + NAME_TAG, + UNMANAGED_TAG, + NAMED_TAG, + OPTIONAL_TAG, + ]; +} + +export const NON_CUSTOM_TAG_KEYS: string[] = getNonCustomTagKeys(); diff --git a/src/planning/target.ts b/src/planning/target.ts index 82f7e24b..bd9d90b1 100644 --- a/src/planning/target.ts +++ b/src/planning/target.ts @@ -64,12 +64,9 @@ class Target implements interfaces.Target { } public isTagged(): boolean { - return this.metadata.some((m) => - (m.key !== METADATA_KEY.INJECT_TAG) && - (m.key !== METADATA_KEY.MULTI_INJECT_TAG) && - (m.key !== METADATA_KEY.NAME_TAG) && - (m.key !== METADATA_KEY.UNMANAGED_TAG) && - (m.key !== METADATA_KEY.NAMED_TAG)); + return this.metadata.some( + (metadata) => METADATA_KEY.NON_CUSTOM_TAG_KEYS.every((key) => metadata.key !== key), + ); } public isOptional(): boolean { @@ -85,14 +82,12 @@ class Target implements interfaces.Target { public getCustomTags(): interfaces.Metadata[] | null { if (this.isTagged()) { - return this.metadata.filter((m) => - (m.key !== METADATA_KEY.INJECT_TAG) && - (m.key !== METADATA_KEY.MULTI_INJECT_TAG) && - (m.key !== METADATA_KEY.NAME_TAG) && - (m.key !== METADATA_KEY.UNMANAGED_TAG) && - (m.key !== METADATA_KEY.NAMED_TAG)); + return this.metadata.filter( + (metadata) => METADATA_KEY.NON_CUSTOM_TAG_KEYS.every((key) => metadata.key !== key), + ); + } else { + return null; } - return null; } public matchesNamedTag(name: string): boolean { diff --git a/src/syntax/binding_to_syntax.ts b/src/syntax/binding_to_syntax.ts index 5b093f1f..2b9163ff 100644 --- a/src/syntax/binding_to_syntax.ts +++ b/src/syntax/binding_to_syntax.ts @@ -1,5 +1,5 @@ import * as ERROR_MSGS from "../constants/error_msgs"; -import { BindingTypeEnum } from "../constants/literal_types"; +import { BindingScopeEnum, BindingTypeEnum } from "../constants/literal_types"; import { interfaces } from "../interfaces/interfaces"; import { BindingInWhenOnSyntax } from "./binding_in_when_on_syntax"; import { BindingWhenOnSyntax } from "./binding_when_on_syntax"; @@ -31,6 +31,7 @@ class BindingToSyntax implements interfaces.BindingToSyntax { this._binding.cache = value; this._binding.dynamicValue = null; this._binding.implementationType = null; + this._binding.scope = BindingScopeEnum.Singleton; return new BindingWhenOnSyntax(this._binding); } @@ -45,12 +46,14 @@ class BindingToSyntax implements interfaces.BindingToSyntax { public toConstructor(constructor: interfaces.Newable): interfaces.BindingWhenOnSyntax { this._binding.type = BindingTypeEnum.Constructor; this._binding.implementationType = constructor as any; + this._binding.scope = BindingScopeEnum.Singleton; return new BindingWhenOnSyntax(this._binding); } public toFactory(factory: interfaces.FactoryCreator): interfaces.BindingWhenOnSyntax { this._binding.type = BindingTypeEnum.Factory; this._binding.factory = factory; + this._binding.scope = BindingScopeEnum.Singleton; return new BindingWhenOnSyntax(this._binding); } @@ -59,6 +62,7 @@ class BindingToSyntax implements interfaces.BindingToSyntax { if (typeof func !== "function") { throw new Error(ERROR_MSGS.INVALID_FUNCTION_BINDING); } const bindingWhenOnSyntax = this.toConstantValue(func); this._binding.type = BindingTypeEnum.Function; + this._binding.scope = BindingScopeEnum.Singleton; return bindingWhenOnSyntax; } @@ -68,12 +72,14 @@ class BindingToSyntax implements interfaces.BindingToSyntax { const autofactory = () => context.container.get(serviceIdentifier); return autofactory; }; + this._binding.scope = BindingScopeEnum.Singleton; return new BindingWhenOnSyntax(this._binding); } public toProvider(provider: interfaces.ProviderCreator): interfaces.BindingWhenOnSyntax { this._binding.type = BindingTypeEnum.Provider; this._binding.provider = provider; + this._binding.scope = BindingScopeEnum.Singleton; return new BindingWhenOnSyntax(this._binding); } diff --git a/test/bugs/issue_1190.test.ts b/test/bugs/issue_1190.test.ts new file mode 100644 index 00000000..c7fbdfc9 --- /dev/null +++ b/test/bugs/issue_1190.test.ts @@ -0,0 +1,61 @@ +import { expect } from "chai"; +import { injectable, inject, optional, Container, named } from "../../src/inversify"; + +describe("Issue 1190", () => { + + it('should inject a katana as default weapon to ninja', () => { + const TYPES = { + Weapon: "Weapon" + }; + + const TAG = { + throwable: "throwable" + }; + + interface Weapon { + name: string; + } + + @injectable() + class Katana implements Weapon { + public name: string; + public constructor() { + this.name = "Katana"; + } + } + + @injectable() + class Shuriken implements Weapon { + public name: string; + public constructor() { + this.name = "Shuriken"; + } + } + + @injectable() + class Ninja { + public name: string; + public katana: Katana; + public shuriken: Shuriken; + public constructor( + @inject(TYPES.Weapon) @optional() katana: Weapon, + @inject(TYPES.Weapon) @named(TAG.throwable) shuriken: Weapon + ) { + this.name = "Ninja"; + this.katana = katana; + this.shuriken = shuriken; + } + } + + const container = new Container(); + + container.bind(TYPES.Weapon).to(Katana).whenTargetIsDefault(); + container.bind(TYPES.Weapon).to(Shuriken).whenTargetNamed(TAG.throwable); + + container.bind("Ninja").to(Ninja); + + const ninja = container.get("Ninja"); + + expect(ninja.katana).to.deep.eq(new Katana()); + }); +}); diff --git a/test/bugs/issue_1297.test.ts b/test/bugs/issue_1297.test.ts new file mode 100644 index 00000000..df90f762 --- /dev/null +++ b/test/bugs/issue_1297.test.ts @@ -0,0 +1,144 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { Container, injectable, interfaces } from "../../src/inversify"; + +describe("Issue 1297", () => { + it('should call onActivation once if the service is a constant value binding', () => { + const container = new Container(); + + const onActivationHandlerSpy = sinon.spy< + (ctx: interfaces.Context, message: string) => string + >((_ctx: interfaces.Context, message: string) => message); + + container.bind("message") + .toConstantValue("Hello world") + .onActivation(onActivationHandlerSpy); + + container.get("message"); + container.get("message"); + + expect(onActivationHandlerSpy.callCount).to.eq(1); + }); + + it('should call onActivation once if the service is a factory binding', () => { + + @injectable() + class Katana { + public hit() { + return "cut!"; + } + } + + const container = new Container(); + + const onActivationHandlerSpy = sinon.spy< + (ctx: interfaces.Context, instance: interfaces.Factory) => interfaces.Factory + >((_ctx: interfaces.Context, instance: interfaces.Factory) => instance); + + container.bind("Katana").to(Katana); + + container.bind>("Factory").toFactory((context) => + () => + context.container.get("Katana")).onActivation(onActivationHandlerSpy); + + container.get("Factory"); + container.get("Factory"); + + expect(onActivationHandlerSpy.callCount).to.eq(1); + }); + + it('should call onActivation once if the service is an auto factory binding', () => { + + @injectable() + class Katana { + public hit() { + return "cut!"; + } + } + + const container = new Container(); + + const onActivationHandlerSpy = sinon.spy< + (ctx: interfaces.Context, instance: interfaces.Factory) => interfaces.Factory + >((_ctx: interfaces.Context, instance: interfaces.Factory) => instance); + + container.bind("Katana").to(Katana); + + container.bind>("Factory") + .toAutoFactory("Katana").onActivation(onActivationHandlerSpy); + + container.get("Factory"); + container.get("Factory"); + + expect(onActivationHandlerSpy.callCount).to.eq(1); + }); + + it('should call onActivation once if the service is a function binding', () => { + + const container = new Container(); + + const onActivationHandlerSpy = sinon.spy< + (ctx: interfaces.Context, messageGenerator: () => string) => () => string + >((_ctx: interfaces.Context, messageGenerator: () => string) => messageGenerator); + + container.bind<() => string>("message") + .toFunction(() => "Hello world") + .onActivation(onActivationHandlerSpy); + + container.get("message"); + container.get("message"); + + expect(onActivationHandlerSpy.callCount).to.eq(1); + }); + + it('should call onActivation once if the service is a constructor binding', () => { + + @injectable() + class Katana { + public hit() { + return "cut!"; + } + } + + const container = new Container(); + + const onActivationHandlerSpy = sinon.spy< + (ctx: interfaces.Context, injectableObj: unknown) => unknown + >((_ctx: interfaces.Context, injectableObj: unknown) => injectableObj); + + container.bind("Katana") + .toConstructor(Katana) + .onActivation(onActivationHandlerSpy); + + container.get("Katana"); + container.get("Katana"); + + expect(onActivationHandlerSpy.callCount).to.eq(1); + }); + + it('should call onActivation once if the service is a provider binding', () => { + + @injectable() + class Katana { + public hit() { + return "cut!"; + } + } + + const container = new Container(); + + const onActivationHandlerSpy = sinon.spy< + (ctx: interfaces.Context, injectableObj: unknown) => unknown + >((_ctx: interfaces.Context, injectableObj: unknown) => injectableObj); + + container.bind("Provider") + .toProvider((context: interfaces.Context) => + () => + Promise.resolve(new Katana())).onActivation(onActivationHandlerSpy); + + container.get("Provider"); + container.get("Provider"); + + expect(onActivationHandlerSpy.callCount).to.eq(1); + }); +}); diff --git a/test/planning/target.test.ts b/test/planning/target.test.ts index 30e23a49..b2e52bfe 100644 --- a/test/planning/target.test.ts +++ b/test/planning/target.test.ts @@ -78,6 +78,29 @@ describe("Target", () => { target3.metadata.push(new Metadata("power", 5), new Metadata("speed", 5)); expect(target3.isTagged()).to.be.eql(true); + const target4 = new Target(TargetTypeEnum.Variable, "", "Katana"); + target4.metadata.push(new Metadata(METADATA_KEY.INJECT_TAG, "Katana")) + expect(target4.isTagged()).to.be.eql(false); + + const target5 = new Target(TargetTypeEnum.Variable, "", "Katana"); + target5.metadata.push(new Metadata(METADATA_KEY.MULTI_INJECT_TAG, "Katana")) + expect(target5.isTagged()).to.be.eql(false); + + const target6 = new Target(TargetTypeEnum.Variable, "katanaName", "Katana"); + target6.metadata.push(new Metadata(METADATA_KEY.NAME_TAG, "katanaName")) + expect(target6.isTagged()).to.be.eql(false); + + const target7 = new Target(TargetTypeEnum.Variable, "", "Katana"); + target7.metadata.push(new Metadata(METADATA_KEY.UNMANAGED_TAG, true)) + expect(target7.isTagged()).to.be.eql(false); + + const target8 = new Target(TargetTypeEnum.Variable, "katanaName", "Katana"); + target8.metadata.push(new Metadata(METADATA_KEY.NAMED_TAG, "katanaName")) + expect(target8.isTagged()).to.be.eql(false); + + const target9 = new Target(TargetTypeEnum.Variable, "", "Katana"); + target9.metadata.push(new Metadata(METADATA_KEY.OPTIONAL_TAG, true)) + expect(target9.isTagged()).to.be.eql(false); }); it("Should be able to match tagged metadata", () => {