import { PartialServiceMap, ActivationType, ContainerKeys, TypeOfService, ILifetimeManager } from "./interfaces"; import { argumentNotEmptyString, isPrimitive, isPromise, delegate, argumentOfType, argumentNotNull, get, primitive } from "../safe"; import { AggregateDescriptor } from "./AggregateDescriptor"; import { ValueDescriptor } from "./ValueDescriptor"; import { Container } from "./Container"; import { ReferenceDescriptor } from "./ReferenceDescriptor"; import { TypeServiceDescriptor } from "./TypeServiceDescriptor"; import { FactoryServiceDescriptor } from "./FactoryServiceDescriptor"; import { TraceSource } from "../log/TraceSource"; import { ConfigError } from "./ConfigError"; import { Cancellation } from "../Cancellation"; import { makeResolver } from "./ResolverHelper"; import { ICancellation } from "../interfaces"; import { isDescriptor } from "./traits"; import { LazyReferenceDescriptor } from "./LazyReferenceDescriptor"; import { LifetimeManager } from "./LifetimeManager"; export interface RegistrationScope { /** сервисы, которые регистрируются в контексте активации и таким образом * могут переопределять ранее зарегистрированные сервисы. за это свойство * нужно платить, кроме того порядок активации будет влиять на результат * разрешения зависимостей. */ services?: RegistrationMap; } /** * Базовый интерфейс конфигурации сервисов */ export interface ServiceRegistration extends RegistrationScope { activation?: ActivationType; params?: any; /** Специальный идентификатор используется при активации singleton, если * не указан для TypeRegistration вычисляется как oid($type) */ typeId?: string; inject?: object | object[]; cleanup?: ((instance: T) => void) | string; } export interface TypeRegistration any, S extends object> extends ServiceRegistration, S> { $type: string | C; params?: Registration, S>; } export interface StrictTypeRegistration any, S extends object> extends ServiceRegistration, S> { $type: C; params?: Registration, S>; } export interface FactoryRegistration any, S extends object> extends ServiceRegistration, S> { $factory: string | F; } export interface ValueRegistration { $value: T; parse?: boolean; } export interface DependencyRegistration = ContainerKeys> extends RegistrationScope { $dependency: K; lazy?: boolean; optional?: boolean; default?: TypeOfService; } export interface LazyDependencyRegistration = ContainerKeys> extends DependencyRegistration { lazy: true; } export type Registration = T extends primitive ? T : ( T | { [k in keyof T]: Registration } | TypeRegistration T, S> | FactoryRegistration<(...args: any[]) => T, S> | ValueRegistration | DependencyRegistration ); export type RegistrationMap = { [k in keyof S]?: Registration; }; const _activationTypes: { [k in ActivationType]: number; } = { singleton: 1, container: 2, hierarchy: 3, context: 4, call: 5 }; export function isTypeRegistration(x: any): x is TypeRegistration any, any> { return (!isPrimitive(x)) && ("$type" in x); } export function isFactoryRegistration(x: any): x is FactoryRegistration<() => any, any> { return (!isPrimitive(x)) && ("$factory" in x); } export function isValueRegistration(x: any): x is ValueRegistration { return (!isPrimitive(x)) && ("$value" in x); } export function isDependencyRegistration(x: any): x is DependencyRegistration { return (!isPrimitive(x)) && ("$dependency" in x); } export function isActivationType(x: string): x is ActivationType { return typeof x === "string" && x in _activationTypes; } const trace = TraceSource.get("@implab/core/di/Configuration"); async function mapAll(data: any[], map?: (v: any, k: number) => any): Promise; async function mapAll(data: any, map?: (v: any, k: string) => any): Promise; async function mapAll(data: any, map?: (v: any, k: any) => any): Promise { if (data instanceof Array) { return Promise.all(map ? data.map(map) : data); } else { const keys = Object.keys(data); const o: any = {}; await Promise.all(keys.map(async k => { const v = map ? map(data[k], k) : data[k]; o[k] = isPromise(v) ? await v : v; })); return o; } } export type ModuleResolver = (moduleName: string, ct?: ICancellation) => any; export class Configuration { _hasInnerDescriptors = false; readonly _container: Container; _path: Array; _configName: string | undefined; _require: ModuleResolver | undefined; constructor(container: Container) { argumentNotNull(container, "container"); this._container = container; this._path = []; } async loadConfiguration(moduleName: string, contextRequire?: any, ct = Cancellation.none) { argumentNotEmptyString(moduleName, "moduleName"); trace.log( "loadConfiguration moduleName={0}, contextRequire={1}", moduleName, contextRequire ? typeof (contextRequire) : "" ); this._configName = moduleName; const r = await makeResolver(undefined, contextRequire); const config = await r(moduleName, ct); await this._applyConfiguration( config, await makeResolver(moduleName, contextRequire), ct ); } async applyConfiguration(data: RegistrationMap, contextRequire?: any, ct = Cancellation.none) { argumentNotNull(data, "data"); await this._applyConfiguration(data, await makeResolver(void (0), contextRequire), ct); } async _applyConfiguration(data: RegistrationMap, resolver?: ModuleResolver, ct = Cancellation.none) { trace.log("applyConfiguration"); this._configName = "$"; if (resolver) this._require = resolver; let services: PartialServiceMap; try { services = await this._visitRegistrations(data, "$"); } catch (e) { throw this._makeError(e); } this._container.register(services); } _makeError(inner: any) { const e = new ConfigError("Failed to load configuration", inner); e.configName = this._configName || ""; e.path = this._makePath(); return e; } _makePath() { return this._path .reduce( (prev, cur) => typeof cur === "number" ? `${prev}[${cur}]` : `${prev}.${cur}` ) .toString(); } async _resolveType(moduleName: string, localName: string) { trace.log("resolveType moduleName={0}, localName={1}", moduleName, localName); try { const m = await this._loadModule(moduleName); if (localName) { return get(localName, m); } else { if (m instanceof Function) return m; if ("default" in m) return m.default; return m; } } catch (e) { trace.error("Failed to resolve type moduleName={0}, localName={1}", moduleName, localName); throw e; } } _loadModule(moduleName: string) { trace.debug("loadModule {0}", moduleName); if (!this._require) throw new Error("Module loader isn't specified"); return this._require(moduleName); } async _visitRegistrations(data: RegistrationMap, name: string) { this._enter(name); if (data.constructor && data.constructor.prototype !== Object.prototype) throw new Error("Configuration must be a simple object"); const services = await mapAll(data, async (v, k) => { const d = await this._visit(v, k.toString()); return isDescriptor(d) ? d : new AggregateDescriptor(d); }) as PartialServiceMap; this._leave(); return services; } _enter(name: string) { this._path.push(name.toString()); trace.debug(">{0}", name); } _leave() { const name = this._path.pop(); trace.debug("<{0}", name); } async _visit(data: any, name: string): Promise { if (isPrimitive(data) || isDescriptor(data)) return data; if (isDependencyRegistration(data)) { return this._visitDependencyRegistration(data, name); } else if (isValueRegistration(data)) { return this._visitValueRegistration(data, name); } else if (isTypeRegistration(data)) { return this._visitTypeRegistration(data, name); } else if (isFactoryRegistration(data)) { return this._visitFactoryRegistration(data, name); } else if (data instanceof Array) { return this._visitArray(data, name); } return this._visitObject(data, name); } async _visitObject(data: any, name: string) { if (data.constructor && data.constructor.prototype !== Object.prototype) return new ValueDescriptor(data); this._enter(name); const v = await mapAll(data, delegate(this, "_visit")); // TODO: handle inline descriptors properly // const ex = { // activate(ctx) { // const value = ctx.activate(this.prop, "prop"); // // some code // }, // // will be turned to ReferenceDescriptor // prop: { $dependency: "depName" } // }; this._leave(); return v; } async _visitArray(data: any[], name: string) { if (data.constructor && data.constructor.prototype !== Array.prototype) return new ValueDescriptor(data); this._enter(name); const v = await mapAll(data, delegate(this, "_visit")); this._leave(); return v; } _makeServiceParams(data: ServiceRegistration) { const opts: any = { }; if (data.services) opts.services = this._visitRegistrations(data.services, "services"); if (data.inject) { this._enter("inject"); opts.inject = mapAll( data.inject instanceof Array ? data.inject : [data.inject], delegate(this, "_visitObject") ); this._leave(); } if ("params" in data) opts.params = data.params instanceof Array ? this._visitArray(data.params, "params") : this._visit(data.params, "params"); if (data.activation) { opts.activation = this._getLifetimeManager(data.activation, data.typeId); } if (data.cleanup) opts.cleanup = data.cleanup; return opts; } async _visitValueRegistration(data: ValueRegistration, name: string) { this._enter(name); const d = data.parse ? new AggregateDescriptor(data.$value) : new ValueDescriptor(data.$value); this._leave(); return d; } async _visitDependencyRegistration(data: DependencyRegistration, name: string) { argumentNotEmptyString(data && data.$dependency, "data.$dependency"); this._enter(name); const options = { name: data.$dependency, optional: data.optional, default: data.default, services: data.services && await this._visitRegistrations(data.services, "services") }; const d = data.lazy ? new LazyReferenceDescriptor(options) : new ReferenceDescriptor(options); this._leave(); return d; } async _visitTypeRegistration(data: TypeRegistration any, S>, name: string) { argumentNotNull(data.$type, "data.$type"); this._enter(name); const opts = this._makeServiceParams(data); if (data.$type instanceof Function) { opts.type = data.$type; } else { const [moduleName, typeName] = data.$type.split(":", 2); const t = opts.type = this._resolveType(moduleName, typeName); if (!(t instanceof Function)) throw Error("$type (" + data.$type + ") is not a constructable"); } const d = new TypeServiceDescriptor( await mapAll(opts) ); this._leave(); return d; } async _visitFactoryRegistration(data: FactoryRegistration<() => any, S>, name: string) { argumentOfType(data.$factory, Function, "data.$factory"); this._enter(name); const opts = this._makeServiceParams(data); opts.factory = data.$factory; const d = new FactoryServiceDescriptor( await mapAll(opts) ); this._leave(); return d; } _getLifetimeManager(activation: ActivationType, typeId: string | undefined): ILifetimeManager { switch (activation) { case "container": return this._container.getLifetimeManager(); case "hierarchy": return LifetimeManager.hierarchyLifetime; case "context": return LifetimeManager.contextLifetime; case "singleton": if (typeId === undefined) throw Error("The singleton activation requires a typeId"); return LifetimeManager.singletonLifetime(typeId); default: return LifetimeManager.empty; } } }