aurelia-dependency-injection
Version:
A lightweight, extensible dependency injection container for JavaScript.
655 lines (600 loc) • 20.5 kB
text/typescript
import { protocol } from 'aurelia-metadata';
import { Container } from './container';
import { autoinject } from './injection';
import type {
PrimitiveOrDependencyCtor,
DependencyCtorOrFunctor,
PrimitiveOrDependencyCtorOrFunctor,
DependencyCtor,
DependencyFunctor,
ImplOrAny,
Impl,
Args
} from './types';
/**
* Decorator: Indicates that the decorated class/object is a custom resolver.
*/
export const resolver: {
decorates?: (key: any) => key is { get(container: Container, key: any): any };
} & (() => any) = ((protocol as unknown) as { create(...args: any[]): any }).create(
'aurelia:resolver',
(target): string | boolean => {
if (!(typeof target.get === 'function')) {
return 'Resolvers must implement: get(container: Container, key: any): any';
}
return true;
}
);
/**
* Used to allow functions/classes to specify custom dependency resolution logic.
*/
export interface Resolver {
/**
* Called by the container to allow custom resolution of dependencies for a
* function/class.
* @param container The container to resolve from.
* @param key The key that the resolver was registered as.
* @return Returns the resolved object.
*/
get(container: Container, key: any): any;
}
export enum Strategy {
instance = 0,
singleton = 1,
transient = 2,
function = 3,
array = 4,
alias = 5
}
export type IStrategy = 1 | 2 | 3 | 4 | 5;
export type StrategyFunctor<
TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>
> = (
container?: Container,
ctor?: PrimitiveOrDependencyCtorOrFunctor<TBase, TImpl, TArgs>,
strategyResolver?: any
) => TImpl;
export interface StrategyState<
TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>
> {
[Strategy.instance]: TImpl;
[Strategy.singleton]: DependencyCtorOrFunctor<TBase, TImpl, TArgs>;
[Strategy.transient]: DependencyCtorOrFunctor<TBase, TImpl, TArgs>;
[Strategy.function]: StrategyFunctor<TBase, TImpl, TArgs>;
/**
* For typings purposes, this is done as ({ get: StrategyFunctor } | TImpl)[]
* But it should be understood, and used as [{ get: StrategyFunctor }, ...TImp[]]
*/
[Strategy.array]: ({
get: (
container: Container,
key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>
) => TImpl
} | TImpl)[];
[Strategy.alias]: any;
}
function isStrategy<
TBase,
TImpl extends Impl<TBase>,
TArgs extends Args<TBase>,
TKey extends keyof StrategyState<TBase, TImpl, TArgs>
>(
actual: Strategy,
expected: TKey,
state):
state is StrategyState<TBase, TImpl, TArgs>[TKey] {
return actual === expected;
}
/**
* Used to resolve instances, singletons, transients, aliases
*/
export class StrategyResolver<
TBase,
TImpl extends Impl<TBase>,
TArgs extends Args<TBase>,
TStrategyKey extends keyof StrategyState<TBase, TImpl, TArgs>> {
public strategy: keyof StrategyState<TBase, TImpl, TArgs>;
public state: StrategyState<TBase, TImpl, TArgs>[keyof StrategyState<TBase, TImpl, TArgs>];
/**
* Creates an instance of the StrategyResolver class.
* @param strategy The type of resolution strategy.
* @param state The state associated with the resolution strategy.
*/
constructor(strategy: TStrategyKey, state: StrategyState<TBase, TImpl, TArgs>[TStrategyKey]) {
this.strategy = strategy;
this.state = state;
}
/**
* Called by the container to allow custom resolution of dependencies for a
* function/class.
* @param container The container to resolve from.
* @param key The key that the resolver was registered as.
* @return Returns the resolved object.
*/
public get(container: Container, key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>) {
if (isStrategy<TBase, TImpl, TArgs, Strategy.instance>(this.strategy, Strategy.instance, this.state)) {
return this.state;
}
if (isStrategy<TBase, TImpl, TArgs, Strategy.singleton>(this.strategy, Strategy.singleton, this.state)) {
const singleton = container.invoke<TBase, TImpl, TArgs>(this.state);
this.state = singleton;
this.strategy = 0;
return singleton;
}
if (isStrategy<TBase, TImpl, TArgs, Strategy.transient>(this.strategy, Strategy.transient, this.state)) {
return container.invoke<TBase, TImpl, TArgs>(this.state);
}
if (isStrategy<TBase, TImpl, TArgs, Strategy.function>(this.strategy, Strategy.function, this.state)) {
return this.state(container, key, this);
}
if (isStrategy<TBase, TImpl, TArgs, Strategy.array>(this.strategy, Strategy.array, this.state)) {
return (this.state[0] as { get: StrategyFunctor<TBase, TImpl, TArgs> }).get(container, key);
}
if (isStrategy<TBase, TImpl, TArgs, Strategy.alias>(this.strategy, Strategy.alias, this.state)) {
return container.get(this.state) as TImpl;
}
throw new Error('Invalid strategy: ' + this.strategy);
}
}
/**
* Used to allow functions/classes to specify lazy resolution logic.
*/
export class Lazy<TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>> {
/** @internal */
public _key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>;
/**
* A non existent property to help TS distinguish resolvers
*
* This is semi-private, and should not be used by application
*/
__resolver_type__!: 'lazy';
/**
* Creates an instance of the Lazy class.
* @param key The key to lazily resolve.
*/
constructor(key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>) {
this._key = key;
}
/**
* Called by the container to lazily resolve the dependency into a lazy locator
* function.
* @param container The container to resolve from.
* @return Returns a function which can be invoked at a later time to obtain
* the actual dependency.
*/
public get(container: Container): () => ImplOrAny<TImpl> {
return () => container.get<TBase, TImpl, TArgs>(this._key);
}
/**
* Creates a Lazy Resolver for the supplied key.
* @param key The key to lazily resolve.
* @return Returns an instance of Lazy for the key.
*/
public static of<
TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>>(
key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>
) {
return new Lazy<TBase, TImpl, TArgs>(key);
}
}
/**
* Used to allow functions/classes to specify resolution of all matches to a key.
*/
export class All<TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>> {
/** @internal */
public _key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>;
/**
* A non existent property to help TS distinguish resolvers
*
* This is semi-private, and should not be used by application
*/
__resolver_type__!: 'all';
/**
* Creates an instance of the All class.
* @param key The key to lazily resolve all matches for.
*/
constructor(key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>) {
this._key = key;
}
/**
* Called by the container to resolve all matching dependencies as an array of
* instances.
* @param container The container to resolve from.
* @return Returns an array of all matching instances.
*/
public get(container: Container): TImpl[] {
return container.getAll(this._key);
}
/**
* Creates an All Resolver for the supplied key.
* @param key The key to resolve all instances for.
* @return Returns an instance of All for the key.
*/
public static of<TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>>(
key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>
): All<TBase, TImpl, TArgs> {
return new All(key);
}
}
/**
* Used to allow functions/classes to specify an optional dependency, which will
* be resolved only if already registred with the container.
*/
export class Optional<TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>> {
/** @internal */
public _key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>;
/** @internal */
public _checkParent: boolean;
/**
* A non existent property to help TS distinguish resolvers
*
* This is semi-private, and should not be used by application
*/
__resolver_type__!: 'optional';
/**
* Creates an instance of the Optional class.
* @param key The key to optionally resolve for.
* @param checkParent Indicates whether or not the parent container hierarchy
* should be checked.
*/
constructor(key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>, checkParent: boolean = true) {
this._key = key;
this._checkParent = checkParent;
}
/**
* Called by the container to provide optional resolution of the key.
* @param container The container to resolve from.
* @return Returns the instance if found; otherwise null.
*/
public get(container: Container): TImpl | null {
if (container.hasResolver(this._key, this._checkParent)) {
return container.get(this._key);
}
return null;
}
/**
* Creates an Optional Resolver for the supplied key.
* @param key The key to optionally resolve for.
* @param [checkParent=true] Indicates whether or not the parent container
* hierarchy should be checked.
* @return Returns an instance of Optional for the key.
*/
public static of<TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>>(
key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>,
checkParent: boolean = true): Optional<TBase, TImpl, TArgs> {
return new Optional(key, checkParent);
}
}
/**
* Used to inject the dependency from the parent container instead of the current
* one.
*/
export class Parent<TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>> {
/** @internal */
public _key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>;
/**
* A non existent property to help TS distinguish resolvers
*
* This is semi-private, and should not be used by application
*/
__resolver_type__!: 'parent';
/**
* Creates an instance of the Parent class.
* @param key The key to resolve from the parent container.
*/
constructor(key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>) {
this._key = key;
}
/**
* Called by the container to load the dependency from the parent container
* @param container The container to resolve the parent from.
* @return Returns the matching instance from the parent container
*/
public get(container: Container): TImpl | null {
return container.parent ? container.parent.get(this._key) : null;
}
/**
* Creates a Parent Resolver for the supplied key.
* @param key The key to resolve.
* @return Returns an instance of Parent for the key.
*/
public static of<TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>>(
key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>
): Parent<TBase, TImpl, TArgs> {
return new Parent(key);
}
}
/**
* Used to allow injecting dependencies but also passing data to the constructor.
*/
export class Factory<TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>> {
/** @internal */
public _key: PrimitiveOrDependencyCtorOrFunctor<TBase, TImpl, TArgs>;
/**
* A non existent property to help TS distinguish resolvers
*
* This is semi-private, and should not be used by application
*/
__resolver_type__!: 'factory';
/**
* Creates an instance of the Factory class.
* @param key The key to resolve from the parent container.
*/
constructor(key: PrimitiveOrDependencyCtorOrFunctor<TBase, TImpl, TArgs>) {
this._key = key;
}
/**
* Called by the container to pass the dependencies to the constructor.
* @param container The container to invoke the constructor with dependencies
* and other parameters.
* @return Returns a function that can be invoked to resolve dependencies
* later, and the rest of the parameters.
*/
public get(container: Container): DependencyFunctor<TBase, TImpl, TArgs> {
let fn = this._key;
const resolver = container.getResolver(fn);
if (resolver && resolver.strategy === Strategy.function) {
fn = resolver.state;
}
return (...rest) => container.invoke(fn as DependencyCtorOrFunctor<TBase, TImpl, TArgs>, rest);
}
/**
* Creates a Factory Resolver for the supplied key.
* @param key The key to resolve.
* @return Returns an instance of Factory for the key.
*/
public static of<TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>>(
key: DependencyCtor<TBase, TImpl, TArgs>
): Factory<TBase, TImpl, TArgs> {
return new Factory(key);
}
}
/**
* Used to inject a new instance of a dependency, without regard for existing
* instances in the container. Instances can optionally be registered in the
* container
* under a different key by supplying a key using the `as` method.
*/
export class NewInstance<
TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>> {
/** @internal */
public key: PrimitiveOrDependencyCtorOrFunctor<TBase, TImpl, TArgs>;
/** @internal */
public asKey: PrimitiveOrDependencyCtorOrFunctor<TBase, TImpl, TArgs>;
/** @internal */
public dynamicDependencies: TArgs[number][];
/**
* A non existent property to help TS distinguish resolvers
*
* This is semi-private, and should not be used by application
*/
__resolver_type__!: 'newInstance';
/**
* Creates an instance of the NewInstance class.
* @param key The key to resolve/instantiate.
* @param dynamicDependencies An optional list of dynamic dependencies.
*/
constructor(key: PrimitiveOrDependencyCtorOrFunctor<TBase, TImpl, TArgs>, ...dynamicDependencies: TArgs[number][]) {
this.key = key;
this.asKey = key;
this.dynamicDependencies = dynamicDependencies;
}
/**
* Called by the container to instantiate the dependency and potentially
* register
* as another key if the `as` method was used.
* @param container The container to resolve the parent from.
* @return Returns the matching instance from the parent container
*/
public get(container: Container) {
const dynamicDependencies =
this.dynamicDependencies.length > 0
? this.dynamicDependencies.map(dependency =>
dependency['protocol:aurelia:resolver']
? dependency.get(container)
: container.get(dependency)
)
: undefined;
let fn = this.key;
const resolver = container.getResolver(fn);
if (resolver && resolver.strategy === 3) {
fn = resolver.state;
}
const instance = container.invoke(fn as DependencyCtorOrFunctor<TBase, TImpl, TArgs>, dynamicDependencies);
container.registerInstance(this.asKey, instance);
return instance;
}
/**
* Instructs the NewInstance resolver to register the resolved instance using
* the supplied key.
* @param key The key to register the instance with.
* @return Returns the NewInstance resolver.
*/
public as(key: PrimitiveOrDependencyCtorOrFunctor<TBase, TImpl, TArgs>) {
this.asKey = key;
return this;
}
/**
* Creates an NewInstance Resolver for the supplied key.
* @param key The key to resolve/instantiate.
* @param dynamicDependencies An optional list of dynamic dependencies.
* @return Returns an instance of NewInstance for the key.
*/
public static of<TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>>(
key: PrimitiveOrDependencyCtorOrFunctor<TBase, TImpl, TArgs>,
...dynamicDependencies: TArgs[number][]): NewInstance<TBase, TImpl, TArgs> {
return new NewInstance(key, ...dynamicDependencies);
}
}
/**
* Used by parameter decorators to call autoinject for the target and retrieve
* the target's inject property.
* @param target The target class.
* @return Returns the target's own inject property.
*/
export function getDecoratorDependencies<
TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>>(
target: DependencyCtor<TBase, TImpl, TArgs> & { inject?: TArgs[number][] }
) {
autoinject(target);
return target.inject;
}
/**
* Decorator: Specifies the dependency should be lazy loaded
*/
export function lazy<
TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>>(
keyValue: any
) {
return (
target: { new (...args: TArgs): TBase | TImpl },
_key,
index: number
) => {
const inject = getDecoratorDependencies(target)!;
inject[index] = Lazy.of(keyValue);
};
}
/**
* Decorator: Specifies the dependency should load all instances of the given
* key.
*/
export function all<
TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>>(
keyValue: any
) {
return (
target: DependencyCtor<TBase, TImpl, TArgs> & { inject?: TArgs[number][] },
_key,
index: number
) => {
const inject = getDecoratorDependencies(target)!;
inject[index] = All.of(keyValue);
};
}
/**
* Decorator: Specifies the dependency as optional
*/
export function optional<
TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>>(
checkParentOrTarget: boolean = true) {
const deco = (checkParent: boolean) => {
return (
target: DependencyCtor<TBase, TImpl, TArgs> & { inject?: TArgs[number][] },
_key,
index: number) => {
const inject = getDecoratorDependencies(target)!;
inject[index] = Optional.of(inject[index], checkParent);
};
};
if (typeof checkParentOrTarget === 'boolean') {
return deco(checkParentOrTarget);
}
return deco(true);
}
/**
* Decorator: Specifies the dependency to look at the parent container for
* resolution
*/
export function parent<
TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>>(
target: DependencyCtor<TBase, TImpl, TArgs> & { inject?: TArgs[number][] },
_key,
index: number) {
const inject = getDecoratorDependencies(target)!;
inject[index] = Parent.of(inject[index]);
}
/**
* Decorator: Specifies the dependency to create a factory method, that can
* accept optional arguments
*/
export function factory<
TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>>(
keyValue: any
) {
return (
target: DependencyCtor<TBase, TImpl, TArgs> & { inject?: TArgs[number][] },
_key,
index: number
) => {
const inject = getDecoratorDependencies(target)!;
inject[index] = Factory.of(keyValue);
};
}
/**
* Decorator: Specifies the dependency as a new instance. Instances can optionally be registered in the container
* under a different key and/or use dynamic dependencies
*/
export function newInstance<
TBase,
TImpl extends Impl<TBase> = Impl<TBase>,
TArgs extends Args<TBase> = Args<TBase>
>(
asKeyOrTarget?: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs> & { inject?: TArgs[number][] },
...dynamicDependencies: TArgs[number][]
) {
const deco = (asKey?: typeof asKeyOrTarget) => {
return (
target: { new (...args: any[]): any },
_key,
index: number
) => {
const inject = getDecoratorDependencies(target)!;
inject[index] = NewInstance.of(inject[index], ...dynamicDependencies);
if (!!asKey) {
inject[index].as(asKey);
}
};
};
if (arguments.length >= 1) {
return deco(asKeyOrTarget);
}
return deco();
}