2020-06-16 01:35:30 +00:00
|
|
|
import AbstractFactory from './factory/AbstractFactory.ts'
|
|
|
|
import Factory from './factory/Factory.ts'
|
|
|
|
import SingletonFactory from './factory/SingletonFactory.ts'
|
|
|
|
import FunctionFactory from './factory/FunctionFactory.ts'
|
|
|
|
import NamedFactory from './factory/NamedFactory.ts'
|
|
|
|
import { DependencyKey } from './type/DependencyKey.ts'
|
|
|
|
import { collect, Collection } from '../../lib/src/collection/Collection.ts'
|
|
|
|
import Instantiable, {isInstantiable} from './type/Instantiable.ts'
|
|
|
|
|
|
|
|
type MaybeFactory = AbstractFactory | undefined
|
|
|
|
type MaybeDependency = any | undefined
|
|
|
|
type ResolvedDependency = { param_index: number, key: DependencyKey, resolved: any }
|
|
|
|
|
2020-08-16 19:31:47 +00:00
|
|
|
/**
|
|
|
|
* Interface used to keep track of singleton factory values, by their dependency key.
|
|
|
|
*/
|
2020-06-16 01:35:30 +00:00
|
|
|
interface InstanceRef {
|
|
|
|
key: DependencyKey,
|
|
|
|
value: any,
|
|
|
|
}
|
|
|
|
|
2020-08-16 19:31:47 +00:00
|
|
|
/**
|
|
|
|
* Error thrown when a factory is registered with a duplicate dependency key.
|
|
|
|
* @extends Error
|
|
|
|
*/
|
2020-06-16 01:35:30 +00:00
|
|
|
class DuplicateFactoryKeyError extends Error {
|
|
|
|
constructor(key: DependencyKey) {
|
|
|
|
super(`A factory definition already exists with the key for ${key}.`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-16 19:31:47 +00:00
|
|
|
/**
|
|
|
|
* Error thrown when a dependency key that has not been registered is passed to a resolver.
|
|
|
|
* @extends Error
|
|
|
|
*/
|
2020-06-16 01:35:30 +00:00
|
|
|
class InvalidDependencyKeyError extends Error {
|
|
|
|
constructor(key: DependencyKey) {
|
|
|
|
super(`No such dependency is registered with this container: ${key}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-08-16 19:31:47 +00:00
|
|
|
/**
|
|
|
|
* A factory-based inversion-of-control container.
|
|
|
|
*/
|
2020-06-16 01:35:30 +00:00
|
|
|
class Container {
|
2020-08-16 19:31:47 +00:00
|
|
|
/**
|
|
|
|
* Collection of factories registered with this container.
|
|
|
|
* @type Collection<AbstractFactory>
|
|
|
|
*/
|
2020-06-16 01:35:30 +00:00
|
|
|
private factories: Collection<AbstractFactory> = new Collection<AbstractFactory>()
|
2020-08-16 19:31:47 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Collection of singleton instances produced by this container.
|
|
|
|
* @type Collection<InstanceRef>
|
|
|
|
*/
|
2020-06-16 01:35:30 +00:00
|
|
|
private instances: Collection<InstanceRef> = new Collection<InstanceRef>()
|
|
|
|
|
2020-07-06 14:53:03 +00:00
|
|
|
constructor() {
|
|
|
|
this.register(Container)
|
|
|
|
this.instances.push({
|
|
|
|
key: Container,
|
|
|
|
value: this,
|
|
|
|
})
|
|
|
|
|
|
|
|
this.register_singleton('injector', this)
|
|
|
|
}
|
|
|
|
|
2020-08-16 19:31:47 +00:00
|
|
|
/**
|
|
|
|
* Register a basic instantiable class as a standard Factory with this container.
|
|
|
|
* @param {Instantiable} dependency
|
|
|
|
*/
|
2020-06-16 01:35:30 +00:00
|
|
|
register(dependency: Instantiable<any>) {
|
|
|
|
if ( this.resolve(dependency) )
|
|
|
|
throw new DuplicateFactoryKeyError(dependency)
|
|
|
|
|
|
|
|
const factory = new Factory(dependency)
|
|
|
|
this.factories.push(factory)
|
|
|
|
}
|
|
|
|
|
2020-08-16 19:31:47 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2020-06-16 01:35:30 +00:00
|
|
|
register_producer(name: string, producer: () => any) {
|
|
|
|
if ( this.resolve(name) )
|
|
|
|
throw new DuplicateFactoryKeyError(name)
|
|
|
|
|
|
|
|
const factory = new FunctionFactory(name, producer)
|
|
|
|
this.factories.push(factory)
|
|
|
|
}
|
|
|
|
|
2020-08-16 19:31:47 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2020-06-16 01:35:30 +00:00
|
|
|
register_named(name: string, dependency: Instantiable<any>) {
|
|
|
|
if ( this.resolve(name) )
|
|
|
|
throw new DuplicateFactoryKeyError(name)
|
|
|
|
|
|
|
|
const factory = new NamedFactory(name, dependency)
|
|
|
|
this.factories.push(factory)
|
|
|
|
}
|
|
|
|
|
2020-08-16 19:31:47 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2020-06-16 01:35:30 +00:00
|
|
|
register_singleton(key: string, value: any) {
|
|
|
|
if ( this.resolve(key) )
|
|
|
|
throw new DuplicateFactoryKeyError(key)
|
|
|
|
|
|
|
|
this.factories.push(new SingletonFactory(value, key))
|
|
|
|
}
|
|
|
|
|
2020-08-16 19:31:47 +00:00
|
|
|
/**
|
|
|
|
* Register a given factory with the container.
|
|
|
|
* @param {AbstractFactory} factory
|
|
|
|
*/
|
2020-06-16 01:35:30 +00:00
|
|
|
register_factory(factory: AbstractFactory) {
|
|
|
|
if ( !this.factories.includes(factory) )
|
|
|
|
this.factories.push(factory)
|
|
|
|
}
|
|
|
|
|
2020-08-16 19:31:47 +00:00
|
|
|
/**
|
|
|
|
* Returns true if the container has an already-produced value for the given key.
|
|
|
|
* @param {DependencyKey} key
|
|
|
|
*/
|
2020-06-16 01:35:30 +00:00
|
|
|
has_instance(key: DependencyKey): boolean {
|
|
|
|
return this.instances.where('key', '=', key).isNotEmpty()
|
|
|
|
}
|
|
|
|
|
2020-08-16 19:31:47 +00:00
|
|
|
/**
|
|
|
|
* Returns true if the container has a factory for the given key.
|
|
|
|
* @param {DependencyKey} key
|
|
|
|
*/
|
2020-06-16 01:35:30 +00:00
|
|
|
has_key(key: DependencyKey): boolean {
|
|
|
|
return !!this.resolve(key)
|
|
|
|
}
|
|
|
|
|
2020-08-16 19:31:47 +00:00
|
|
|
/**
|
|
|
|
* Get the already-produced value for the given key, if one exists.
|
|
|
|
* @param {DependencyKey} key
|
|
|
|
*/
|
2020-06-16 01:35:30 +00:00
|
|
|
get_existing_instance(key: DependencyKey): MaybeDependency {
|
|
|
|
const instances = this.instances.where('key', '=', key)
|
|
|
|
if ( instances.isNotEmpty() ) return instances.first()
|
|
|
|
}
|
|
|
|
|
2020-08-16 19:31:47 +00:00
|
|
|
/**
|
|
|
|
* Find the factory for the given key, if one is registered with this container.
|
|
|
|
* @param {DependencyKey} key
|
|
|
|
*/
|
2020-06-16 01:35:30 +00:00
|
|
|
resolve(key: DependencyKey): MaybeFactory {
|
|
|
|
const factory = this.factories.firstWhere(item => item.match(key))
|
|
|
|
if ( factory ) return factory
|
|
|
|
}
|
|
|
|
|
2020-08-16 19:31:47 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2020-06-16 01:35:30 +00:00
|
|
|
resolve_and_create(key: DependencyKey, ...parameters: any[]): any {
|
|
|
|
// If we've already instantiated this, just return that
|
|
|
|
const instance = this.get_existing_instance(key)
|
|
|
|
if ( typeof instance !== 'undefined' ) return instance.value
|
|
|
|
|
|
|
|
// Otherwise, attempt to create it
|
|
|
|
const factory = this.resolve(key)
|
|
|
|
if ( !factory )
|
|
|
|
throw new InvalidDependencyKeyError(key)
|
|
|
|
|
|
|
|
// Produce and store a new instance
|
|
|
|
const new_instance = this.produce_factory(factory, parameters)
|
|
|
|
this.instances.push({
|
|
|
|
key,
|
|
|
|
value: new_instance,
|
|
|
|
})
|
|
|
|
|
|
|
|
return new_instance
|
|
|
|
}
|
|
|
|
|
2020-08-16 19:31:47 +00:00
|
|
|
/**
|
|
|
|
* Given a factory and manually-provided parameters, resolve the dependencies for the
|
|
|
|
* factory and produce its value.
|
|
|
|
* @param {AbstractFactory} factory
|
|
|
|
* @param {array} parameters
|
|
|
|
*/
|
2020-06-16 01:35:30 +00:00
|
|
|
protected produce_factory(factory: AbstractFactory, parameters: any[]) {
|
|
|
|
// Create the dependencies for the factory
|
|
|
|
const keys = factory.get_dependency_keys().filter(req => this.has_key(req.key))
|
|
|
|
const dependencies = keys.map<ResolvedDependency>(req => {
|
|
|
|
return {
|
|
|
|
param_index: req.param_index,
|
|
|
|
key: req.key,
|
|
|
|
resolved: this.resolve_and_create(req.key),
|
|
|
|
}
|
|
|
|
}).sortBy('param_index')
|
|
|
|
|
|
|
|
// Build the arguments for the factory, using dependencies in the
|
|
|
|
// correct param_index 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('param_index'); i++ ) {
|
|
|
|
const dep = dependencies.firstWhere('param_index', '=', i)
|
|
|
|
if ( dep ) construction_args.push(dep.resolved)
|
|
|
|
else construction_args.push(params.pop())
|
|
|
|
}
|
|
|
|
|
|
|
|
// Produce a new instance
|
|
|
|
return factory.produce(construction_args, params.reverse().all())
|
|
|
|
}
|
|
|
|
|
2020-08-16 19:31:47 +00:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2020-06-17 14:48:01 +00:00
|
|
|
make(target: DependencyKey, ...parameters: any[]) {
|
2020-07-06 14:53:03 +00:00
|
|
|
if ( this.has_key(target) )
|
2020-06-17 14:48:01 +00:00
|
|
|
return this.resolve_and_create(target, ...parameters)
|
2020-07-24 14:53:30 +00:00
|
|
|
else if ( typeof target !== 'string' && isInstantiable(target) )
|
2020-06-16 01:35:30 +00:00
|
|
|
return this.produce_factory(new Factory(target), parameters)
|
|
|
|
else
|
2020-06-17 14:48:01 +00:00
|
|
|
throw new TypeError(`Invalid or unknown make target: ${target}`)
|
2020-06-16 01:35:30 +00:00
|
|
|
}
|
2020-07-06 14:53:03 +00:00
|
|
|
|
2020-08-16 19:31:47 +00:00
|
|
|
/**
|
|
|
|
* Get a collection of dependency keys required by the given target, if it is registered with this container.
|
|
|
|
* @param {DependencyKey} target
|
|
|
|
*/
|
2020-07-06 14:53:03 +00:00
|
|
|
get_dependencies(target: DependencyKey): Collection<DependencyKey> {
|
|
|
|
const factory = this.resolve(target)
|
|
|
|
|
|
|
|
if ( !factory )
|
|
|
|
throw new InvalidDependencyKeyError(target)
|
|
|
|
|
|
|
|
return factory.get_dependency_keys().pluck('key')
|
|
|
|
}
|
2020-06-16 01:35:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export {
|
|
|
|
Container,
|
|
|
|
InvalidDependencyKeyError,
|
|
|
|
DuplicateFactoryKeyError,
|
|
|
|
MaybeDependency,
|
|
|
|
MaybeFactory,
|
|
|
|
ResolvedDependency,
|
|
|
|
InstanceRef,
|
|
|
|
}
|