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 = 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 */ protected factories: Collection = new Collection() /** * Collection of singleton instances produced by this container. * @type Collection */ protected instances: Collection = new Collection() constructor() { this.registerSingletonInstance(Container, this) this.registerSingleton('injector', this) } /** * Register a basic instantiable class as a standard Factory with this container. * @param {Instantiable} dependency */ register(dependency: Instantiable) { 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, 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) { 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(staticClass: Instantiable, 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(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(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 { 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() } }