Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for @optional dependencies #432

Merged
merged 2 commits into from
Dec 4, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions src/annotation/optional.ts
Original file line number Diff line number Diff line change
@@ -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 };
3 changes: 3 additions & 0 deletions src/constants/metadata_keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
1 change: 1 addition & 0 deletions src/interfaces/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ namespace interfaces {
matchesArray(name: interfaces.ServiceIdentifier<any>): boolean;
isNamed(): boolean;
isTagged(): boolean;
isOptional(): boolean;
matchesNamedTag(name: string): boolean;
matchesTag(key: string|number|symbol): (value: any) => boolean;
}
Expand Down
1 change: 1 addition & 0 deletions src/inversify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
14 changes: 9 additions & 5 deletions src/planning/planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions src/planning/target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
5 changes: 5 additions & 0 deletions src/resolution/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
110 changes: 110 additions & 0 deletions test/annotation/optional.test.ts
Original file line number Diff line number Diff line change
@@ -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>("Katana").to(Katana);
container.bind<Ninja>("Ninja").to(Ninja);

let ninja = container.get<Ninja>("Ninja");
expect(ninja.name).to.eql("Ninja");
expect(ninja.katana.name).to.eql("Katana");
expect(ninja.shuriken).to.eql(undefined);

container.bind<Shuriken>("Shuriken").to(Shuriken);

ninja = container.get<Ninja>("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>("Katana").to(Katana);
container.bind<Ninja>("Ninja").to(Ninja);

let ninja = container.get<Ninja>("Ninja");
expect(ninja.name).to.eql("Ninja");
expect(ninja.katana.name).to.eql("Katana");
expect(ninja.shuriken.name).to.eql("DefaultShuriken");

container.bind<Shuriken>("Shuriken").to(Shuriken);

ninja = container.get<Ninja>("Ninja");
expect(ninja.name).to.eql("Ninja");
expect(ninja.katana.name).to.eql("Katana");
expect(ninja.shuriken.name).to.eql("Shuriken");

});

});
8 changes: 6 additions & 2 deletions test/bugs/bugs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
86 changes: 86 additions & 0 deletions wiki/optional_dependencies.md
Original file line number Diff line number Diff line change
@@ -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>("Katana").to(Katana);
container.bind<Ninja>("Ninja").to(Ninja);

let ninja = container.get<Ninja>("Ninja");
expect(ninja.name).to.eql("Ninja");
expect(ninja.katana.name).to.eql("Katana");
expect(ninja.shuriken).to.eql(undefined);

container.bind<Shuriken>("Shuriken").to(Shuriken);

ninja = container.get<Ninja>("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>("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;
}
}
```
1 change: 1 addition & 0 deletions wiki/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down