Start JSDocs
This commit is contained in:
@@ -11,17 +11,28 @@ type MaybeFactory = AbstractFactory | undefined
|
||||
type MaybeDependency = any | undefined
|
||||
type ResolvedDependency = { param_index: number, key: DependencyKey, resolved: any }
|
||||
|
||||
/**
|
||||
* Interface used to keep track of singleton factory values, by their dependency key.
|
||||
*/
|
||||
interface InstanceRef {
|
||||
key: DependencyKey,
|
||||
value: any,
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when a factory is registered with a duplicate dependency key.
|
||||
* @extends Error
|
||||
*/
|
||||
class DuplicateFactoryKeyError extends Error {
|
||||
constructor(key: DependencyKey) {
|
||||
super(`A factory definition already exists with the key for ${key}.`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when a dependency key that has not been registered is passed to a resolver.
|
||||
* @extends Error
|
||||
*/
|
||||
class InvalidDependencyKeyError extends Error {
|
||||
constructor(key: DependencyKey) {
|
||||
super(`No such dependency is registered with this container: ${key}`)
|
||||
@@ -29,8 +40,20 @@ class InvalidDependencyKeyError extends Error {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory-based inversion-of-control container.
|
||||
*/
|
||||
class Container {
|
||||
/**
|
||||
* Collection of factories registered with this container.
|
||||
* @type Collection<AbstractFactory>
|
||||
*/
|
||||
private factories: Collection<AbstractFactory> = new Collection<AbstractFactory>()
|
||||
|
||||
/**
|
||||
* Collection of singleton instances produced by this container.
|
||||
* @type Collection<InstanceRef>
|
||||
*/
|
||||
private instances: Collection<InstanceRef> = new Collection<InstanceRef>()
|
||||
|
||||
constructor() {
|
||||
@@ -43,6 +66,10 @@ class Container {
|
||||
this.register_singleton('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)
|
||||
@@ -51,6 +78,11 @@ class Container {
|
||||
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
|
||||
*/
|
||||
register_producer(name: string, producer: () => any) {
|
||||
if ( this.resolve(name) )
|
||||
throw new DuplicateFactoryKeyError(name)
|
||||
@@ -59,6 +91,12 @@ class Container {
|
||||
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
|
||||
*/
|
||||
register_named(name: string, dependency: Instantiable<any>) {
|
||||
if ( this.resolve(name) )
|
||||
throw new DuplicateFactoryKeyError(name)
|
||||
@@ -67,6 +105,12 @@ class Container {
|
||||
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
|
||||
*/
|
||||
register_singleton(key: string, value: any) {
|
||||
if ( this.resolve(key) )
|
||||
throw new DuplicateFactoryKeyError(key)
|
||||
@@ -74,29 +118,55 @@ class Container {
|
||||
this.factories.push(new SingletonFactory(value, key))
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a given factory with the container.
|
||||
* @param {AbstractFactory} factory
|
||||
*/
|
||||
register_factory(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
|
||||
*/
|
||||
has_instance(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
|
||||
*/
|
||||
has_key(key: DependencyKey): boolean {
|
||||
return !!this.resolve(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the already-produced value for the given key, if one exists.
|
||||
* @param {DependencyKey} key
|
||||
*/
|
||||
get_existing_instance(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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
resolve_and_create(key: DependencyKey, ...parameters: any[]): any {
|
||||
// If we've already instantiated this, just return that
|
||||
const instance = this.get_existing_instance(key)
|
||||
@@ -117,6 +187,12 @@ class Container {
|
||||
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 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))
|
||||
@@ -143,6 +219,15 @@ class Container {
|
||||
return factory.produce(construction_args, params.reverse().all())
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(target: DependencyKey, ...parameters: any[]) {
|
||||
if ( this.has_key(target) )
|
||||
return this.resolve_and_create(target, ...parameters)
|
||||
@@ -152,6 +237,10 @@ class Container {
|
||||
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
|
||||
*/
|
||||
get_dependencies(target: DependencyKey): Collection<DependencyKey> {
|
||||
const factory = this.resolve(target)
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@ import {DEPENDENCY_KEYS_METADATA_KEY, DependencyKey} from '../type/DependencyKey
|
||||
import {collect, Collection} from '../../../lib/src/collection/Collection.ts'
|
||||
import {DependencyRequirement} from '../type/DependencyRequirement.ts'
|
||||
|
||||
/**
|
||||
* Get a collection of dependency requirements for the given target object.
|
||||
* @param {Object} target
|
||||
* @return Collection<DependencyRequirement>
|
||||
*/
|
||||
const initDependencyMetadata = (target: Object): Collection<DependencyRequirement> => {
|
||||
const param_types = Reflect.getMetadata('design:paramtypes', target)
|
||||
return collect<DependencyKey>(param_types).map<DependencyRequirement>((type, index) => {
|
||||
@@ -14,6 +19,11 @@ const initDependencyMetadata = (target: Object): Collection<DependencyRequiremen
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
const Injectable = (): ClassDecorator => {
|
||||
return (target) => {
|
||||
const meta = initDependencyMetadata(target)
|
||||
@@ -47,6 +57,12 @@ const Injectable = (): ClassDecorator => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
const Inject = (key: DependencyKey): ParameterDecorator => {
|
||||
return (target, property, param_index) => {
|
||||
if ( !Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, target) ) {
|
||||
|
||||
@@ -4,6 +4,13 @@ import { Injectable } from './Injection.ts'
|
||||
|
||||
const injectable = Injectable()
|
||||
|
||||
/**
|
||||
* Class decorator that marks a class as injectable and singleton in the dependency container.
|
||||
* When this is applied, the Injectable decorator is automatically applied, and the class is
|
||||
* marked so that, when it is injected into classes, a singleton instance is used.
|
||||
* @param {string} [name] - optionally, refer to the dependency by a name, rather than by class type
|
||||
* @constructor
|
||||
*/
|
||||
const Service = (name?: string): ClassDecorator => {
|
||||
return (target) => {
|
||||
if ( isInstantiable(target) ) {
|
||||
|
||||
@@ -1,12 +1,38 @@
|
||||
import {Collection} from '../../../lib/src/collection/Collection.ts'
|
||||
import {DependencyRequirement} from '../type/DependencyRequirement.ts'
|
||||
|
||||
/**
|
||||
* Abstract base class for dependency container factories.
|
||||
* @abstract
|
||||
*/
|
||||
export default 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 get_dependency_keys(): Collection<DependencyRequirement>
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ import { Collection } from '../../../lib/src/collection/Collection.ts'
|
||||
import { DependencyRequirement } from '../type/DependencyRequirement.ts'
|
||||
import AbstractFactory from './AbstractFactory.ts'
|
||||
|
||||
/**
|
||||
* Standard factory that produces injected versions of instantiable classes.
|
||||
* @extends AbstractFactory
|
||||
*/
|
||||
export default class Factory extends AbstractFactory {
|
||||
constructor(
|
||||
protected token: Instantiable<any>
|
||||
|
||||
@@ -2,9 +2,22 @@ import AbstractFactory from './AbstractFactory.ts'
|
||||
import {DependencyRequirement} from '../type/DependencyRequirement.ts'
|
||||
import {Collection} from '../../../lib/src/collection/Collection.ts'
|
||||
|
||||
/**
|
||||
* Container factory that produces an item by calling the token as a function.
|
||||
* @extends AbstractFactory
|
||||
*/
|
||||
export default class FunctionFactory extends AbstractFactory {
|
||||
constructor(
|
||||
/**
|
||||
* The name identifying this factory in the container.
|
||||
* @type {string}
|
||||
*/
|
||||
protected name: string,
|
||||
|
||||
/**
|
||||
* The token, which is a function that returns the value of this factory.
|
||||
* @type {function}
|
||||
*/
|
||||
protected token: () => any,
|
||||
) {
|
||||
super(token)
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
import Factory from './Factory.ts'
|
||||
import Instantiable from "../type/Instantiable.ts";
|
||||
import Instantiable from '../type/Instantiable.ts'
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
@@ -2,9 +2,24 @@ import Factory from './Factory.ts'
|
||||
import {Collection} from '../../../lib/src/collection/Collection.ts'
|
||||
import {DependencyRequirement} from '../type/DependencyRequirement.ts'
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @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)
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import Instantiable from './Instantiable.ts'
|
||||
import {StaticClass} from './StaticClass.ts'
|
||||
const DEPENDENCY_KEYS_METADATA_KEY = 'daton:di:dependencyKeys.ts'
|
||||
|
||||
/**
|
||||
* Type used to represent a value that can identify a factory in the container.
|
||||
*/
|
||||
type DependencyKey = Instantiable<any> | StaticClass<any, any> | string
|
||||
|
||||
export { DependencyKey, DEPENDENCY_KEYS_METADATA_KEY }
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { DependencyKey } from './DependencyKey.ts'
|
||||
|
||||
/**
|
||||
* Interface used to store dependency requirements by their place in the injectable
|
||||
* target's constructor parameters.
|
||||
*/
|
||||
interface DependencyRequirement {
|
||||
param_index: number,
|
||||
key: DependencyKey,
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
/**
|
||||
* Interface that designates a particular value as able to be constructed.
|
||||
*/
|
||||
export default interface Instantiable<T> {
|
||||
new(...args: any[]): T
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given value is instantiable.
|
||||
* @param what
|
||||
*/
|
||||
const isInstantiable = <T>(what: any): what is Instantiable<T> => {
|
||||
return (typeof what === 'object' || typeof what === 'function') && 'constructor' in what && typeof what.constructor === 'function'
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
/**
|
||||
* 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'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user