# HG changeset patch # User cin # Date 2018-12-05 18:12:07 # Node ID eae7e609c38a39c3176c969e529f1b6eea7e66b6 # Parent 6559c5b81a19bdf4df5954243c88ffe45977409a tests diff --git a/license b/license --- a/license +++ b/license @@ -1,4 +1,4 @@ -Copyright 2017-2018 Implab team +Copyright 2017-2019 Implab team Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/package-lock.json b/package-lock.json --- a/package-lock.json +++ b/package-lock.json @@ -83,7 +83,7 @@ }, "duplexer": { "version": "0.1.1", - "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", "dev": true }, @@ -128,7 +128,7 @@ "dependencies": { "tape": { "version": "2.3.3", - "resolved": "http://registry.npmjs.org/tape/-/tape-2.3.3.tgz", + "resolved": "https://registry.npmjs.org/tape/-/tape-2.3.3.tgz", "integrity": "sha1-Lnzgox3wn41oUWZKcYQuDKUFevc=", "dev": true, "requires": { @@ -288,7 +288,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, @@ -300,7 +300,7 @@ }, "readable-stream": { "version": "1.1.14", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "dev": true, "requires": { @@ -353,7 +353,7 @@ }, "string_decoder": { "version": "0.10.31", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true }, @@ -410,13 +410,13 @@ }, "through": { "version": "2.3.8", - "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, "through2": { "version": "0.2.3", - "resolved": "http://registry.npmjs.org/through2/-/through2-0.2.3.tgz", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz", "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=", "dev": true, "requires": { diff --git a/src/ts/di/ActivationContext.ts b/src/ts/di/ActivationContext.ts --- a/src/ts/di/ActivationContext.ts +++ b/src/ts/di/ActivationContext.ts @@ -8,7 +8,7 @@ const trace = TraceSource.get("@implab/c export interface ActivationContextInfo { name: string; - service: Descriptor; + service: string; scope: ServiceMap; } @@ -22,12 +22,17 @@ export class ActivationContext { _visited: object; + _name: string; + + _localized: boolean; + container: Container; - constructor(container: Container, services: ServiceMap, cache?: object, visited?) { + constructor(container: Container, services: ServiceMap, name?: string, cache?: object, visited?) { argumentNotNull(container, "container"); argumentNotNull(services, "services"); + this._name = name; this._visited = visited || {}; this._stack = []; this._cache = cache || {}; @@ -35,7 +40,11 @@ export class ActivationContext { this.container = container; } - getService(name, def?): any { + getName() { + return this._name; + } + + resolve(name, def?): any { const d = this._services[name]; if (!d) @@ -44,7 +53,7 @@ export class ActivationContext { else throw new Error(`Service ${name} not found`); - return isDescriptor(d) ? d.activate(this, name) : d; + return this.activate(d, name); } /** @@ -62,7 +71,8 @@ export class ActivationContext { clone() { return new ActivationContext( this.container, - Object.create(this._services), + this._services, + this._name, this._cache, this._visited ); @@ -80,25 +90,18 @@ export class ActivationContext { return (this._cache[id] = value); } - parse(data, name: string) { - if (isPrimitive(data)) - return data; + activate(d: Descriptor, name: string) { + if (trace.isLogEnabled()) + trace.log(`enter ${name} ${d}`); - if (isDescriptor(data)) { - return data.activate(this, name); - } else if (data instanceof Array) { - this.enter(name); - const v = data.map( (x, i) => this.parse(x, `[${i}]`)); - this.leave(); - return v; - } else { - this.enter(name); - const result = {}; - for (const p in data) - result[p] = this.parse(data[p], "." + p); - this.leave(); - return result; - } + this.enter(name, d.toString()); + const v = d.activate(this); + this.leave(); + + if (trace.isLogEnabled()) + trace.log(`leave ${name}`); + + return v; } visit(id: string) { @@ -111,24 +114,19 @@ export class ActivationContext { return this._stack.slice().reverse(); } - enter(name: string, d?: Descriptor, localize?: boolean) { - if (trace.isLogEnabled()) - trace.log("enter " + name + " " + (d || "") + - (localize ? " localize" : "")); + private enter(name: string, service: string) { this._stack.push({ name, - service: d, + service, scope: this._services }); - if (localize) - this._services = Object.create(this._services); + this._name = name; + this._services = Object.create(this._services); } - leave() { + private leave() { const ctx = this._stack.pop(); this._services = ctx.scope; - - if (trace.isLogEnabled()) - trace.log("leave " + ctx.name + " " + (ctx.service || "")); + this._name = ctx.name; } } diff --git a/src/ts/di/ActivationError.ts b/src/ts/di/ActivationError.ts --- a/src/ts/di/ActivationError.ts +++ b/src/ts/di/ActivationError.ts @@ -27,7 +27,7 @@ export class ActivationError { if (this.activationStack) { parts.push("at"); this.activationStack - .forEach(x => parts.push(` ${x.name} ${x.service ? x.service.toString() : ""}`)); + .forEach(x => parts.push(` ${x.name} ${x.service}`)); } diff --git a/src/ts/di/AggregateDescriptor.ts b/src/ts/di/AggregateDescriptor.ts --- a/src/ts/di/AggregateDescriptor.ts +++ b/src/ts/di/AggregateDescriptor.ts @@ -1,5 +1,6 @@ -import { Descriptor } from "./interfaces"; +import { Descriptor, isDescriptor } from "./interfaces"; import { ActivationContext } from "./ActivationContext"; +import { isPrimitive } from "util"; export class AggregateDescriptor implements Descriptor { _value: object; @@ -8,17 +9,29 @@ export class AggregateDescriptor impleme this._value = value; } - activate(context: ActivationContext, name: string) { - context.enter(name); - const v = context.parse(this._value, ".params"); - context.leave(); - return v; + activate(context: ActivationContext) { + return this._parse(this._value, context, "$value"); } - isInstanceCreated(): boolean { - return false; + // TODO: make async + _parse(value, context: ActivationContext, path: string) { + if (isPrimitive(value)) + return value; + + if (isDescriptor(value)) + return context.activate(value, path); + + if (value instanceof Array) + return value.map((x, i) => this._parse(x, context, `${path}[${i}]`)); + + const t = {}; + for (const p of Object.keys(value)) + t[p] = this._parse(value[p], context, `${path}.${p}`); + return t; + } - getInstance(): any { - throw new Error("Not supported"); + + toString() { + return "@walk"; } } diff --git a/src/ts/di/Container.ts b/src/ts/di/Container.ts --- a/src/ts/di/Container.ts +++ b/src/ts/di/Container.ts @@ -1,13 +1,14 @@ import { ActivationContext } from "./ActivationContext"; import { ValueDescriptor } from "./ValueDescriptor"; import { ActivationError } from "./ActivationError"; -import { isDescriptor, ActivationType, ServiceMap, isDependencyRegistration, isValueRegistration, ServiceRegistration } from "./interfaces"; +import { isDescriptor, ActivationType, ServiceMap, isDependencyRegistration, isValueRegistration, ServiceRegistration, DependencyRegistration } from "./interfaces"; import { AggregateDescriptor } from "./AggregateDescriptor"; -import { isPrimitive } from "../safe"; +import { isPrimitive, pmap } from "../safe"; import { ReferenceDescriptor } from "./ReferenceDescriptor"; import { ServiceDescriptor, ServiceDescriptorParams } from "./ServiceDescriptor"; import { ModuleResolverBase } from "./ModuleResolverBase"; import format = require("../text/format"); +import { throws } from "assert"; export class Container { _services: ServiceMap; @@ -39,35 +40,36 @@ export class Container { return this._parent; } - getService(name: string, def?) { + resolve(name: string, def?) { const d = this._services[name]; - if (!d) + if (d === undefined) { if (arguments.length > 1) return def; else throw new Error("Service '" + name + "' isn't found"); - - if (!isDescriptor(d)) - return d; - - if (d.isInstanceCreated()) - return d.getInstance(); + } const context = new ActivationContext(this, this._services); - try { - return d.activate(context, name); + return context.activate(d, name); } catch (error) { throw new ActivationError(name, context.getStack(), error); } } + getService(name: string, def?) { + return this.resolve.apply(this, arguments); + } + register(nameOrCollection, service?) { if (arguments.length === 1) { const data = nameOrCollection; for (const name in data) this.register(name, data[name]); } else { + if (!isDescriptor(service)) + throw new Error("The service parameter must be a descriptor"); + this._services[nameOrCollection] = service; } return this; @@ -126,19 +128,7 @@ export class Container { async _configure(data: object, opts?: { resolver: ModuleResolverBase }) { const resolver = (opts && opts.resolver) || this._resolver; - const services: ServiceMap = {}; - - resolver.beginBatch(); - - async function parse(k) { - services[k] = await this._parse(data[k], resolver); - } - - const batch = Object.keys(data).map(parse); - - resolver.completeBatch(); - - await Promise.all(batch); + const services = await this._parseRegistrations(data, resolver); this.register(services); } @@ -148,17 +138,8 @@ export class Container { return registration; if (isDependencyRegistration(registration)) { - - return new ReferenceDescriptor({ - name: registration.$dependency, - lazy: registration.lazy, - optional: registration.optional, - default: registration.default, - services: registration.services && this._parseObject(registration.services, resolver) - }); - + return this._paseReference(registration, resolver); } else if (isValueRegistration(registration)) { - return !registration.parse ? new ValueDescriptor(registration.$value) : new AggregateDescriptor(this._parse(registration.$value, resolver)); @@ -172,6 +153,16 @@ export class Container { return this._parseObject(registration, resolver); } + async _paseReference(registration: DependencyRegistration, resolver: ModuleResolverBase) { + return new ReferenceDescriptor({ + name: registration.$dependency, + lazy: registration.lazy, + optional: registration.optional, + default: registration.default, + services: registration.services && await this._parseRegistrations(registration.services, resolver) + }); + } + async _parseService(data: ServiceRegistration, resolver: ModuleResolverBase) { const opts: ServiceDescriptorParams = { owner: this @@ -194,12 +185,14 @@ export class Container { } if (data.services) - opts.services = await this._parseObject(data.services, resolver); + opts.services = await this._parseRegistrations(data.services, resolver); - if (data.inject instanceof Array) - opts.inject = await Promise.all(data.inject.map(x => this._parseObject(x, resolver))); - else - opts.inject = [await this._parseObject(data.inject, resolver)]; + if (data.inject) { + if (data.inject instanceof Array) + opts.inject = await Promise.all(data.inject.map(x => this._parseObject(x, resolver))); + else + opts.inject = [await this._parseObject(data.inject, resolver)]; + } if (data.params) opts.params = this._parse(data.params, resolver); @@ -237,7 +230,7 @@ export class Container { return new ServiceDescriptor(opts); } - _parseObject(data: object, resolver: ModuleResolverBase) { + async _parseObject(data: object, resolver: ModuleResolverBase) { if (data.constructor && data.constructor.prototype !== Object.prototype) return new ValueDescriptor(data); @@ -245,16 +238,42 @@ export class Container { const o = {}; for (const p in data) - o[p] = this._parse(data[p], resolver); + o[p] = await this._parse(data[p], resolver); + + // 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" } + // }; return o; } - _parseArray(data: Array, resolver: ModuleResolverBase) { + async _parseArray(data: Array, resolver: ModuleResolverBase) { if (data.constructor && data.constructor.prototype !== Array.prototype) return new ValueDescriptor(data); - return data.map(x => this._parse(x, resolver)); + return pmap(data, x => this._parse(x, resolver)); + } + + async _parseRegistrations(data: object, resolver: ModuleResolverBase) { + if (data.constructor && + data.constructor.prototype !== Object.prototype) + throw new Error("Registrations must be a simple object"); + + const o: ServiceMap = {}; + + for (const p of Object.keys(data)) { + const v = await this._parse(data[p], resolver); + o[p] = isDescriptor(v) ? v : new AggregateDescriptor(v); + } + + return o; } } diff --git a/src/ts/di/ReferenceDescriptor.ts b/src/ts/di/ReferenceDescriptor.ts --- a/src/ts/di/ReferenceDescriptor.ts +++ b/src/ts/di/ReferenceDescriptor.ts @@ -27,41 +27,37 @@ export class ReferenceDescriptor impleme this._name = opts.name; this._lazy = !!opts.lazy; this._optional = !!opts.optional; - this._default = !!opts.default; + this._default = opts.default; this._services = opts.services; } activate(context: ActivationContext, name: string) { + // добавляем сервисы + if (this._services) { + for (const p of Object.keys(this._services)) + context.register(p, this._services[p]); + } if (this._lazy) { - // сохраняем контекст активации - context = context.clone(); - - // добавляем сервисы - if (this._services) { - for (const p of Object.keys(this._services)) - context.register(p, this._services[p]); - } + const saved = context.clone(); return (cfg: ServiceMap) => { // защищаем контекст на случай исключения в процессе // активации - const ct = context.clone(); + const ct = saved.clone(); try { if (cfg) { for (const k in cfg) ct.register(k, cfg[k]); } - return this._optional ? ct.getService(this._name, this._default) : ct - .getService(this._name); + return this._optional ? ct.resolve(this._name, this._default) : ct + .resolve(this._name); } catch (error) { throw new ActivationError(this._name, ct.getStack(), error); } }; } else { - context.enter(name, this, !!this._services); - // добавляем сервисы if (this._services) { for (const p of Object.keys(this._services)) @@ -69,23 +65,13 @@ export class ReferenceDescriptor impleme } const v = this._optional ? - context.getService(this._name, this._default) : - context.getService(this._name); - - context.leave(); + context.resolve(this._name, this._default) : + context.resolve(this._name); return v; } } - isInstanceCreated() { - return false; - } - - getInstance() { - throw new Error("The reference descriptor doesn't allowed to hold an instance"); - } - toString() { const opts = []; if (this._optional) diff --git a/src/ts/di/ServiceDescriptor.ts b/src/ts/di/ServiceDescriptor.ts --- a/src/ts/di/ServiceDescriptor.ts +++ b/src/ts/di/ServiceDescriptor.ts @@ -1,11 +1,14 @@ import { ActivationContext } from "./ActivationContext"; -import { Descriptor, ActivationType, ServiceMap } from "./interfaces"; +import { Descriptor, ActivationType, ServiceMap, isDescriptor } from "./interfaces"; import { Container } from "./Container"; import { argumentNotNull, isPrimitive, oid, isPromise } from "../safe"; import { Constructor, Factory } from "../interfaces"; +import { TraceSource } from "../log/TraceSource"; let cacheId = 0; +const trace = TraceSource.get("@implab/core/di/ActivationContext"); + function injectMethod(target, method, context, args) { const m = target[method]; if (!m) @@ -29,6 +32,24 @@ function makeClenupCallback(target, meth } } +// TODO: make async +function _parse(value, context: ActivationContext, path: string) { + if (isPrimitive(value)) + return value; + + if (isDescriptor(value)) + return context.activate(value, path); + + if (value instanceof Array) + return value.map((x, i) => this._parse(x, context, `${path}[${i}]`)); + + const t = {}; + for (const p of Object.keys(value)) + t[p] = this._parse(value[p], context, `${path}.${p}`); + return t; + +} + export interface ServiceDescriptorParams { activation?: ActivationType; @@ -119,7 +140,7 @@ export class ServiceDescriptor implement } } - activate(context: ActivationContext, name: string) { + activate(context: ActivationContext) { // if we have a local service records, register them first let instance; @@ -135,7 +156,7 @@ export class ServiceDescriptor implement if (container.has(this._cacheId)) { instance = container.get(this._cacheId); } else { - instance = this._create(context, name); + instance = this._create(context); container.store(this._cacheId, instance); if (this._cleanup) container.onDispose( @@ -152,7 +173,7 @@ export class ServiceDescriptor implement return this._instance; // create an instance - instance = this._create(context, name); + instance = this._create(context); // the instance is bound to the container if (this._cleanup) @@ -168,12 +189,10 @@ export class ServiceDescriptor implement if (context.has(this._cacheId)) return context.get(this._cacheId); // context context activated instances are controlled by callers - return context.store(this._cacheId, this._create( - context, - name)); + return context.store(this._cacheId, this._create(context)); case ActivationType.Call: // CALL // per-call created instances are controlled by callers - return this._create(context, name); + return this._create(context); case ActivationType.Hierarchy: // HIERARCHY // hierarchy activated instances are behave much like container activated // except they are created and bound to the child container @@ -182,7 +201,7 @@ export class ServiceDescriptor implement if (context.container.has(this._cacheId)) return context.container.get(this._cacheId); - instance = this._create(context, name); + instance = this._create(context); if (this._cleanup) context.container.onDispose(makeClenupCallback( @@ -203,8 +222,8 @@ export class ServiceDescriptor implement return this._instance; } - _create(context, name) { - context.enter(name, this, Boolean(this._services)); + _create(context: ActivationContext) { + trace.debug(`constructing ${context._name}`); if (this._activationType !== ActivationType.Call && context.visit(this._cacheId) > 0) @@ -235,13 +254,9 @@ export class ServiceDescriptor implement if (this._params === undefined) { instance = this._factory(); } else if (this._params instanceof Array) { - instance = this._factory.apply(this, context.parse( - this._params, - ".params")); + instance = this._factory.apply(this, _parse(this._params, context, "args")); } else { - instance = this._factory(context.parse( - this._params, - ".params")); + instance = this._factory(_parse(this._params, context, "args")); } if (this._inject) { @@ -251,8 +266,6 @@ export class ServiceDescriptor implement }); } - context.leave(); - return instance; } diff --git a/src/ts/di/ValueDescriptor.ts b/src/ts/di/ValueDescriptor.ts --- a/src/ts/di/ValueDescriptor.ts +++ b/src/ts/di/ValueDescriptor.ts @@ -1,5 +1,4 @@ import { Descriptor } from "./interfaces"; -import { ActivationContext } from "./ActivationContext"; export class ValueDescriptor implements Descriptor { _value; @@ -8,16 +7,11 @@ export class ValueDescriptor implements this._value = value; } - activate(context: ActivationContext, name: string) { - context.enter(name); - const v = this._value; - context.leave(); - return v; - } - isInstanceCreated(): boolean { - return true; - } - getInstance() { + activate() { return this._value; } + + toString() { + return `@type=${typeof this._value}`; + } } diff --git a/src/ts/di/interfaces.ts b/src/ts/di/interfaces.ts --- a/src/ts/di/interfaces.ts +++ b/src/ts/di/interfaces.ts @@ -4,8 +4,6 @@ import { Constructor, Factory } from ".. export interface Descriptor { activate(context: ActivationContext, name?: string); - isInstanceCreated(): boolean; - getInstance(); } export function isDescriptor(x): x is Descriptor { @@ -14,7 +12,7 @@ export function isDescriptor(x): x is De } export interface ServiceMap { - [s: string]: any; + [s: string]: Descriptor; } export enum ActivationType { diff --git a/src/ts/safe.ts b/src/ts/safe.ts --- a/src/ts/safe.ts +++ b/src/ts/safe.ts @@ -1,5 +1,5 @@ let _nextOid = 0; -const _oid = Symbol("__oid"); +const _oid = typeof Symbol === "function" ? Symbol("__oid") : "__oid"; export function oid(instance: object): string { if (isNull(instance)) @@ -232,7 +232,7 @@ export function delegate pmap(data, cb)); if (isNull(items) || !items.length) diff --git a/test/ts/ContainerTests.ts b/test/ts/ContainerTests.ts --- a/test/ts/ContainerTests.ts +++ b/test/ts/ContainerTests.ts @@ -1,14 +1,26 @@ -import { test } from "./TestTraits"; +import { test, TapeWriter } from "./TestTraits"; import { Container } from "@implab/core/di/Container"; import { ReferenceDescriptor } from "@implab/core/di/ReferenceDescriptor"; import { AggregateDescriptor } from "@implab/core/di/AggregateDescriptor"; +import { ValueDescriptor } from "@implab/core/di/ValueDescriptor"; +import { TraceSource, DebugLevel } from "@implab/core/log/TraceSource"; +import { Foo } from "./mock/Foo"; +import { Bar } from "./mock/Bar"; +import { isNull } from "@implab/core/safe"; -test("Container register/getService tests", async t => { +test("Container register/resolve tests", async t => { + const writer = new TapeWriter(t); + + TraceSource.on(ts => { + ts.level = DebugLevel; + writer.writeEvents(ts.events); + }); + const container = new Container(); const connection1 = "db://localhost"; - container.register("connection", connection1); + container.register("connection", new ValueDescriptor(connection1)); t.equals(container.getService("connection"), connection1); @@ -16,11 +28,47 @@ test("Container register/getService test "dbParams", new AggregateDescriptor({ timeout: 10, - connection: new ReferenceDescriptor({name: "connection"}) + connection: new ReferenceDescriptor({ name: "connection" }) }) ); const dbParams = container.getService("dbParams"); - t.equals(dbParams.connection, connection1); + t.equals(dbParams.connection, connection1, "should get connection"); + + writer.destroy(); +}); + +test("Container configure/resolve tests", async t => { + const writer = new TapeWriter(t); + + TraceSource.on(ts => { + ts.level = DebugLevel; + writer.writeEvents(ts.events); + }); + + const container = new Container(); + + await container.configure({ + foo: { + $type: Foo + }, + bar: { + $type: Bar, + params: { + db: { + provider: { + $dependency: "db" + } + } + } + } + }); + + const f1 = container.resolve("foo"); + t.assert(!isNull(f1), "foo should be not null"); + + const b1 = container.resolve("bar"); + + writer.destroy(); }); diff --git a/test/ts/TestTraits.ts b/test/ts/TestTraits.ts --- a/test/ts/TestTraits.ts +++ b/test/ts/TestTraits.ts @@ -66,8 +66,12 @@ export function test(name: string, cb: ( try { await cb(t); } catch (e) { + + // verbose error information + // tslint:disable-next-line console.error(e); t.fail(e); + } finally { t.end(); } diff --git a/test/ts/mock/Bar.ts b/test/ts/mock/Bar.ts new file mode 100644 --- /dev/null +++ b/test/ts/mock/Bar.ts @@ -0,0 +1,3 @@ +export class Bar { + name = "bar"; +} diff --git a/test/ts/mock/Foo.ts b/test/ts/mock/Foo.ts new file mode 100644 --- /dev/null +++ b/test/ts/mock/Foo.ts @@ -0,0 +1,3 @@ +export class Foo { + name = "foo"; +} diff --git a/tsconfig.json b/tsconfig.json --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,9 @@ "outDir" : "build/dist", "declaration": true, "lib": [ - "es2015" + "es5", + "es2015.promise", + "es2015.symbol" ] }, "include" : [