Configuration.ts
406 lines
| 12.5 KiB
| video/mp2t
|
TypeScriptLexer
|
|
r49 | import { | ||
|
|
r118 | PartialServiceMap, | ||
|
|
r120 | ActivationType, | ||
| ContainerKeys, | ||||
| ContainerResolve | ||||
|
|
r49 | } from "./interfaces"; | ||
|
|
r120 | import { argumentNotEmptyString, isPrimitive, isPromise, delegate, argumentOfType, argumentNotNull, get, primitive } from "../safe"; | ||
|
|
r49 | 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"; | ||||
|
|
r59 | import { makeResolver } from "./ResolverHelper"; | ||
|
|
r120 | import { ICancellation } from "../interfaces"; | ||
|
|
r118 | import { isDescriptor } from "./traits"; | ||
|
|
r120 | export interface RegistrationScope<S extends object> { | ||
|
|
r118 | |||
| /** сервисы, которые регистрируются в контексте активации и таким образом | ||||
| * могут переопределять ранее зарегистрированные сервисы. за это свойство | ||||
| * нужно платить, кроме того порядок активации будет влиять на результат | ||||
| * разрешения зависимостей. | ||||
| */ | ||||
|
|
r120 | services?: RegistrationMap<S>; | ||
|
|
r118 | } | ||
| /** | ||||
| * Базовый интефейс конфигурации сервисов | ||||
| */ | ||||
|
|
r121 | export interface ServiceRegistration<T, S extends object> extends RegistrationScope<S> { | ||
|
|
r118 | |||
| activation?: ActivationType; | ||||
|
|
r121 | params?: any; | ||
|
|
r118 | |||
| inject?: object | object[]; | ||||
| cleanup?: ((instance: T) => void) | string; | ||||
| } | ||||
|
|
r121 | export interface TypeRegistration<C extends new () => any, S extends object> extends ServiceRegistration<InstanceType<C>, S> { | ||
| $type: string | C; | ||||
|
|
r118 | } | ||
|
|
r121 | export interface FactoryRegistration<F extends () => any, S extends object> extends ServiceRegistration<ReturnType<F>, S> { | ||
| $factory: string | F; | ||||
|
|
r118 | } | ||
| export interface ValueRegistration<T> { | ||||
| $value: T; | ||||
| parse?: boolean; | ||||
| } | ||||
|
|
r120 | export interface DependencyRegistration<S extends object, K extends ContainerKeys<S> = ContainerKeys<S>> extends RegistrationScope<S> { | ||
|
|
r118 | $dependency: K; | ||
| lazy?: boolean; | ||||
| optional?: boolean; | ||||
|
|
r120 | default?: ContainerResolve<S, K>; | ||
|
|
r118 | } | ||
|
|
r121 | export interface LazyDependencyRegistration<S extends object, K extends ContainerKeys<S> = ContainerKeys<S>> extends DependencyRegistration<S, K> { | ||
| lazy: true; | ||||
| } | ||||
|
|
r120 | export type Registration<T, S extends object> = T extends primitive ? T : | ||
| ( | ||||
| T | | ||||
| { [k in keyof T]: Registration<T[k], S> } | | ||||
|
|
r121 | TypeRegistration<new () => T, S> | | ||
| FactoryRegistration<() => T, S> | | ||||
|
|
r120 | ValueRegistration<any> | | ||
| DependencyRegistration<S, keyof S> | ||||
| ); | ||||
| export type RegistrationMap<S extends object> = { | ||||
| [k in keyof S]?: Registration<S[k], S>; | ||||
| }; | ||||
|
|
r118 | const _activationTypes: { [k in ActivationType]: number; } = { | ||
| singleton: 1, | ||||
| container: 2, | ||||
| hierarchy: 3, | ||||
| context: 4, | ||||
| call: 5 | ||||
| }; | ||||
|
|
r121 | export function isTypeRegistration(x: any): x is TypeRegistration<new () => any, any> { | ||
|
|
r118 | return (!isPrimitive(x)) && ("$type" in x); | ||
| } | ||||
|
|
r121 | export function isFactoryRegistration(x: any): x is FactoryRegistration<() => any, any> { | ||
|
|
r118 | return (!isPrimitive(x)) && ("$factory" in x); | ||
| } | ||||
| export function isValueRegistration(x: any): x is ValueRegistration<any> { | ||||
| return (!isPrimitive(x)) && ("$value" in x); | ||||
| } | ||||
|
|
r120 | export function isDependencyRegistration<S extends object>(x: any): x is DependencyRegistration<S, keyof S> { | ||
|
|
r118 | return (!isPrimitive(x)) && ("$dependency" in x); | ||
| } | ||||
| export function isActivationType(x: string): x is ActivationType { | ||||
| return typeof x === "string" && x in _activationTypes; | ||||
| } | ||||
|
|
r49 | |||
| const trace = TraceSource.get("@implab/core/di/Configuration"); | ||||
|
|
r115 | async function mapAll(data: any[], map?: (v: any, k: number) => any): Promise<any[]>; | ||
| async function mapAll(data: any, map?: (v: any, k: string) => any): Promise<any>; | ||||
| async function mapAll(data: any, map?: (v: any, k: any) => any): Promise<any> { | ||||
|
|
r49 | 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; | ||||
| } | ||||
| } | ||||
|
|
r59 | export type ModuleResolver = (moduleName: string, ct?: ICancellation) => any; | ||
|
|
r49 | |||
|
|
r120 | export class Configuration<S extends object> { | ||
|
|
r49 | |||
| _hasInnerDescriptors = false; | ||||
|
|
r120 | readonly _container: Container<S>; | ||
|
|
r49 | |||
|
|
r114 | _path: Array<string>; | ||
|
|
r49 | |||
|
|
r113 | _configName: string | undefined; | ||
|
|
r49 | |||
|
|
r113 | _require: ModuleResolver | undefined; | ||
|
|
r49 | |||
|
|
r114 | constructor(container: Container<S>) { | ||
|
|
r76 | argumentNotNull(container, "container"); | ||
|
|
r49 | this._container = container; | ||
| this._path = []; | ||||
| } | ||||
|
|
r59 | async loadConfiguration(moduleName: string, contextRequire?: any, ct = Cancellation.none) { | ||
|
|
r51 | argumentNotEmptyString(moduleName, "moduleName"); | ||
|
|
r59 | |||
|
|
r60 | trace.log( | ||
| "loadConfiguration moduleName={0}, contextRequire={1}", | ||||
| moduleName, | ||||
| contextRequire ? typeof (contextRequire) : "<nil>" | ||||
| ); | ||||
|
|
r59 | |||
| this._configName = moduleName; | ||||
|
|
r113 | const r = await makeResolver(undefined, contextRequire); | ||
|
|
r51 | |||
|
|
r59 | const config = await r(moduleName, ct); | ||
| await this._applyConfiguration( | ||||
| config, | ||||
|
|
r65 | await makeResolver(moduleName, contextRequire), | ||
|
|
r59 | ct | ||
| ); | ||||
|
|
r51 | } | ||
|
|
r120 | async applyConfiguration(data: RegistrationMap<S>, contextRequire?: any, ct = Cancellation.none) { | ||
|
|
r49 | argumentNotNull(data, "data"); | ||
|
|
r65 | await this._applyConfiguration(data, await makeResolver(void (0), contextRequire), ct); | ||
|
|
r59 | } | ||
|
|
r120 | async _applyConfiguration(data: RegistrationMap<S>, resolver?: ModuleResolver, ct = Cancellation.none) { | ||
|
|
r49 | trace.log("applyConfiguration"); | ||
| this._configName = "$"; | ||||
|
|
r50 | if (resolver) | ||
| this._require = resolver; | ||||
|
|
r49 | |||
|
|
r115 | let services: PartialServiceMap<S>; | ||
|
|
r49 | |||
| try { | ||||
| services = await this._visitRegistrations(data, "$"); | ||||
| } catch (e) { | ||||
| throw this._makeError(e); | ||||
| } | ||||
| this._container.register(services); | ||||
| } | ||||
|
|
r113 | _makeError(inner: any) { | ||
|
|
r49 | const e = new ConfigError("Failed to load configuration", inner); | ||
|
|
r113 | e.configName = this._configName || "<inline>"; | ||
|
|
r49 | 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); | ||||
| return localName ? get(localName, m) : m; | ||||
| } catch (e) { | ||||
| trace.error("Failed to resolve type moduleName={0}, localName={1}", moduleName, localName); | ||||
| throw e; | ||||
| } | ||||
| } | ||||
|
|
r59 | _loadModule(moduleName: string) { | ||
|
|
r49 | trace.debug("loadModule {0}", moduleName); | ||
|
|
r113 | if (!this._require) | ||
| throw new Error("Module loader isn't specified"); | ||||
|
|
r49 | |||
|
|
r59 | return this._require(moduleName); | ||
|
|
r49 | } | ||
|
|
r120 | async _visitRegistrations(data: RegistrationMap<S>, name: string) { | ||
|
|
r49 | 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) => { | ||||
|
|
r114 | const d = await this._visit(v, k.toString()); | ||
|
|
r49 | return isDescriptor(d) ? d : new AggregateDescriptor(d); | ||
|
|
r115 | }) as PartialServiceMap<S>; | ||
|
|
r49 | |||
| this._leave(); | ||||
| return services; | ||||
| } | ||||
|
|
r115 | _enter(name: string) { | ||
|
|
r113 | this._path.push(name.toString()); | ||
|
|
r49 | trace.debug(">{0}", name); | ||
| } | ||||
| _leave() { | ||||
| const name = this._path.pop(); | ||||
| trace.debug("<{0}", name); | ||||
| } | ||||
|
|
r118 | async _visit(data: any, name: string): Promise<any> { | ||
|
|
r49 | if (isPrimitive(data) || isDescriptor(data)) | ||
| return data; | ||||
|
|
r114 | if (isDependencyRegistration<S>(data)) { | ||
|
|
r49 | 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); | ||||
| } | ||||
|
|
r118 | return this._visitObject(data, name); | ||
|
|
r49 | } | ||
|
|
r118 | async _visitObject(data: any, name: string) { | ||
|
|
r49 | 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; | ||||
| } | ||||
|
|
r114 | async _visitArray(data: any[], name: string) { | ||
|
|
r49 | 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; | ||||
| } | ||||
|
|
r121 | _makeServiceParams(data: ServiceRegistration<any, S>) { | ||
|
|
r49 | const opts: any = { | ||
| owner: this._container | ||||
| }; | ||||
| if (data.services) | ||||
| opts.services = this._visitRegistrations(data.services, "services"); | ||||
| if (data.inject) { | ||||
|
|
r65 | this._enter("inject"); | ||
|
|
r49 | 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) { | ||||
|
|
r118 | opts.activation = data.activation; | ||
|
|
r49 | } | ||
| if (data.cleanup) | ||||
| opts.cleanup = data.cleanup; | ||||
| return opts; | ||||
| } | ||||
|
|
r114 | async _visitValueRegistration<T>(data: ValueRegistration<T>, name: string) { | ||
|
|
r49 | this._enter(name); | ||
| const d = data.parse ? new AggregateDescriptor(data.$value) : new ValueDescriptor(data.$value); | ||||
| this._leave(); | ||||
| return d; | ||||
| } | ||||
|
|
r114 | async _visitDependencyRegistration<K extends keyof S>(data: DependencyRegistration<S, K>, name: string) { | ||
|
|
r49 | argumentNotEmptyString(data && data.$dependency, "data.$dependency"); | ||
| this._enter(name); | ||||
|
|
r113 | const d = new ReferenceDescriptor<S, K>({ | ||
|
|
r49 | name: data.$dependency, | ||
| lazy: data.lazy, | ||||
| optional: data.optional, | ||||
| default: data.default, | ||||
| services: data.services && await this._visitRegistrations(data.services, "services") | ||||
| }); | ||||
| this._leave(); | ||||
| return d; | ||||
| } | ||||
|
|
r121 | async _visitTypeRegistration(data: TypeRegistration<new () => any, S>, name: string) { | ||
|
|
r49 | 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); | ||||
| opts.type = this._resolveType(moduleName, typeName); | ||||
| } | ||||
|
|
r118 | const d = new TypeServiceDescriptor<S, any, any[]>( | ||
|
|
r49 | await mapAll(opts) | ||
| ); | ||||
| this._leave(); | ||||
| return d; | ||||
| } | ||||
|
|
r121 | async _visitFactoryRegistration(data: FactoryRegistration<() => any, S>, name: string) { | ||
|
|
r65 | argumentOfType(data.$factory, Function, "data.$factory"); | ||
|
|
r49 | this._enter(name); | ||
| const opts = this._makeServiceParams(data); | ||||
|
|
r65 | opts.factory = data.$factory; | ||
|
|
r49 | |||
|
|
r118 | const d = new FactoryServiceDescriptor<S, any, any[]>( | ||
|
|
r49 | await mapAll(opts) | ||
| ); | ||||
| this._leave(); | ||||
| return d; | ||||
| } | ||||
| } | ||||
