Skip to content

Commit

Permalink
Allow multiple types bound to a single token (#40)
Browse files Browse the repository at this point in the history
* Allow multiple types bound to a single token [WIP]
* Implement custom Registry to allow multiple registrations on the same token
* Change _registry property on InternalDependencyContainer to our new Registry class
* Add new interface "TokenDescriptor"
* Change construct method on InternalDependencyContainer to understand TokenDescriptor

* Remove commited .vscode folder

* Add @injectAll decorator
* Implemented @injectAll decorator
* Add test cases for explicit array types bound to the container to ensure nothing is broke
* Add @injectAll test case
* Fix function "isTokenDescriptor"

* Refactor @Inject and @injectAll decorators

* Add more test cases
* Add child container test cases for resolveAll method
* Add failing test for resolveAll method

* Add failing test case for array dependency

* Fix code style

* @autoInjectable resolve multiple dependencies
* Change dependency resolution on decorator @autoInjectable
* Add new test cases

* Fix method name
  • Loading branch information
skiptirengu authored and Xapphire13 committed Jul 8, 2019
1 parent b8f1ce0 commit 7e7babd
Show file tree
Hide file tree
Showing 13 changed files with 420 additions and 45 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -5,6 +5,9 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*

# VS code folder
.vscode

# Runtime data
pids
*.pid
Expand Down
38 changes: 38 additions & 0 deletions src/__tests__/auto-injectable.test.ts
@@ -1,5 +1,6 @@
import {autoInjectable, injectable, singleton} from "../decorators";
import {instance as globalContainer} from "../dependency-container";
import injectAll from "../decorators/inject-all";

afterEach(() => {
globalContainer.reset();
Expand Down Expand Up @@ -141,3 +142,40 @@ test("@autoInjectable works with @singleton", () => {
expect(instance1).toBe(instance2);
expect(instance1.bar).toBe(instance2.bar);
});

test("@autoInjectable resolves multiple registered dependencies", () => {
interface Bar {
str: string;
}

@injectable()
class FooBar implements Bar {
str: string = "";
}

globalContainer.register<Bar>("Bar", {useClass: FooBar});

@autoInjectable()
class Foo {
constructor(@injectAll("Bar") public bar?: Bar[]) {}
}

const foo = new Foo();
expect(Array.isArray(foo.bar)).toBeTruthy();
expect(foo.bar!.length).toBe(1);
expect(foo.bar![0]).toBeInstanceOf(FooBar);
});

test("@autoInjectable resolves multiple transient dependencies", () => {
class Foo {}

@autoInjectable()
class Bar {
constructor(@injectAll(Foo) public foo?: Foo[]) {}
}

const bar = new Bar();
expect(Array.isArray(bar.foo)).toBeTruthy();
expect(bar.foo!.length).toBe(1);
expect(bar.foo![0]).toBeInstanceOf(Foo);
});
34 changes: 33 additions & 1 deletion src/__tests__/child-container.test.ts
Expand Up @@ -2,6 +2,10 @@

import {instance as globalContainer} from "../dependency-container";

afterEach(() => {
globalContainer.reset();
});

test("child container resolves even when parent doesn't have registration", () => {
interface IFoo {}
class Foo implements IFoo {}
Expand All @@ -14,7 +18,7 @@ test("child container resolves even when parent doesn't have registration", () =
expect(myFoo instanceof Foo).toBeTruthy();
});

test("child container resolves using parents' registration when child container doesn't have registration", () => {
test("child container resolves using parent's registration when child container doesn't have registration", () => {
interface IFoo {}
class Foo implements IFoo {}

Expand All @@ -25,3 +29,31 @@ test("child container resolves using parents' registration when child container

expect(myFoo instanceof Foo).toBeTruthy();
});

test("child container resolves all even when parent doesn't have registration", () => {
interface IFoo {}
class Foo implements IFoo {}

const container = globalContainer.createChildContainer();
container.register("IFoo", {useClass: Foo});

const myFoo = container.resolveAll<IFoo>("IFoo");

expect(Array.isArray(myFoo)).toBeTruthy();
expect(myFoo.length).toBe(1);
expect(myFoo[0] instanceof Foo).toBeTruthy();
});

test("child container resolves all using parent's registration when child container doesn't have registration", () => {
interface IFoo {}
class Foo implements IFoo {}

globalContainer.register("IFoo", {useClass: Foo});
const container = globalContainer.createChildContainer();

const myFoo = container.resolveAll<IFoo>("IFoo");

expect(Array.isArray(myFoo)).toBeTruthy();
expect(myFoo.length).toBe(1);
expect(myFoo[0] instanceof Foo).toBeTruthy();
});
118 changes: 118 additions & 0 deletions src/__tests__/global-container.test.ts
Expand Up @@ -4,6 +4,7 @@ import {inject, injectable, registry, singleton} from "../decorators";
import {instanceCachingFactory, predicateAwareClassFactory} from "../factories";
import {DependencyContainer} from "../types";
import {instance as globalContainer} from "../dependency-container";
import injectAll from "../decorators/inject-all";

interface IBar {
value: string;
Expand Down Expand Up @@ -40,6 +41,33 @@ test("fails to resolve unregistered dependency by name", () => {
}).toThrow();
});

test("allows arrays to be registered by value provider", () => {
class Bar {}

const value = [new Bar()];
globalContainer.register<Bar[]>("BarArray", {useValue: value});

const barArray = globalContainer.resolve<Bar[]>("BarArray");
expect(Array.isArray(barArray)).toBeTruthy();
expect(value === barArray).toBeTruthy();
});

test("allows arrays to be registered by factory provider", () => {
class Bar {}

globalContainer.register<Bar>(Bar, {useClass: Bar});
globalContainer.register<Bar[]>("BarArray", {
useFactory: (container): Bar[] => {
return [container.resolve(Bar)];
}
});

const barArray = globalContainer.resolve<Bar[]>("BarArray");
expect(Array.isArray(barArray)).toBeTruthy();
expect(barArray.length).toBe(1);
expect(barArray[0]).toBeInstanceOf(Bar);
});

test("resolves transient instances when not registered", () => {
class Bar {}

Expand Down Expand Up @@ -169,6 +197,49 @@ test("resolves anonymous classes separately", () => {
expect(globalContainer.resolve(ctor2) instanceof ctor2).toBeTruthy();
});

// --- resolveAll() ---

test("fails to resolveAll unregistered dependency by name", () => {
expect(() => {
globalContainer.resolveAll("NotRegistered");
}).toThrow();
});

test("resolves an array of transient instances bound to a single interface", () => {
interface FooInterface {
bar: string;
}

class FooOne implements FooInterface {
public bar: string = "foo1";
}

class FooTwo implements FooInterface {
public bar: string = "foo2";
}

globalContainer.register<FooInterface>("FooInterface", {useClass: FooOne});
globalContainer.register<FooInterface>("FooInterface", {useClass: FooTwo});

const fooArray = globalContainer.resolveAll<FooInterface>("FooInterface");
expect(Array.isArray(fooArray)).toBeTruthy();
expect(fooArray[0]).toBeInstanceOf(FooOne);
expect(fooArray[1]).toBeInstanceOf(FooTwo);
});

test("resolves all transient instances when not registered", () => {
class Foo {}

const foo1 = globalContainer.resolveAll<Foo>(Foo);
const foo2 = globalContainer.resolveAll<Foo>(Foo);

expect(Array.isArray(foo1)).toBeTruthy();
expect(Array.isArray(foo2)).toBeTruthy();
expect(foo1[0]).toBeInstanceOf(Foo);
expect(foo2[0]).toBeInstanceOf(Foo);
expect(foo1[0]).not.toBe(foo2[0]);
});

// --- isRegistered() ---

test("returns true for a registered singleton class", () => {
Expand Down Expand Up @@ -528,6 +599,53 @@ test("allows interfaces to be resolved from the constructor with just a name", (
expect(myFoo.myBar instanceof Bar).toBeTruthy();
});

test("allows explicit array dependencies to be resolved by inject decorator", () => {
@injectable()
class Foo {}

@injectable()
class Bar {
constructor(@inject("FooArray") public foo: Foo[]) {}
}

const fooArray = [new Foo()];
globalContainer.register<Foo[]>("FooArray", {useValue: fooArray});
globalContainer.register<Bar>(Bar, {useClass: Bar});

const bar = globalContainer.resolve<Bar>(Bar);
expect(bar.foo === fooArray).toBeTruthy();
});

// --- @injectAll ---

test("injects all dependencies bound to a given interface", () => {
interface Foo {
str: string;
}

class FooImpl1 implements Foo {
public str: string = "foo1";
}

class FooImpl2 implements Foo {
public str: string = "foo2";
}

@injectable()
class Bar {
constructor(@injectAll("Foo") public foo: Foo[]) {}
}

globalContainer.register<Foo>("Foo", {useClass: FooImpl1});
globalContainer.register<Foo>("Foo", {useClass: FooImpl2});

const bar = globalContainer.resolve<Bar>(Bar);
expect(Array.isArray(bar.foo)).toBeTruthy();
expect(bar.foo.length).toBe(2);
expect(bar.foo[0]).toBeInstanceOf(FooImpl1);
expect(bar.foo[1]).toBeInstanceOf(FooImpl2);
});

// --- factories ---

test("instanceCachingFactory caches the returned instance", () => {
Expand Down
64 changes: 64 additions & 0 deletions src/__tests__/registry.test.ts
@@ -0,0 +1,64 @@
import Registry from "../registry";
import {Registration} from "../dependency-container";

let registry: Registry;
beforeEach(() => {
registry = new Registry();
});

test("getAll returns all registrations of a given key", () => {
const registration1: Registration = {
options: {singleton: false},
provider: {useValue: "provider"}
};
const registration2: Registration = {
options: {singleton: false},
provider: {useValue: "provider"}
};

registry.set("Foo", registration1);
registry.set("Foo", registration2);

expect(registry.has("Foo")).toBeTruthy();

const all = registry.getAll("Foo");
expect(Array.isArray(all)).toBeTruthy();
expect(all.length).toBe(2);
expect(all[0]).toStrictEqual(registration1);
expect(all[1]).toStrictEqual(registration2);
});

test("get returns the last registration", () => {
const registration1: Registration = {
options: {singleton: false},
provider: {useValue: "provider"}
};
const registration2: Registration = {
options: {singleton: false},
provider: {useValue: "provider"}
};

registry.set("Bar", registration1);
registry.set("Bar", registration2);

expect(registry.has("Bar")).toBeTruthy();
expect(registry.get("Bar")).toStrictEqual(registration2);
});

test("get returns null when there is no registration", () => {
expect(registry.has("FooBar")).toBeFalsy();
expect(registry.get("FooBar")).toBeNull();
});

test("clear removes all registrations", () => {
const registration: Registration = {
options: {singleton: false},
provider: {useValue: "provider"}
};

registry.set("Foo", registration);
expect(registry.has("Foo")).toBeTruthy();

registry.clear();
expect(registry.has("Foo")).toBeFalsy();
});
6 changes: 6 additions & 0 deletions src/decorators/auto-injectable.ts
@@ -1,6 +1,7 @@
import constructor from "../types/constructor";
import {getParamInfo} from "../reflection-helpers";
import {instance as globalContainer} from "../dependency-container";
import {isTokenDescriptor} from "../providers/injection-token";

/**
* Class decorator factory that replaces the decorated class' constructor with
Expand All @@ -20,6 +21,11 @@ function autoInjectable(): (target: constructor<any>) => any {
...args.concat(
paramInfo.slice(args.length).map((type, index) => {
try {
if (isTokenDescriptor(type)) {
return type.multiple
? globalContainer.resolveAll(type.token)
: globalContainer.resolve(type.token);
}
return globalContainer.resolve(type);
} catch (e) {
const argIndex = index + args.length;
Expand Down
16 changes: 16 additions & 0 deletions src/decorators/inject-all.ts
@@ -0,0 +1,16 @@
import {defineInjectionTokenMetadata} from "../reflection-helpers";
import InjectionToken, {TokenDescriptor} from "../providers/injection-token";

/**
* Parameter decorator factory that allows for interface information to be stored in the constructor's metadata
*
* @return {Function} The parameter decorator
*/
function injectAll(
token: InjectionToken<any>
): (target: any, propertyKey: string | symbol, parameterIndex: number) => any {
const data: TokenDescriptor = {token, multiple: true};
return defineInjectionTokenMetadata(data);
}

export default injectAll;
17 changes: 2 additions & 15 deletions src/decorators/inject.ts
@@ -1,4 +1,4 @@
import {INJECTION_TOKEN_METADATA_KEY} from "../reflection-helpers";
import {defineInjectionTokenMetadata} from "../reflection-helpers";
import InjectionToken from "../providers/injection-token";

/**
Expand All @@ -9,20 +9,7 @@ import InjectionToken from "../providers/injection-token";
function inject(
token: InjectionToken<any>
): (target: any, propertyKey: string | symbol, parameterIndex: number) => any {
return function(
target: any,
_propertyKey: string | symbol,
parameterIndex: number
): any {
const injectionTokens =
Reflect.getOwnMetadata(INJECTION_TOKEN_METADATA_KEY, target) || {};
injectionTokens[parameterIndex] = token;
Reflect.defineMetadata(
INJECTION_TOKEN_METADATA_KEY,
injectionTokens,
target
);
};
return defineInjectionTokenMetadata(token);
}

export default inject;

0 comments on commit 7e7babd

Please sign in to comment.