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 } interface InstanceRef { key: DependencyKey, value: any, } class DuplicateFactoryKeyError extends Error { constructor(key: DependencyKey) { super(`A factory definition already exists with the key for ${key}.`) } } class InvalidDependencyKeyError extends Error { constructor(key: DependencyKey) { super(`No such dependency is registered with this container: ${key}`) } } class Container { private factories: Collection = new Collection() private instances: Collection = new Collection() register(dependency: Instantiable) { if ( this.resolve(dependency) ) throw new DuplicateFactoryKeyError(dependency) const factory = new Factory(dependency) this.factories.push(factory) } register_producer(name: string, producer: () => any) { if ( this.resolve(name) ) throw new DuplicateFactoryKeyError(name) const factory = new FunctionFactory(name, producer) this.factories.push(factory) } register_named(name: string, dependency: Instantiable) { if ( this.resolve(name) ) throw new DuplicateFactoryKeyError(name) const factory = new NamedFactory(name, dependency) this.factories.push(factory) } register_singleton(key: string, value: any) { if ( this.resolve(key) ) throw new DuplicateFactoryKeyError(key) this.factories.push(new SingletonFactory(value, key)) } register_factory(factory: AbstractFactory) { if ( !this.factories.includes(factory) ) this.factories.push(factory) } has_instance(key: DependencyKey): boolean { return this.instances.where('key', '=', key).isNotEmpty() } has_key(key: DependencyKey): boolean { return !!this.resolve(key) } get_existing_instance(key: DependencyKey): MaybeDependency { const instances = this.instances.where('key', '=', key) if ( instances.isNotEmpty() ) return instances.first() } resolve(key: DependencyKey): MaybeFactory { const factory = this.factories.firstWhere(item => item.match(key)) if ( factory ) return factory } 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 } 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(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()) } make(target: DependencyKey, ...parameters: any[]) { if ( this.has_key(target) ) { return this.resolve_and_create(target, ...parameters) } else if ( typeof target !== 'string' ) return this.produce_factory(new Factory(target), parameters) else throw new TypeError(`Invalid or unknown make target: ${target}`) } } export { Container, InvalidDependencyKeyError, DuplicateFactoryKeyError, MaybeDependency, MaybeFactory, ResolvedDependency, InstanceRef, }