From 02144bd7bd8b07f6640de2b65c36f307b2697a99 Mon Sep 17 00:00:00 2001 From: remojansen Date: Sun, 4 Dec 2016 20:18:15 +0000 Subject: [PATCH] Implemented #411 --- README.md | 1 + src/annotation/optional.ts | 12 ++++ src/constants/metadata_keys.ts | 3 + src/interfaces/interfaces.ts | 1 + src/inversify.ts | 1 + src/planning/planner.ts | 14 ++-- src/planning/target.ts | 4 ++ src/resolution/resolver.ts | 5 ++ test/annotation/optional.test.ts | 110 +++++++++++++++++++++++++++++++ test/bugs/bugs.test.ts | 8 ++- wiki/optional_dependencies.md | 86 ++++++++++++++++++++++++ wiki/readme.md | 1 + 12 files changed, 239 insertions(+), 7 deletions(-) create mode 100644 src/annotation/optional.ts create mode 100644 test/annotation/optional.test.ts create mode 100644 wiki/optional_dependencies.md diff --git a/README.md b/README.md index fb5c13d3f..8aead2ecb 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,7 @@ Let's take a look to the InversifyJS features! - [Declaring container modules](https://github.com/inversify/InversifyJS/blob/master/wiki/container_modules.md) - [Container snapshots](https://github.com/inversify/InversifyJS/blob/master/wiki/container_snapshots.md) - [Controlling the scope of the dependencies](https://github.com/inversify/InversifyJS/blob/master/wiki/scope.md) +- [Declaring optional dependencies](https://github.com/inversify/InversifyJS/blob/master/wiki/optional_dependencies.md) - [Injecting a constant or dynamic value](https://github.com/inversify/InversifyJS/blob/master/wiki/value_injection.md) - [Injecting a class constructor](https://github.com/inversify/InversifyJS/blob/master/wiki/constructor_injection.md) - [Injecting a Factory](https://github.com/inversify/InversifyJS/blob/master/wiki/factory_injection.md) diff --git a/src/annotation/optional.ts b/src/annotation/optional.ts new file mode 100644 index 000000000..621f6d75c --- /dev/null +++ b/src/annotation/optional.ts @@ -0,0 +1,12 @@ +import { Metadata } from "../planning/metadata"; +import { tagParameter } from "./decorator_utils"; +import * as METADATA_KEY from "../constants/metadata_keys"; + +function optional() { + return function(target: any, targetKey: string, index: number) { + let metadata = new Metadata(METADATA_KEY.OPTIONAL_TAG, true); + return tagParameter(target, targetKey, index, metadata); + }; +} + +export { optional }; diff --git a/src/constants/metadata_keys.ts b/src/constants/metadata_keys.ts index bd00cf572..6a4f474a4 100644 --- a/src/constants/metadata_keys.ts +++ b/src/constants/metadata_keys.ts @@ -7,6 +7,9 @@ export const NAME_TAG = "name"; // The for unmanaged injections (in base classes when using inheritance) export const UNMANAGED_TAG = "unmanaged"; +// The for optional injections +export const OPTIONAL_TAG = "optional"; + // The type of the binding at design time export const INJECT_TAG = "inject"; diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index cae32d0a6..bf6eed6d0 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -150,6 +150,7 @@ namespace interfaces { matchesArray(name: interfaces.ServiceIdentifier): boolean; isNamed(): boolean; isTagged(): boolean; + isOptional(): boolean; matchesNamedTag(name: string): boolean; matchesTag(key: string|number|symbol): (value: any) => boolean; } diff --git a/src/inversify.ts b/src/inversify.ts index a76c38a3d..38570dad2 100644 --- a/src/inversify.ts +++ b/src/inversify.ts @@ -4,6 +4,7 @@ export { injectable } from "./annotation/injectable"; export { tagged } from "./annotation/tagged"; export { named } from "./annotation/named"; export { inject } from "./annotation/inject"; +export { optional } from "./annotation/optional"; export { unmanaged } from "./annotation/unmanaged"; export { multiInject } from "./annotation/multi_inject"; export { targetName } from "./annotation/target_name"; diff --git a/src/planning/planner.ts b/src/planning/planner.ts index c5c5a9f53..6dedc1742 100644 --- a/src/planning/planner.ts +++ b/src/planning/planner.ts @@ -91,11 +91,15 @@ function _validateActiveBindingCount( switch (bindings.length) { case BindingCount.NoBindingsAvailable: - let serviceIdentifierString = getServiceIdentifierAsString(serviceIdentifier); - let msg = ERROR_MSGS.NOT_REGISTERED; - msg += listMetadataForTarget(serviceIdentifierString, target); - msg += listRegisteredBindingsForServiceIdentifier(container, serviceIdentifierString, getBindings); - throw new Error(msg); + if (target.isOptional() === true) { + return bindings; + } else { + let serviceIdentifierString = getServiceIdentifierAsString(serviceIdentifier); + let msg = ERROR_MSGS.NOT_REGISTERED; + msg += listMetadataForTarget(serviceIdentifierString, target); + msg += listRegisteredBindingsForServiceIdentifier(container, serviceIdentifierString, getBindings); + throw new Error(msg); + } case BindingCount.OnlyOneBindingAvailable: if (target.isArray() === false) { diff --git a/src/planning/target.ts b/src/planning/target.ts index 4ab0ab993..cb555643e 100644 --- a/src/planning/target.ts +++ b/src/planning/target.ts @@ -74,6 +74,10 @@ class Target implements interfaces.Target { }); } + public isOptional(): boolean { + return this.matchesTag(METADATA_KEY.OPTIONAL_TAG)(true); + } + public getNamedTag(): interfaces.Metadata | null { if (this.isNamed()) { return this.metadata.filter((m) => m.key === METADATA_KEY.NAMED_TAG)[0]; diff --git a/src/resolution/resolver.ts b/src/resolution/resolver.ts index 38c6d567e..3c9090581 100644 --- a/src/resolution/resolver.ts +++ b/src/resolution/resolver.ts @@ -26,6 +26,11 @@ function _resolveRequest(request: interfaces.Request): any { } else { let result: any = null; + + if (request.target.isOptional() === true && bindings.length === 0) { + return undefined; + } + let binding = bindings[0]; let isSingleton = binding.scope === BindingScopeEnum.Singleton; diff --git a/test/annotation/optional.test.ts b/test/annotation/optional.test.ts new file mode 100644 index 000000000..33914036c --- /dev/null +++ b/test/annotation/optional.test.ts @@ -0,0 +1,110 @@ +import { expect } from "chai"; +import { Container, injectable, inject, optional } from "../../src/inversify"; + +describe("@optional", () => { + + it("Should allow to flag dependencies as optional", () => { + + @injectable() + class Katana { + public name: string; + public constructor() { + this.name = "Katana"; + } + } + + @injectable() + class Shuriken { + public name: string; + public constructor() { + this.name = "Shuriken"; + } + } + + @injectable() + class Ninja { + public name: string; + public katana: Katana; + public shuriken: Shuriken; + public constructor( + @inject("Katana") katana: Katana, + @inject("Shuriken") @optional() shuriken: Shuriken + ) { + this.name = "Ninja"; + this.katana = katana; + this.shuriken = shuriken; + } + } + + let container = new Container(); + + container.bind("Katana").to(Katana); + container.bind("Ninja").to(Ninja); + + let ninja = container.get("Ninja"); + expect(ninja.name).to.eql("Ninja"); + expect(ninja.katana.name).to.eql("Katana"); + expect(ninja.shuriken).to.eql(undefined); + + container.bind("Shuriken").to(Shuriken); + + ninja = container.get("Ninja"); + expect(ninja.name).to.eql("Ninja"); + expect(ninja.katana.name).to.eql("Katana"); + expect(ninja.shuriken.name).to.eql("Shuriken"); + + }); + + it("Should allow to set a default value for dependencies flagged as optional", () => { + + @injectable() + class Katana { + public name: string; + public constructor() { + this.name = "Katana"; + } + } + + @injectable() + class Shuriken { + public name: string; + public constructor() { + this.name = "Shuriken"; + } + } + + @injectable() + class Ninja { + public name: string; + public katana: Katana; + public shuriken: Shuriken; + public constructor( + @inject("Katana") katana: Katana, + @inject("Shuriken") @optional() shuriken: Shuriken = { name: "DefaultShuriken" } + ) { + this.name = "Ninja"; + this.katana = katana; + this.shuriken = shuriken; + } + } + + let container = new Container(); + + container.bind("Katana").to(Katana); + container.bind("Ninja").to(Ninja); + + let ninja = container.get("Ninja"); + expect(ninja.name).to.eql("Ninja"); + expect(ninja.katana.name).to.eql("Katana"); + expect(ninja.shuriken.name).to.eql("DefaultShuriken"); + + container.bind("Shuriken").to(Shuriken); + + ninja = container.get("Ninja"); + expect(ninja.name).to.eql("Ninja"); + expect(ninja.katana.name).to.eql("Katana"); + expect(ninja.shuriken.name).to.eql("Shuriken"); + + }); + +}); diff --git a/test/bugs/bugs.test.ts b/test/bugs/bugs.test.ts index f0fc47980..0ab047ccc 100644 --- a/test/bugs/bugs.test.ts +++ b/test/bugs/bugs.test.ts @@ -71,7 +71,9 @@ describe("Bugs", () => { }); - it("Should not throw when args length of base and derived class match (inject into the derived class)", () => { + it("Should not throw when args length of base and derived class match", () => { + + // Injecting into the derived class @injectable() class Warrior { @@ -103,7 +105,9 @@ describe("Bugs", () => { }); - it("Should not throw when args length of base and derived class match (inject into the derived class with multiple args)", () => { + it("Should not throw when args length of base and derived class match", () => { + + // Injecting into the derived class with multiple args @injectable() class Warrior { diff --git a/wiki/optional_dependencies.md b/wiki/optional_dependencies.md new file mode 100644 index 000000000..5960cd42b --- /dev/null +++ b/wiki/optional_dependencies.md @@ -0,0 +1,86 @@ +# Optional dependencies + +We can declare an optional dependency using the `@optional()` decorator: + +```ts +@injectable() +class Katana { + public name: string; + public constructor() { + this.name = "Katana"; + } +} + +@injectable() +class Shuriken { + public name: string; + public constructor() { + this.name = "Shuriken"; + } +} + +@injectable() +class Ninja { + public name: string; + public katana: Katana; + public shuriken: Shuriken; + public constructor( + @inject("Katana") katana: Katana, + @inject("Shuriken") @optional() shuriken: Shuriken // Optional! + ) { + this.name = "Ninja"; + this.katana = katana; + this.shuriken = shuriken; + } +} + +let container = new Container(); + +container.bind("Katana").to(Katana); +container.bind("Ninja").to(Ninja); + +let ninja = container.get("Ninja"); +expect(ninja.name).to.eql("Ninja"); +expect(ninja.katana.name).to.eql("Katana"); +expect(ninja.shuriken).to.eql(undefined); + +container.bind("Shuriken").to(Shuriken); + +ninja = container.get("Ninja"); +expect(ninja.name).to.eql("Ninja"); +expect(ninja.katana.name).to.eql("Katana"); +expect(ninja.shuriken.name).to.eql("Shuriken"); +``` + +In the example we can see how the first time we resolve `Ninja`, its +property `shuriken` is undefined because no binsings have been declared +for `Shuriken` and the property is annotated with the `@optional()` decorator. + +After declaring a binding for `Shuriken`: + +```ts +container.bind("Shuriken").to(Shuriken); +``` + +All the resolved instances of `Ninja` will contain an instance of `Shuriken`. + +## Default values +If a dependency is decorated with the `@optional()` decorator, we will be able to to declare +a default value just like you can do in any other TypeScript application: + +```ts +@injectable() +class Ninja { + public name: string; + public katana: Katana; + public shuriken: Shuriken; + public constructor( + @inject("Katana") katana: Katana, + @inject("Shuriken") @optional() shuriken: Shuriken = { name: "DefaultShuriken" } // Default value! + ) { + this.name = "Ninja"; + this.katana = katana; + this.shuriken = shuriken; + } +} +``` diff --git a/wiki/readme.md b/wiki/readme.md index 17647bf3e..b0b143294 100644 --- a/wiki/readme.md +++ b/wiki/readme.md @@ -16,6 +16,7 @@ Welcome to the InversifyJS wiki! - [Declaring container modules](https://github.com/inversify/InversifyJS/blob/master/wiki/container_modules.md) - [Container snapshots](https://github.com/inversify/InversifyJS/blob/master/wiki/container_snapshots.md) - [Controlling the scope of the dependencies](https://github.com/inversify/InversifyJS/blob/master/wiki/scope.md) +- [Declaring optional dependencies](https://github.com/inversify/InversifyJS/blob/master/wiki/optional_dependencies.md) - [Injecting a constant or dynamic value](https://github.com/inversify/InversifyJS/blob/master/wiki/value_injection.md) - [Injecting a class constructor](https://github.com/inversify/InversifyJS/blob/master/wiki/constructor_injection.md) - [Injecting a Factory](https://github.com/inversify/InversifyJS/blob/master/wiki/factory_injection.md)