Import other modules into monorepo
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
272
src/di/Container.ts
Normal file
272
src/di/Container.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import {DependencyKey, InstanceRef, Instantiable, isInstantiable, StaticClass} from "./types";
|
||||
import {AbstractFactory} from "./factory/AbstractFactory";
|
||||
import {collect, Collection, globalRegistry, logIfDebugging} from "../util";
|
||||
import {Factory} from "./factory/Factory";
|
||||
import {DuplicateFactoryKeyError} from "./error/DuplicateFactoryKeyError";
|
||||
import {ClosureFactory} from "./factory/ClosureFactory";
|
||||
import NamedFactory from "./factory/NamedFactory";
|
||||
import SingletonFactory from "./factory/SingletonFactory";
|
||||
import {InvalidDependencyKeyError} from "./error/InvalidDependencyKeyError";
|
||||
|
||||
export type MaybeFactory = AbstractFactory | undefined
|
||||
export type MaybeDependency = any | undefined
|
||||
export type ResolvedDependency = { paramIndex: number, key: DependencyKey, resolved: any }
|
||||
|
||||
/**
|
||||
* A container of resolve-able dependencies that are created via inversion-of-control.
|
||||
*/
|
||||
export class Container {
|
||||
/**
|
||||
* Get the global instance of this container.
|
||||
*/
|
||||
public static getContainer(): Container {
|
||||
const existing = <Container | undefined> globalRegistry.getGlobal('extollo/injector')
|
||||
if ( !existing ) {
|
||||
const container = new Container()
|
||||
globalRegistry.setGlobal('extollo/injector', container)
|
||||
return container
|
||||
}
|
||||
|
||||
return existing
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection of factories registered with this container.
|
||||
* @type Collection<AbstractFactory>
|
||||
*/
|
||||
protected factories: Collection<AbstractFactory> = new Collection<AbstractFactory>()
|
||||
|
||||
/**
|
||||
* Collection of singleton instances produced by this container.
|
||||
* @type Collection<InstanceRef>
|
||||
*/
|
||||
protected instances: Collection<InstanceRef> = new Collection<InstanceRef>()
|
||||
|
||||
constructor() {
|
||||
this.registerSingletonInstance<Container>(Container, this)
|
||||
this.registerSingleton('injector', this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a basic instantiable class as a standard Factory with this container.
|
||||
* @param {Instantiable} dependency
|
||||
*/
|
||||
register(dependency: Instantiable<any>) {
|
||||
if ( this.resolve(dependency) )
|
||||
throw new DuplicateFactoryKeyError(dependency)
|
||||
|
||||
const factory = new Factory(dependency)
|
||||
this.factories.push(factory)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the given function as a factory within the container.
|
||||
* @param {string} name - unique name to identify the factory in the container
|
||||
* @param {function} producer - factory to produce a value
|
||||
*/
|
||||
registerProducer(name: string | StaticClass<any, any>, producer: () => any) {
|
||||
if ( this.resolve(name) )
|
||||
throw new DuplicateFactoryKeyError(name)
|
||||
|
||||
const factory = new ClosureFactory(name, producer)
|
||||
this.factories.push(factory)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a basic instantiable class as a standard Factory with this container,
|
||||
* identified by a string name rather than static class.
|
||||
* @param {string} name - unique name to identify the factory in the container
|
||||
* @param {Instantiable} dependency
|
||||
*/
|
||||
registerNamed(name: string, dependency: Instantiable<any>) {
|
||||
if ( this.resolve(name) )
|
||||
throw new DuplicateFactoryKeyError(name)
|
||||
|
||||
const factory = new NamedFactory(name, dependency)
|
||||
this.factories.push(factory)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a value as a singleton in the container. It will not be instantiated, but
|
||||
* can be injected by its unique name.
|
||||
* @param {string} key - unique name to identify the singleton in the container
|
||||
* @param value
|
||||
*/
|
||||
registerSingleton(key: string, value: any) {
|
||||
if ( this.resolve(key) )
|
||||
throw new DuplicateFactoryKeyError(key)
|
||||
|
||||
this.factories.push(new SingletonFactory(value, key))
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a static class to the container along with its already-instantiated
|
||||
* instance that will be used to resolve the class.
|
||||
* @param staticClass
|
||||
* @param instance
|
||||
*/
|
||||
registerSingletonInstance<T>(staticClass: Instantiable<T>, instance: T) {
|
||||
if ( this.resolve(staticClass) )
|
||||
throw new DuplicateFactoryKeyError(staticClass)
|
||||
|
||||
this.register(staticClass)
|
||||
this.instances.push({
|
||||
key: staticClass,
|
||||
value: instance,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a given factory with the container.
|
||||
* @param {AbstractFactory} factory
|
||||
*/
|
||||
registerFactory(factory: AbstractFactory) {
|
||||
if ( !this.factories.includes(factory) )
|
||||
this.factories.push(factory)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the container has an already-produced value for the given key.
|
||||
* @param {DependencyKey} key
|
||||
*/
|
||||
hasInstance(key: DependencyKey): boolean {
|
||||
return this.instances.where('key', '=', key).isNotEmpty()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the container has a factory for the given key.
|
||||
* @param {DependencyKey} key
|
||||
*/
|
||||
hasKey(key: DependencyKey): boolean {
|
||||
return !!this.resolve(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the already-produced value for the given key, if one exists.
|
||||
* @param {DependencyKey} key
|
||||
*/
|
||||
getExistingInstance(key: DependencyKey): MaybeDependency {
|
||||
const instances = this.instances.where('key', '=', key)
|
||||
if ( instances.isNotEmpty() ) return instances.first()
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the factory for the given key, if one is registered with this container.
|
||||
* @param {DependencyKey} key
|
||||
*/
|
||||
resolve(key: DependencyKey): MaybeFactory {
|
||||
const factory = this.factories.firstWhere(item => item.match(key))
|
||||
if ( factory ) return factory
|
||||
else logIfDebugging('extollo.di.injector', 'unable to resolve factory', factory, this.factories)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the dependency key. If a singleton value for that key already exists in this container,
|
||||
* return that value. Otherwise, use the factory an given parameters to produce and return the value.
|
||||
* @param {DependencyKey} key
|
||||
* @param {...any} parameters
|
||||
*/
|
||||
resolveAndCreate(key: DependencyKey, ...parameters: any[]): any {
|
||||
logIfDebugging('extollo.di.injector', 'resolveAndCreate', key, {parameters})
|
||||
|
||||
// If we've already instantiated this, just return that
|
||||
const instance = this.getExistingInstance(key)
|
||||
logIfDebugging('extollo.di.injector', 'resolveAndCreate existing instance?', instance)
|
||||
if ( typeof instance !== 'undefined' ) return instance.value
|
||||
|
||||
// Otherwise, attempt to create it
|
||||
const factory = this.resolve(key)
|
||||
logIfDebugging('extollo.di.injector', 'resolveAndCreate factory', factory)
|
||||
if ( !factory )
|
||||
throw new InvalidDependencyKeyError(key)
|
||||
|
||||
// Produce and store a new instance
|
||||
const new_instance = this.produceFactory(factory, parameters)
|
||||
this.instances.push({
|
||||
key,
|
||||
value: new_instance,
|
||||
})
|
||||
|
||||
return new_instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a factory and manually-provided parameters, resolve the dependencies for the
|
||||
* factory and produce its value.
|
||||
* @param {AbstractFactory} factory
|
||||
* @param {array} parameters
|
||||
*/
|
||||
protected produceFactory(factory: AbstractFactory, parameters: any[]) {
|
||||
// Create the dependencies for the factory
|
||||
const keys = factory.getDependencyKeys().filter(req => this.hasKey(req.key))
|
||||
const dependencies = keys.map<ResolvedDependency>(req => {
|
||||
return {
|
||||
paramIndex: req.paramIndex,
|
||||
key: req.key,
|
||||
resolved: this.resolveAndCreate(req.key),
|
||||
}
|
||||
}).sortBy('paramIndex')
|
||||
|
||||
// Build the arguments for the factory, using dependencies in the
|
||||
// correct paramIndex positions, or parameters of we don't have
|
||||
// the dependency.
|
||||
const construction_args = []
|
||||
let params = collect(parameters).reverse()
|
||||
for ( let i = 0; i <= dependencies.max('paramIndex'); i++ ) {
|
||||
const dep = dependencies.firstWhere('paramIndex', '=', i)
|
||||
if ( dep ) construction_args.push(dep.resolved)
|
||||
else construction_args.push(params.pop())
|
||||
}
|
||||
|
||||
// Produce a new instance
|
||||
const inst = factory.produce(construction_args, params.reverse().all())
|
||||
|
||||
factory.getInjectedProperties().each(dependency => {
|
||||
if ( dependency.key && inst ) {
|
||||
inst[dependency.property] = this.resolveAndCreate(dependency.key)
|
||||
}
|
||||
})
|
||||
|
||||
return inst
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of the given target. The target can either be a DependencyKey registered with
|
||||
* this container (in which case, the singleton value will be returned), or an instantiable class.
|
||||
*
|
||||
* If the instantiable class has the Injectable decorator, its injectable parameters will be automatically
|
||||
* injected into the instance.
|
||||
* @param {DependencyKey} target
|
||||
* @param {...any} parameters
|
||||
*/
|
||||
make<T>(target: DependencyKey, ...parameters: any[]): T {
|
||||
if ( this.hasKey(target) )
|
||||
return this.resolveAndCreate(target, ...parameters)
|
||||
else if ( typeof target !== 'string' && isInstantiable(target) )
|
||||
return this.produceFactory(new Factory(target), parameters)
|
||||
else
|
||||
throw new TypeError(`Invalid or unknown make target: ${target}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a collection of dependency keys required by the given target, if it is registered with this container.
|
||||
* @param {DependencyKey} target
|
||||
*/
|
||||
getDependencies(target: DependencyKey): Collection<DependencyKey> {
|
||||
const factory = this.resolve(target)
|
||||
|
||||
if ( !factory )
|
||||
throw new InvalidDependencyKeyError(target)
|
||||
|
||||
return factory.getDependencyKeys().pluck('key')
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a different container, copy the factories and instances from this container over to it.
|
||||
* @param container
|
||||
*/
|
||||
cloneTo(container: Container) {
|
||||
container.factories = this.factories.clone()
|
||||
container.instances = this.instances.clone()
|
||||
}
|
||||
}
|
||||
61
src/di/ScopedContainer.ts
Normal file
61
src/di/ScopedContainer.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {Container, MaybeDependency, MaybeFactory} from "./Container"
|
||||
import {DependencyKey} from "./types"
|
||||
|
||||
/**
|
||||
* A container that uses some parent container as a base, but
|
||||
* can have other factories distinct from that parent.
|
||||
*
|
||||
* If an instance is not found in this container, it will be resolved from
|
||||
* the parent container.
|
||||
*
|
||||
* However, if an instance IS found in this container, it will ALWAYS be
|
||||
* resolved from this container, rather than the parent.
|
||||
*
|
||||
* This can be used to create scope-specific containers that can still resolve
|
||||
* the global dependencies, while keeping scope-specific dependencies separate.
|
||||
*
|
||||
* @example
|
||||
* The Request class from @extollo/lib is a ScopedContainer. It can resolve
|
||||
* all dependencies that exist in the global Container, but it can also have
|
||||
* request-specific services (like the Session) injected into it.
|
||||
*
|
||||
* @extends Container
|
||||
*/
|
||||
export class ScopedContainer extends Container {
|
||||
/**
|
||||
* Create a new scoped container based on a parent container instance.
|
||||
* @param container
|
||||
*/
|
||||
public static fromParent(container: Container) {
|
||||
return new ScopedContainer(container);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private parentContainer: Container,
|
||||
) {
|
||||
super()
|
||||
this.registerSingletonInstance<ScopedContainer>(ScopedContainer, this)
|
||||
}
|
||||
|
||||
hasInstance(key: DependencyKey): boolean {
|
||||
return super.hasInstance(key) || this.parentContainer.hasInstance(key)
|
||||
}
|
||||
|
||||
hasKey(key: DependencyKey): boolean {
|
||||
return super.hasKey(key) || this.parentContainer.hasKey(key)
|
||||
}
|
||||
|
||||
getExistingInstance(key: DependencyKey): MaybeDependency {
|
||||
const inst = super.getExistingInstance(key)
|
||||
if ( inst ) return inst;
|
||||
|
||||
return this.parentContainer.getExistingInstance(key);
|
||||
}
|
||||
|
||||
resolve(key: DependencyKey): MaybeFactory {
|
||||
const factory = super.resolve(key);
|
||||
if ( factory ) return factory;
|
||||
|
||||
return this.parentContainer?.resolve(key);
|
||||
}
|
||||
}
|
||||
151
src/di/decorator/injection.ts
Normal file
151
src/di/decorator/injection.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import 'reflect-metadata'
|
||||
import {collect, Collection} from "../../util";
|
||||
import {
|
||||
DependencyKey,
|
||||
DependencyRequirement,
|
||||
DEPENDENCY_KEYS_METADATA_KEY,
|
||||
DEPENDENCY_KEYS_PROPERTY_METADATA_KEY,
|
||||
isInstantiable,
|
||||
InjectionType,
|
||||
DEPENDENCY_KEYS_SERVICE_TYPE_KEY,
|
||||
PropertyDependency,
|
||||
} from "../types";
|
||||
import {Container} from "../Container";
|
||||
|
||||
/**
|
||||
* Get a collection of dependency requirements for the given target object.
|
||||
* @param {Object} target
|
||||
* @return Collection<DependencyRequirement>
|
||||
*/
|
||||
function initDependencyMetadata(target: Object): Collection<DependencyRequirement> {
|
||||
const paramTypes = Reflect.getMetadata('design:paramtypes', target)
|
||||
return collect<DependencyKey>(paramTypes).map<DependencyRequirement>((type, idx) => {
|
||||
return {
|
||||
paramIndex: idx,
|
||||
key: type,
|
||||
overridden: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Class decorator that marks a class as injectable. When this is applied, dependency
|
||||
* metadata for the constructors params is resolved and stored in metadata.
|
||||
* @constructor
|
||||
*/
|
||||
export const Injectable = (): ClassDecorator => {
|
||||
return (target) => {
|
||||
const meta = initDependencyMetadata(target)
|
||||
const existing = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, target)
|
||||
const new_meta = new Collection<DependencyRequirement>()
|
||||
|
||||
if ( existing ) {
|
||||
const max_new = meta.max('paramIndex')
|
||||
const max_existing = existing.max('paramIndex')
|
||||
for ( let i = 0; i <= Math.max(max_new, max_existing); i++ ) {
|
||||
const existing_dr = existing.firstWhere('paramIndex', '=', i)
|
||||
const new_dr = meta.firstWhere('paramIndex', '=', i)
|
||||
|
||||
if ( existing_dr && !new_dr ) {
|
||||
new_meta.push(existing_dr)
|
||||
} else if ( new_dr && !existing_dr ) {
|
||||
new_meta.push(new_dr)
|
||||
} else if ( new_dr && existing_dr ) {
|
||||
if ( existing_dr.overridden ) {
|
||||
new_meta.push(existing_dr)
|
||||
} else {
|
||||
new_meta.push(new_dr)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
new_meta.concat(meta)
|
||||
}
|
||||
|
||||
Reflect.defineMetadata(DEPENDENCY_KEYS_METADATA_KEY, new_meta, target)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the given class property to be injected by the container.
|
||||
* If a `key` is specified, that DependencyKey will be injected.
|
||||
* Otherwise, the DependencyKey is inferred from the type annotation.
|
||||
* @param key
|
||||
* @constructor
|
||||
*/
|
||||
export const Inject = (key?: DependencyKey): PropertyDecorator => {
|
||||
return (target, property) => {
|
||||
let propertyMetadata = Reflect.getMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, target?.constructor || target) as Collection<PropertyDependency>
|
||||
if ( !propertyMetadata ) {
|
||||
propertyMetadata = new Collection<PropertyDependency>()
|
||||
Reflect.defineMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, propertyMetadata, target?.constructor || target)
|
||||
}
|
||||
|
||||
const type = Reflect.getMetadata('design:type', target, property)
|
||||
if ( !key && type ) key = type
|
||||
|
||||
if ( key ) {
|
||||
const existing = propertyMetadata.firstWhere('property', '=', property)
|
||||
if ( existing ) {
|
||||
existing.key = key
|
||||
} else {
|
||||
propertyMetadata.push({ property, key })
|
||||
}
|
||||
}
|
||||
|
||||
Reflect.defineMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, propertyMetadata, target?.constructor || target)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter decorator to manually mark a parameter as being an injection target on injectable
|
||||
* classes. This can be used to override the dependency key of a given parameter.
|
||||
* @param {DependencyKey} key
|
||||
* @constructor
|
||||
*/
|
||||
export const InjectParam = (key: DependencyKey): ParameterDecorator => {
|
||||
return (target, property, paramIndex) => {
|
||||
if ( !Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, target) ) {
|
||||
Reflect.defineMetadata(DEPENDENCY_KEYS_METADATA_KEY, initDependencyMetadata(target), target)
|
||||
}
|
||||
|
||||
const meta = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, target)
|
||||
const req = meta.firstWhere('paramIndex', '=', paramIndex)
|
||||
if ( req ) {
|
||||
req.key = key
|
||||
req.overridden = true
|
||||
} else {
|
||||
meta.push({
|
||||
paramIndex,
|
||||
key,
|
||||
overridden: true
|
||||
})
|
||||
}
|
||||
|
||||
Reflect.defineMetadata(DEPENDENCY_KEYS_METADATA_KEY, meta, target)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class decorator that registers the class as a singleton instance in the global container.
|
||||
* @param {string} name
|
||||
*/
|
||||
export const Singleton = (name?: string): ClassDecorator => {
|
||||
return (target) => {
|
||||
if ( isInstantiable(target) ) {
|
||||
const injectionType: InjectionType = {
|
||||
type: name ? 'named' : 'singleton',
|
||||
...(name ? { name } : {})
|
||||
}
|
||||
|
||||
Reflect.defineMetadata(DEPENDENCY_KEYS_SERVICE_TYPE_KEY, injectionType, target)
|
||||
Injectable()(target)
|
||||
|
||||
if ( name ) {
|
||||
Container.getContainer().registerNamed(name, target)
|
||||
} else {
|
||||
Container.getContainer().register(target)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/di/error/DuplicateFactoryKeyError.ts
Normal file
11
src/di/error/DuplicateFactoryKeyError.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import {DependencyKey} from "../types";
|
||||
|
||||
/**
|
||||
* Error thrown when a factory is registered with a duplicate dependency key.
|
||||
* @extends Error
|
||||
*/
|
||||
export class DuplicateFactoryKeyError extends Error {
|
||||
constructor(key: DependencyKey) {
|
||||
super(`A factory definition already exists with the key for ${key}.`)
|
||||
}
|
||||
}
|
||||
11
src/di/error/InvalidDependencyKeyError.ts
Normal file
11
src/di/error/InvalidDependencyKeyError.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import {DependencyKey} from "../types";
|
||||
|
||||
/**
|
||||
* Error thrown when a dependency key that has not been registered is passed to a resolver.
|
||||
* @extends Error
|
||||
*/
|
||||
export class InvalidDependencyKeyError extends Error {
|
||||
constructor(key: DependencyKey) {
|
||||
super(`No such dependency is registered with this container: ${key}`)
|
||||
}
|
||||
}
|
||||
44
src/di/factory/AbstractFactory.ts
Normal file
44
src/di/factory/AbstractFactory.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {DependencyRequirement, PropertyDependency} from "../types";
|
||||
import { Collection } from "../../util";
|
||||
|
||||
/**
|
||||
* Abstract base class for dependency container factories.
|
||||
* @abstract
|
||||
*/
|
||||
export abstract class AbstractFactory {
|
||||
protected constructor(
|
||||
/**
|
||||
* Token that was registered for this factory. In most cases, this is the static
|
||||
* form of the item that is to be produced by this factory.
|
||||
* @var
|
||||
* @protected
|
||||
*/
|
||||
protected token: any
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Produce an instance of the token.
|
||||
* @param {Array} dependencies - the resolved dependencies, in order
|
||||
* @param {Array} parameters - the bound constructor parameters, in order
|
||||
*/
|
||||
abstract produce(dependencies: any[], parameters: any[]): any
|
||||
|
||||
/**
|
||||
* Should return true if the given identifier matches the token for this factory.
|
||||
* @param something
|
||||
* @return boolean
|
||||
*/
|
||||
abstract match(something: any): boolean
|
||||
|
||||
/**
|
||||
* Get the dependency requirements required by this factory's token.
|
||||
* @return Collection<DependencyRequirement>
|
||||
*/
|
||||
abstract getDependencyKeys(): Collection<DependencyRequirement>
|
||||
|
||||
/**
|
||||
* Get the property dependencies that should be injected to the created instance.
|
||||
* @return Collection<PropertyDependency>
|
||||
*/
|
||||
abstract getInjectedProperties(): Collection<PropertyDependency>
|
||||
}
|
||||
43
src/di/factory/ClosureFactory.ts
Normal file
43
src/di/factory/ClosureFactory.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import {AbstractFactory} from "./AbstractFactory";
|
||||
import {DependencyRequirement, PropertyDependency, StaticClass} from "../types";
|
||||
import {Collection} from "../../util";
|
||||
|
||||
/**
|
||||
* A factory whose token is produced by calling a function.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* let i = 0
|
||||
* const fact = new ClosureFactory('someName', () => {
|
||||
* i += 1
|
||||
* return i * 2
|
||||
* })
|
||||
*
|
||||
* fact.produce([], []) // => 2
|
||||
* fact.produce([], []) // => 4
|
||||
* ```
|
||||
*/
|
||||
export class ClosureFactory extends AbstractFactory {
|
||||
constructor(
|
||||
protected readonly name: string | StaticClass<any, any>,
|
||||
protected readonly token: () => any,
|
||||
) {
|
||||
super(token)
|
||||
}
|
||||
|
||||
produce(dependencies: any[], parameters: any[]): any {
|
||||
return this.token()
|
||||
}
|
||||
|
||||
match(something: any) {
|
||||
return something === this.name
|
||||
}
|
||||
|
||||
getDependencyKeys(): Collection<DependencyRequirement> {
|
||||
return new Collection<DependencyRequirement>()
|
||||
}
|
||||
|
||||
getInjectedProperties(): Collection<PropertyDependency> {
|
||||
return new Collection<PropertyDependency>()
|
||||
}
|
||||
}
|
||||
65
src/di/factory/Factory.ts
Normal file
65
src/di/factory/Factory.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {AbstractFactory} from "./AbstractFactory";
|
||||
import {
|
||||
DEPENDENCY_KEYS_METADATA_KEY,
|
||||
DEPENDENCY_KEYS_PROPERTY_METADATA_KEY,
|
||||
DependencyRequirement,
|
||||
Instantiable,
|
||||
PropertyDependency
|
||||
} from "../types";
|
||||
import {Collection} from "../../util";
|
||||
import 'reflect-metadata'
|
||||
|
||||
/**
|
||||
* Standard static-class factory. The token of this factory is a reference to a
|
||||
* static class that is instantiated when the factory produces.
|
||||
*
|
||||
* Dependency keys are inferred from injection metadata on the constructor's params,
|
||||
* as are the injected properties.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* class A {
|
||||
* constructor(
|
||||
* protected readonly myService: MyService
|
||||
* ) { }
|
||||
* }
|
||||
*
|
||||
* const fact = new Factory(A)
|
||||
*
|
||||
* fact.produce([myServiceInstance], []) // => A { myService: myServiceInstance }
|
||||
* ```
|
||||
*/
|
||||
export class Factory extends AbstractFactory {
|
||||
constructor(
|
||||
protected readonly token: Instantiable<any>
|
||||
) {
|
||||
super(token)
|
||||
}
|
||||
|
||||
produce(dependencies: any[], parameters: any[]): any {
|
||||
return new this.token(...dependencies, ...parameters)
|
||||
}
|
||||
|
||||
match(something: any) {
|
||||
return something === this.token // || (something?.name && something.name === this.token.name)
|
||||
}
|
||||
|
||||
getDependencyKeys(): Collection<DependencyRequirement> {
|
||||
const meta = Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, this.token)
|
||||
if ( meta ) return meta
|
||||
return new Collection<DependencyRequirement>()
|
||||
}
|
||||
|
||||
getInjectedProperties(): Collection<PropertyDependency> {
|
||||
const meta = new Collection<PropertyDependency>()
|
||||
let currentToken = this.token
|
||||
|
||||
do {
|
||||
const loadedMeta = Reflect.getMetadata(DEPENDENCY_KEYS_PROPERTY_METADATA_KEY, currentToken)
|
||||
if ( loadedMeta ) meta.concat(loadedMeta)
|
||||
currentToken = Object.getPrototypeOf(currentToken)
|
||||
} while (Object.getPrototypeOf(currentToken) !== Function.prototype && Object.getPrototypeOf(currentToken) !== Object.prototype)
|
||||
|
||||
return meta
|
||||
}
|
||||
}
|
||||
29
src/di/factory/NamedFactory.ts
Normal file
29
src/di/factory/NamedFactory.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import {Factory} from "./Factory";
|
||||
import {Instantiable} from "../types";
|
||||
|
||||
/**
|
||||
* Container factory that produces an instance of the token, however the token
|
||||
* is identified by a string name rather than a class reference.
|
||||
* @extends Factory
|
||||
*/
|
||||
export default class NamedFactory extends Factory {
|
||||
constructor(
|
||||
/**
|
||||
* The name identifying this factory in the container.
|
||||
* @type {string}
|
||||
*/
|
||||
protected name: string,
|
||||
|
||||
/**
|
||||
* The token to be instantiated.
|
||||
* @type {Instantiable}
|
||||
*/
|
||||
protected token: Instantiable<any>,
|
||||
) {
|
||||
super(token)
|
||||
}
|
||||
|
||||
match(something: any) {
|
||||
return something === this.name
|
||||
}
|
||||
}
|
||||
54
src/di/factory/SingletonFactory.ts
Normal file
54
src/di/factory/SingletonFactory.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Factory } from './Factory'
|
||||
import { Collection } from '../../util'
|
||||
import {DependencyRequirement, PropertyDependency} from "../types";
|
||||
|
||||
/**
|
||||
* Container factory which returns its token as its value, without attempting
|
||||
* to instantiate anything. This is used to register already-produced-singletons
|
||||
* with the container.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* class A {}
|
||||
* const exactlyThisInstanceOfA = new A()
|
||||
*
|
||||
* const fact = new SingletonFactory(A, a)
|
||||
*
|
||||
* fact.produce([], []) // => exactlyThisInstanceOfA
|
||||
* ```
|
||||
*
|
||||
* @extends Factory
|
||||
*/
|
||||
export default class SingletonFactory extends Factory {
|
||||
constructor(
|
||||
/**
|
||||
* Instantiated value of this factory.
|
||||
* @type FunctionConstructor
|
||||
*/
|
||||
protected token: FunctionConstructor,
|
||||
|
||||
/**
|
||||
* String name of this singleton identifying it in the container.
|
||||
* @type string
|
||||
*/
|
||||
protected key: string,
|
||||
) {
|
||||
super(token)
|
||||
}
|
||||
|
||||
produce(dependencies: any[], parameters: any[]) {
|
||||
return this.token
|
||||
}
|
||||
|
||||
match(something: any) {
|
||||
return something === this.key
|
||||
}
|
||||
|
||||
getDependencyKeys(): Collection<DependencyRequirement> {
|
||||
return new Collection<DependencyRequirement>()
|
||||
}
|
||||
|
||||
getInjectedProperties(): Collection<PropertyDependency> {
|
||||
return new Collection<PropertyDependency>()
|
||||
}
|
||||
}
|
||||
14
src/di/index.ts
Normal file
14
src/di/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export * from './error/DuplicateFactoryKeyError'
|
||||
export * from './error/InvalidDependencyKeyError'
|
||||
|
||||
export * from './factory/AbstractFactory'
|
||||
export * from './factory/ClosureFactory'
|
||||
export * from './factory/Factory'
|
||||
export * from './factory/NamedFactory'
|
||||
export * from './factory/SingletonFactory'
|
||||
|
||||
export * from './Container'
|
||||
export * from './ScopedContainer'
|
||||
export * from './types'
|
||||
|
||||
export * from './decorator/injection'
|
||||
71
src/di/types.ts
Normal file
71
src/di/types.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
export const DEPENDENCY_KEYS_METADATA_KEY = 'extollo:di:dependencies:ctor';
|
||||
export const DEPENDENCY_KEYS_PROPERTY_METADATA_KEY = 'extollo:di:dependencies:properties';
|
||||
export const DEPENDENCY_KEYS_SERVICE_TYPE_KEY = 'extollo:di:service_type';
|
||||
|
||||
/**
|
||||
* Interface that designates a particular value as able to be constructed.
|
||||
*/
|
||||
export interface Instantiable<T> {
|
||||
new(...args: any[]): T
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given value is instantiable.
|
||||
* @param what
|
||||
*/
|
||||
export function isInstantiable<T>(what: any): what is Instantiable<T> {
|
||||
return (typeof what === 'object' || typeof what === 'function') && 'constructor' in what && typeof what.constructor === 'function'
|
||||
}
|
||||
|
||||
/**
|
||||
* Type that identifies a value as a static class, even if it is not instantiable.
|
||||
*/
|
||||
export type StaticClass<T, T2> = Function & {prototype: T} & T2
|
||||
|
||||
/**
|
||||
* Returns true if the parameter is a static class.
|
||||
* @param something
|
||||
*/
|
||||
export function isStaticClass<T, T2>(something: any): something is StaticClass<T, T2> {
|
||||
return typeof something === 'function' && typeof something.prototype !== 'undefined'
|
||||
}
|
||||
|
||||
/**
|
||||
* Type used to represent a value that can identify a factory in the container.
|
||||
*/
|
||||
export type DependencyKey = Instantiable<any> | StaticClass<any, any> | string
|
||||
|
||||
/**
|
||||
* Interface used to store dependency requirements by their place in the injectable
|
||||
* target's parameters.
|
||||
*/
|
||||
export interface DependencyRequirement {
|
||||
paramIndex: number,
|
||||
key: DependencyKey,
|
||||
overridden: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface used to store dependency requirements by the class property they should
|
||||
* be injected into.
|
||||
*/
|
||||
export interface PropertyDependency {
|
||||
key: DependencyKey,
|
||||
property: string | symbol,
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface used to keep track of singleton factory values, by their dependency key.
|
||||
*/
|
||||
export interface InstanceRef {
|
||||
key: DependencyKey,
|
||||
value: any,
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface used to keep track of the injection type of a class.
|
||||
*/
|
||||
export interface InjectionType {
|
||||
type: 'named' | 'singleton',
|
||||
name?: string,
|
||||
}
|
||||
Reference in New Issue
Block a user