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

Factory aware of target class that resolves the factory's type. #56

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
24 changes: 24 additions & 0 deletions src/__tests__/auto-injectable.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {autoInjectable, injectable, singleton} from "../decorators";
import {instance as globalContainer} from "../dependency-container";
import injectAll from "../decorators/inject-all";
import {constructor} from "../types";

afterEach(() => {
globalContainer.reset();
Expand Down Expand Up @@ -179,3 +180,26 @@ test("@autoInjectable resolves multiple transient dependencies", () => {
expect(bar.foo!.length).toBe(1);
expect(bar.foo![0]).toBeInstanceOf(Foo);
});

test("@autoInjectable with factory allows factory to see target class", () => {
class Bar {
public target?: constructor<any>;
}
@autoInjectable()
class Foo {
constructor(public myBar?: Bar) {}
}

globalContainer.register(Bar, {
useFactory: (_, target) => {
const bar = new Bar();
bar.target = target;
return bar;
}
});

const myFoo = new Foo();

// It is impossible to compare Foo type to target, because Foo here is extended by @autoInjectable
expect(myFoo.myBar!.target!.name).toBe(Foo.name);
});
6 changes: 3 additions & 3 deletions src/decorators/auto-injectable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ function autoInjectable(): (target: constructor<any>) => any {
try {
if (isTokenDescriptor(type)) {
return type.multiple
? globalContainer.resolveAll(type.token)
: globalContainer.resolve(type.token);
? globalContainer.resolveAll(type.token, target)
: globalContainer.resolve(type.token, target);
}
return globalContainer.resolve(type);
return globalContainer.resolve(type, target);
} catch (e) {
const argIndex = index + args.length;

Expand Down
32 changes: 21 additions & 11 deletions src/dependency-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,33 +145,38 @@ class InternalDependencyContainer implements DependencyContainer {
* Resolve a token into an instance
*
* @param token {InjectionToken} The dependency token
* @param target {constructor<any>} Constructor resolving the dependency token
* @return {T} An instance of the dependency
*/
public resolve<T>(token: InjectionToken<T>): T {
public resolve<T>(token: InjectionToken<T>, target?: constructor<any>): T {
const registration = this.getRegistration(token);

if (!registration && isNormalToken(token)) {
throw `Attempted to resolve unregistered dependency token: ${token.toString()}`;
}

if (registration) {
return this.resolveRegistration(registration);
return this.resolveRegistration(registration, target);
}

// No registration for this token, but since it's a constructor, return an instance
return this.construct(<constructor<T>>token);
}

private resolveRegistration<T>(registration: Registration): T {
private resolveRegistration<T>(
registration: Registration,
target?: constructor<T>
): T {
if (isValueProvider(registration.provider)) {
return registration.provider.useValue;
} else if (isTokenProvider(registration.provider)) {
return registration.options.singleton
? registration.instance ||
(registration.instance = this.resolve(
registration.provider.useToken
registration.provider.useToken,
target
))
: this.resolve(registration.provider.useToken);
: this.resolve(registration.provider.useToken, target);
} else if (isClassProvider(registration.provider)) {
return registration.options.singleton
? registration.instance ||
Expand All @@ -180,21 +185,26 @@ class InternalDependencyContainer implements DependencyContainer {
))
: this.construct(registration.provider.useClass);
} else if (isFactoryProvider(registration.provider)) {
return registration.provider.useFactory(this);
return registration.provider.useFactory(this, target);
} else {
return this.construct(registration.provider);
}
}

public resolveAll<T>(token: InjectionToken<T>): T[] {
public resolveAll<T>(
token: InjectionToken<T>,
parent?: constructor<any>
): T[] {
const registration = this.getAllRegistrations(token);

if (!registration && isNormalToken(token)) {
throw `Attempted to resolve unregistered dependency token: ${token.toString()}`;
}

if (registration) {
return registration.map(item => this.resolveRegistration<T>(item));
return registration.map(item =>
this.resolveRegistration<T>(item, parent)
);
}

// No registration for this token, but since it's a constructor, return an instance
Expand Down Expand Up @@ -261,10 +271,10 @@ class InternalDependencyContainer implements DependencyContainer {
const params = paramInfo.map(param => {
if (isTokenDescriptor(param)) {
return param.multiple
? this.resolveAll(param.token)
: this.resolve(param.token);
? this.resolveAll(param.token, ctor)
: this.resolve(param.token, ctor);
}
return this.resolve(param);
return this.resolve(param, ctor);
});

return new ctor(...params);
Expand Down
6 changes: 5 additions & 1 deletion src/factories/factory-function.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import DependencyContainer from "../types/dependency-container";
import {constructor} from "../types";

type FactoryFunction<T> = (dependencyContainer: DependencyContainer) => T;
type FactoryFunction<T> = (
dependencyContainer: DependencyContainer,
target?: constructor<T>
) => T;

export default FactoryFunction;
8 changes: 6 additions & 2 deletions src/factories/instance-caching-factory.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import DependencyContainer from "../types/dependency-container";
import FactoryFunction from "./factory-function";
import {constructor} from "../types";

export default function instanceCachingFactory<T>(
factoryFunc: FactoryFunction<T>
): FactoryFunction<T> {
let instance: T;
return (dependencyContainer: DependencyContainer) => {
return (
dependencyContainer: DependencyContainer,
target?: constructor<T>
) => {
if (instance == undefined) {
instance = factoryFunc(dependencyContainer);
instance = factoryFunc(dependencyContainer, target);
}
return instance;
};
Expand Down
24 changes: 19 additions & 5 deletions src/factories/predicate-aware-class-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,34 @@ import constructor from "../types/constructor";
import FactoryFunction from "./factory-function";

export default function predicateAwareClassFactory<T>(
predicate: (dependencyContainer: DependencyContainer) => boolean,
predicate: (
dependencyContainer: DependencyContainer,
target?: constructor<T>
) => boolean,
trueConstructor: constructor<T>,
falseConstructor: constructor<T>,
useCaching = true
): FactoryFunction<T> {
let instance: T;
let previousPredicate: boolean;
return (dependencyContainer: DependencyContainer) => {
const currentPredicate = predicate(dependencyContainer);
return (
dependencyContainer: DependencyContainer,
target?: constructor<T>
) => {
const currentPredicate = predicate(dependencyContainer, target);
if (!useCaching || previousPredicate !== currentPredicate) {
if ((previousPredicate = currentPredicate)) {
instance = dependencyContainer.resolve(trueConstructor);
if (target) {
instance = dependencyContainer.resolve(trueConstructor, target);
} else {
instance = dependencyContainer.resolve(trueConstructor);
}
} else {
instance = dependencyContainer.resolve(falseConstructor);
if (target) {
instance = dependencyContainer.resolve(falseConstructor, target);
} else {
instance = dependencyContainer.resolve(falseConstructor);
}
}
}
return instance;
Expand Down
6 changes: 5 additions & 1 deletion src/providers/factory-provider.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import DependencyContainer from "../types/dependency-container";
import Provider from "./provider";
import {constructor} from "../types";

/**
* Provide a dependency using a factory.
* Unlike the other providers, this does not support instance caching. If
* you need instance caching, your factory method must implement it.
*/
export default interface FactoryProvider<T> {
useFactory: (dependencyContainer: DependencyContainer) => T;
useFactory: (
dependencyContainer: DependencyContainer,
target?: constructor<T>
) => T;
}

export function isFactoryProvider<T>(
Expand Down
2 changes: 2 additions & 0 deletions src/types/dependency-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ export default interface DependencyContainer {
instance: T
): DependencyContainer;
resolve<T>(token: InjectionToken<T>): T;
resolve<T>(token: InjectionToken<T>, parent: constructor<any>): T;
resolveAll<T>(token: InjectionToken<T>): T[];
resolveAll<T>(token: InjectionToken<T>, parent: constructor<any>): T[];
isRegistered<T>(token: InjectionToken<T>): boolean;
reset(): void;
createChildContainer(): DependencyContainer;
Expand Down