diff --git a/.project b/.project new file mode 100644 --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + core + Project core created by Buildship. + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs new file mode 100644 --- /dev/null +++ b/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir= +eclipse.preferences.version=1 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "java.configuration.updateBuildConfiguration": "disabled", + "tslint.enable": true +} \ No newline at end of file diff --git a/build.gradle b/build.gradle --- a/build.gradle +++ b/build.gradle @@ -40,7 +40,7 @@ task _buildTs(dependsOn: _npmInstall, ty inputs.file('tsc.json') outputs.dir(distDir) - commandLine 'node_modules/.bin/tsc', '-p', 'tsc.json' + commandLine 'node_modules/.bin/tsc', '-p', 'tsconfig.json' } task _packageMeta(type: Copy) { @@ -80,7 +80,7 @@ task buildTests(dependsOn: _localInstall inputs.file('tsc.test.json') outputs.dir(testDir) - commandLine 'node_modules/.bin/tsc', '-p', 'tsc.test.json' + commandLine 'node_modules/.bin/tsc', '-p', 'tsconfig.test.json' } task test(dependsOn: [copyJsTests, buildTests], type: Exec) { diff --git a/package-lock.json b/package-lock.json --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "integrity": "sha512-xil0KO5wkPoixdBWGIGolPv9dekf6dVkjjJLAFYchfKcd4DICou67rgGCIO7wAh3i5Ff/6j9IDgZz+GU9cMaqQ==", "dev": true, "requires": { - "@types/node": "10.5.1" + "@types/node": "*" } }, "balanced-match": { @@ -31,7 +31,7 @@ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "requires": { - "balanced-match": "1.0.0", + "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, @@ -59,8 +59,8 @@ "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", "dev": true, "requires": { - "foreach": "2.0.5", - "object-keys": "1.0.12" + "foreach": "^2.0.5", + "object-keys": "^1.0.8" } }, "defined": { @@ -86,11 +86,11 @@ "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", "dev": true, "requires": { - "es-to-primitive": "1.1.1", - "function-bind": "1.1.1", - "has": "1.0.3", - "is-callable": "1.1.3", - "is-regex": "1.0.4" + "es-to-primitive": "^1.1.1", + "function-bind": "^1.1.1", + "has": "^1.0.1", + "is-callable": "^1.1.3", + "is-regex": "^1.0.4" } }, "es-to-primitive": { @@ -99,9 +99,9 @@ "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", "dev": true, "requires": { - "is-callable": "1.1.3", - "is-date-object": "1.0.1", - "is-symbol": "1.0.1" + "is-callable": "^1.1.1", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.1" } }, "faucet": { @@ -111,12 +111,12 @@ "dev": true, "requires": { "defined": "0.0.0", - "duplexer": "0.1.1", + "duplexer": "~0.1.1", "minimist": "0.0.5", - "sprintf": "0.1.5", - "tap-parser": "0.4.3", - "tape": "2.3.3", - "through2": "0.2.3" + "sprintf": "~0.1.3", + "tap-parser": "~0.4.0", + "tape": "~2.3.2", + "through2": "~0.2.3" }, "dependencies": { "deep-equal": { @@ -143,12 +143,12 @@ "integrity": "sha1-Lnzgox3wn41oUWZKcYQuDKUFevc=", "dev": true, "requires": { - "deep-equal": "0.1.2", - "defined": "0.0.0", - "inherits": "2.0.3", - "jsonify": "0.0.0", - "resumer": "0.0.0", - "through": "2.3.8" + "deep-equal": "~0.1.0", + "defined": "~0.0.0", + "inherits": "~2.0.1", + "jsonify": "~0.0.0", + "resumer": "~0.0.0", + "through": "~2.3.4" } } } @@ -159,7 +159,7 @@ "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", "dev": true, "requires": { - "is-callable": "1.1.3" + "is-callable": "^1.1.3" } }, "foreach": { @@ -186,12 +186,12 @@ "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "dev": true, "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, "has": { @@ -200,7 +200,7 @@ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "requires": { - "function-bind": "1.1.1" + "function-bind": "^1.1.1" } }, "inflight": { @@ -209,8 +209,8 @@ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "once": "^1.3.0", + "wrappy": "1" } }, "inherits": { @@ -237,7 +237,7 @@ "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", "dev": true, "requires": { - "has": "1.0.3" + "has": "^1.0.1" } }, "is-symbol": { @@ -264,7 +264,7 @@ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { - "brace-expansion": "1.1.11" + "brace-expansion": "^1.1.7" } }, "minimist": { @@ -291,7 +291,7 @@ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "requires": { - "wrappy": "1.0.2" + "wrappy": "1" } }, "path-is-absolute": { @@ -312,10 +312,10 @@ "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", "isarray": "0.0.1", - "string_decoder": "0.10.31" + "string_decoder": "~0.10.x" } }, "requirejs": { @@ -330,7 +330,7 @@ "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", "dev": true, "requires": { - "path-parse": "1.0.5" + "path-parse": "^1.0.5" } }, "resumer": { @@ -339,7 +339,7 @@ "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", "dev": true, "requires": { - "through": "2.3.8" + "through": "~2.3.4" } }, "sprintf": { @@ -354,9 +354,9 @@ "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=", "dev": true, "requires": { - "define-properties": "1.1.2", - "es-abstract": "1.12.0", - "function-bind": "1.1.1" + "define-properties": "^1.1.2", + "es-abstract": "^1.5.0", + "function-bind": "^1.0.2" } }, "string_decoder": { @@ -371,8 +371,8 @@ "integrity": "sha1-pOrhkMENdsehEZIf84u+TVjwnuo=", "dev": true, "requires": { - "inherits": "2.0.3", - "readable-stream": "1.1.14" + "inherits": "~2.0.1", + "readable-stream": "~1.1.11" } }, "tape": { @@ -381,19 +381,19 @@ "integrity": "sha512-6fKIXknLpoe/Jp4rzHKFPpJUHDHDqn8jus99IfPnHIjyz78HYlefTGD3b5EkbQzuLfaEvmfPK3IolLgq2xT3kw==", "dev": true, "requires": { - "deep-equal": "1.0.1", - "defined": "1.0.0", - "for-each": "0.3.3", - "function-bind": "1.1.1", - "glob": "7.1.2", - "has": "1.0.3", - "inherits": "2.0.3", - "minimist": "1.2.0", - "object-inspect": "1.6.0", - "resolve": "1.7.1", - "resumer": "0.0.0", - "string.prototype.trim": "1.1.2", - "through": "2.3.8" + "deep-equal": "~1.0.1", + "defined": "~1.0.0", + "for-each": "~0.3.3", + "function-bind": "~1.1.1", + "glob": "~7.1.2", + "has": "~1.0.3", + "inherits": "~2.0.3", + "minimist": "~1.2.0", + "object-inspect": "~1.6.0", + "resolve": "~1.7.1", + "resumer": "~0.0.0", + "string.prototype.trim": "~1.1.2", + "through": "~2.3.8" } }, "through": { @@ -408,8 +408,8 @@ "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=", "dev": true, "requires": { - "readable-stream": "1.1.14", - "xtend": "2.1.2" + "readable-stream": "~1.1.9", + "xtend": "~2.1.1" } }, "typescript": { @@ -430,7 +430,7 @@ "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", "dev": true, "requires": { - "object-keys": "0.4.0" + "object-keys": "~0.4.0" }, "dependencies": { "object-keys": { diff --git a/src/js/Deferred.js b/src/js/Deferred.js deleted file mode 100644 --- a/src/js/Deferred.js +++ /dev/null @@ -1,3 +0,0 @@ -define(["dojo/Deferred"], function(Deferred) { - return Deferred; -}); \ No newline at end of file diff --git a/src/js/declare.js b/src/js/declare.js deleted file mode 100644 --- a/src/js/declare.js +++ /dev/null @@ -1,6 +0,0 @@ -define([ - './declare/_load!' -], function(declare) { - 'use strict'; - return declare; -}); \ No newline at end of file diff --git a/src/ts/Uuid.ts b/src/ts/Uuid.ts --- a/src/ts/Uuid.ts +++ b/src/ts/Uuid.ts @@ -8,7 +8,7 @@ declare var window: any; -let _window : any = 'undefined' !== typeof window ? window : null; +let _window: any = 'undefined' !== typeof window ? window : null; // Unique ID creation requires a high quality random # generator. We // feature @@ -41,7 +41,7 @@ function setupBrowser() { // If all else fails, use Math.random(). It's fast, but is of // unspecified // quality. - let _rnds = new Array(16); + let _rnds = new Array(16); _rng = function () { for (var i = 0, r; i < 16; i++) { if ((i & 0x03) === 0) { @@ -92,8 +92,8 @@ for (let i = 0; i < 256; i++) { } // **`parse()` - Parse a UUID into it's component bytes** -function parse(s, buf?, offset?) : Array { - let i = (buf && offset) || 0, ii = 0; +function _parse(s, buf?, offset?): Array { + let i = (buf && offset) || 0, ii = 0; buf = buf || []; s.toLowerCase().replace(/[0-9a-f]{2}/g, function (oct) { @@ -111,8 +111,8 @@ function parse(s, buf?, offset?) : Array } // **`unparse()` - Convert UUID byte array (ala parse()) into a string** -function unparse(buf, offset?) : string { - let i = offset || 0, bth = _byteToHex; +function _unparse(buf, offset?): string { + let i = offset || 0, bth = _byteToHex; return bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + bth[buf[i++]] + '-' + bth[buf[i++]] + @@ -143,10 +143,10 @@ let _nodeId = [ let _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff; // Previous uuid creation time -let _lastMSecs = 0, _lastNSecs = 0; +let _lastMSecs = 0, _lastNSecs = 0; // See https://github.com/broofa/node-uuid for API details -function v1(options?, buf?, offset?) : string { +function _v1(options?, buf?, offset?): string { let i = buf && offset || 0; let b = buf || []; @@ -225,13 +225,13 @@ function v1(options?, buf?, offset?) : s b[i + n] = node[n]; } - return buf ? buf : unparse(b); + return buf ? buf : _unparse(b); } // **`v4()` - Generate random UUID** // See https://github.com/broofa/node-uuid for API details -function v4(options?, buf?, offset?) : string { +function _v4(options?, buf?, offset?): string { // Deprecated - 'format' argument, as supported in v1.2 let i = buf && offset || 0; @@ -254,29 +254,17 @@ function v4(options?, buf?, offset?) : s } } - return buf || unparse(rnds); + return buf || _unparse(rnds); +} + +export function Uuid() { + } -// Export public API -const empty = "00000000-0000-0000-0000-000000000000"; - -interface uuid { - (options?, buf?, offset?) : string; - v1(options?, buf?, offset?) : string; - v4(options?, buf?, offset?) : string; - readonly empty: string; - parse(s, buf?, offset?) : Array; - unparse(buf, offset?) : string; -} - -export = (() =>{ - var f : any = function(options?, buf?, offset?) : string { - return v4(options, buf, offset); - }; - f.v1 = v1; - f.v4 = v4; - f.empty = empty; - f.parse = parse; - f.unparse = unparse; - return f; -})(); \ No newline at end of file +export namespace Uuid { + export const v4 = _v4; + export const v1 = _v1; + export const empty = "00000000-0000-0000-0000-000000000000"; + export const parse = _parse; + export const unparse = _unparse; +} \ No newline at end of file diff --git a/src/ts/di.ts b/src/ts/di.ts new file mode 100644 --- /dev/null +++ b/src/ts/di.ts @@ -0,0 +1,126 @@ +import { TraceSource } from "./log/TraceSource"; +import { argumentNotNull, argumentNotEmptyString, isPrimitive, each, isNull } from "./safe"; +import { Uuid } from './Uuid'; +import {ActivationContext} from "./di/ActivationContext"; + +let trace = TraceSource.get("di"); + +export interface Descriptor { + activate(context: ActivationContext, name: string): any +} + +export function isDescriptor(value: any): value is Descriptor { + return ("activate" in value); +} + +export interface ServiceMap { + [s: string] : Descriptor +} + +export class ActivationContextInfo { + name: string + + service: string + + scope: ServiceMap +} + +export class ActivationError { + activationStack: ActivationContextInfo[] + + service: string + + innerException: any + + message: string + + constructor(service: string, activationStack: ActivationContextInfo[], innerException) { + this.message = "Failed to activate the service"; + this.activationStack = activationStack; + this.service = service; + this.innerException = innerException; + } + + toString() { + var parts = [this.message]; + if (this.service) + parts.push("when activating: " + this.service.toString()); + + if (this.innerException) + parts.push("caused by: " + this.innerException.toString()); + + if (this.activationStack) { + parts.push("at"); + this.activationStack.forEach(function (x) { + parts.push(" " + x.name + " " + + (x.service ? x.service.toString() : "")); + }); + } + + return parts.join("\n"); + } +} + + +export enum ActivationType { + SINGLETON, + CONTAINER, + HIERARCHY, + CONTEXT, + CALL +} + + + +interface ServiceDescriptorParams { + +} + +class ServiceDescriptor extends Descriptor { + constructor(opts: ServiceDescriptorParams) { + super(); + } + + activate(context: ActivationContext, name: string) { + throw new Error("Method not implemented."); + } + isInstanceCreated(): boolean { + throw new Error("Method not implemented."); + } + getInstance() { + throw new Error("Method not implemented."); + } +} + + + +class AggregateDescriptor extends Descriptor { + constructor(value: T) { + super(); + } + + activate(context: ActivationContext, name: string) { + throw new Error("Method not implemented."); + } + isInstanceCreated(): boolean { + throw new Error("Method not implemented."); + } + getInstance(): T { + throw new Error("Method not implemented."); + } +} + +class ValueDescriptor implements Descriptor { + activate(context: ActivationContext, name: string) { + throw new Error("Method not implemented."); + } + isInstanceCreated(): boolean { + throw new Error("Method not implemented."); + } + getInstance(): T { + throw new Error("Method not implemented."); + } + constructor(value: T) { + + } +} \ No newline at end of file diff --git a/src/ts/di/ActivationContext.ts b/src/ts/di/ActivationContext.ts new file mode 100644 --- /dev/null +++ b/src/ts/di/ActivationContext.ts @@ -0,0 +1,132 @@ +import { TraceSource } from "../log/TraceSource"; +import { argumentNotNull, argumentNotEmptyString, isPrimitive, each, isNull } from "../safe"; +import { Uuid } from '../Uuid'; +import { Container, ActivationContextInfo, ServiceMap, Descriptor, isDescriptor } from "../di"; + +let trace = TraceSource.get("di"); + + +export class ActivationContext { + _cache: object + + _services: ServiceMap + + _stack: ActivationContextInfo[] + + _visited: any + + container: any + + + constructor(container: Container, services: ServiceMap, cache?: object, visited?) { + argumentNotNull(container, "container"); + argumentNotNull(services, "services"); + + this._visited = visited || {}; + this._stack = []; + this._cache = cache || {}; + this._services = services; + this.container = container; + } + + getService(name, def?): any { + let d = this._services[name]; + + if (!d) + if (arguments.length > 1) + return def; + else + throw new Error("Service '" + name + "' not found"); + + return d.activate(this, name); + } + + /** + * registers services local to the the activation context + * + * @name{string} the name of the service + * @service{string} the service descriptor to register + */ + register(name: string, service: Descriptor) { + argumentNotEmptyString(name, "name"); + + this._services[name] = service; + } + + clone() { + return new ActivationContext( + this.container, + Object.create(this._services), + this._cache, + this._visited + ); + + } + + has(id) { + return id in this._cache; + } + + get(id) { + return this._cache[id]; + } + + store(id, value) { + return (this._cache[id] = value); + } + + parse(data: any, name) { + var me = this; + if (isPrimitive(data)) + return data; + + if (isDescriptor(data)) { + return data.activate(this, name); + } else if (data instanceof Array) { + me.enter(name); + var v = data.map(function (x, i) { + return me.parse(x, "." + i); + }); + me.leave(); + return v; + } else { + me.enter(name); + var result = {}; + for (var p in data) + result[p] = me.parse(data[p], "." + p); + me.leave(); + return result; + } + } + + visit(id) { + var count = this._visited[id] || 0; + this._visited[id] = count + 1; + return count; + } + + getStack() { + return this._stack.slice().reverse(); + } + + enter(name, d?, localize?) { + if (trace.isLogEnabled()) + trace.log("enter " + name + " " + (d || "") + + (localize ? " localize" : "")); + this._stack.push({ + name: name, + service: d, + scope: this._services + }); + if (localize) + this._services = Object.create(this._services); + } + + leave() { + var ctx = this._stack.pop(); + this._services = ctx.scope; + + if (trace.isLogEnabled()) + trace.log("leave " + ctx.name + " " + (ctx.service || "")); + } +} \ No newline at end of file diff --git a/src/ts/di/Container.ts b/src/ts/di/Container.ts new file mode 100644 --- /dev/null +++ b/src/ts/di/Container.ts @@ -0,0 +1,282 @@ +import { Uuid } from "../Uuid"; +import { ActivationContext } from "./ActivationContext"; +import { ActivationError } from "../di"; + + +export class Container { + _services + + _cache + + _cleanup: any[] + + _root: Container + + _parent: Container + + constructor(parent?: Container) { + this._parent = parent; + this._services = parent ? Object.create(parent._services) : {}; + this._cache = {}; + this._cleanup = []; + this._root = parent ? parent.getRootContainer() : this; + this._services.container = new ValueDescriptor(this); + } + + getRootContainer() { + return this._root; + } + + getParent() { + return this._parent; + } + + getService(name: string, def?: T) { + let d = this._services[name]; + if (!d) + if (arguments.length > 1) + return def; + else + throw new Error("Service '" + name + "' isn't found"); + + if (d.isInstanceCreated()) + return d.getInstance(); + + var context = new ActivationContext(this, this._services); + + try { + return d.activate(context, name); + } catch (error) { + throw new ActivationError(name, context.getStack(), error); + } + } + + register(nameOrCollection, service?) { + if (arguments.length == 1) { + var data = nameOrCollection; + for (let name in data) + this.register(name, data[name]); + } else { + if (!(service instanceof Descriptor)) + service = new ValueDescriptor(service); + this._services[nameOrCollection] = service; + } + return this; + } + + onDispose(callback) { + if (!(callback instanceof Function)) + throw new Error("The callback must be a function"); + this._cleanup.push(callback); + } + + dispose() { + if (this._cleanup) { + for (var i = 0; i < this._cleanup.length; i++) + this._cleanup[i].call(null); + this._cleanup = null; + } + } + + /** + * @param{String|Object} config + * The configuration of the contaier. Can be either a string or an object, + * if the configuration is an object it's treated as a collection of + * services which will be registed in the contaier. + * + * @param{Function} opts.contextRequire + * The function which will be used to load a configuration or types for services. + * + */ + async configure(config, opts) { + var me = this, + contextRequire = (opts && opts.contextRequire); + + if (typeof (config) === "string") { + let args; + if (!contextRequire) { + var shim = [config, Uuid()].join(config.indexOf("/") != -1 ? "-" : "/"); + args = await new Promise((resolve, reject) => { + define(shim, ["require", config], function (ctx, data) { + resolve([data, { + contextRequire: ctx + }]); + }) + }); + require([shim]); + } else { + // TODO how to get correct contextRequire for the relative config module? + args = await new Promise((resolve, reject) => { + contextRequire([config], function (data) { + resolve([data, { + contextRequire: contextRequire + }]); + }); + }); + } + + return me._configure.apply(me, args); + } else { + return me._configure(config, opts); + } + } + + createChildContainer() { + return new Container(this); + } + + has(id) { + return id in this._cache; + } + + get(id) { + return this._cache[id]; + } + + store(id, value) { + return (this._cache[id] = value); + } + + async _configure(data, opts) { + var typemap = {}, + me = this, + p, + contextRequire = (opts && opts.contextRequire) || require; + + var services = {}; + + for (p in data) { + var service = me._parse(data[p], typemap); + if (!(service instanceof Descriptor)) + service = new AggregateDescriptor(service); + services[p] = service; + } + + me.register(services); + + var names = []; + + for (p in typemap) + names.push(p); + + return new Promise((resolve, reject) => { + if (names.length) { + contextRequire(names, function () { + for (var i = 0; i < names.length; i++) + typemap[names[i]] = arguments[i]; + resolve(me); + }); + } else { + resolve(me); + } + }); + + } + + _parse(data, typemap) { + if (isPrimitive(data) || data instanceof Descriptor) + return data; + if (data.$dependency) { + return new ReferenceDescriptor( + data.$dependency, + data.lazy, + data.optional, + data["default"], + data.services && this._parseObject(data.services, typemap)); + } else if (data.$value) { + return !data.parse ? + new ValueDescriptor(data.$value) : + new AggregateDescriptor(this._parse(data.$value, typemap)); + } else if (data.$type || data.$factory) { + return this._parseService(data, typemap); + } else if (data instanceof Array) { + return this._parseArray(data, typemap); + } + + return this._parseObject(data, typemap); + } + + _parseService(data, typemap) { + var me = this, + opts: any = { + owner: this + }; + if (data.$type) { + + opts.type = data.$type; + + if (typeof (data.$type) === "string") { + typemap[data.$type] = null; + opts.typeMap = typemap; + } + } + + if (data.$factory) + opts.factory = data.$factory; + + if (data.services) + opts.services = me._parseObject(data.services, typemap); + if (data.inject) + opts.inject = data.inject instanceof Array ? data.inject.map(function (x) { + return me._parseObject(x, typemap); + }) : me._parseObject(data.inject, typemap); + if (data.params) + opts.params = me._parse(data.params, typemap); + + if (data.activation) { + if (typeof (data.activation) === "string") { + switch (data.activation.toLowerCase()) { + case "singleton": + opts.activation = ActivationType.SINGLETON; + break; + case "container": + opts.activation = ActivationType.CONTAINER; + break; + case "hierarchy": + opts.activation = ActivationType.HIERARCHY; + break; + case "context": + opts.activation = ActivationType.CONTEXT; + break; + case "call": + opts.activation = ActivationType.CALL; + break; + default: + throw new Error("Unknown activation type: " + + data.activation); + } + } else { + opts.activation = Number(data.activation); + } + } + + if (data.cleanup) + opts.cleanup = data.cleanup; + + return new ServiceDescriptor(opts); + } + + _parseObject(data, typemap) { + if (data.constructor && + data.constructor.prototype !== Object.prototype) + return new ValueDescriptor(data); + + var o = {}; + + for (var p in data) + o[p] = this._parse(data[p], typemap); + + return o; + } + + _parseArray(data, typemap) { + if (data.constructor && + data.constructor.prototype !== Array.prototype) + return new ValueDescriptor(data); + + var me = this; + return data.map(function (x) { + return me._parse(x, typemap); + }); + } +} \ No newline at end of file diff --git a/src/ts/di/Descriptor.ts b/src/ts/di/Descriptor.ts new file mode 100644 --- /dev/null +++ b/src/ts/di/Descriptor.ts @@ -0,0 +1,11 @@ +import { ActivationContext } from "./ActivationContext"; +import { isNull } from "../safe"; + +export interface Descriptor { + activate(context: ActivationContext, name?: string); +} + +export function isDescriptor(instance): instance is Descriptor { + return (!isNull(instance)) && + ('activate' in instance); +} \ No newline at end of file diff --git a/src/ts/di/ReferenceDescriptor.ts b/src/ts/di/ReferenceDescriptor.ts new file mode 100644 --- /dev/null +++ b/src/ts/di/ReferenceDescriptor.ts @@ -0,0 +1,98 @@ +import { isNull, argumentNotEmptyString, each } from "../safe"; + +class ReferenceDescriptor extends Descriptor { + _name: string + + _lazy = false + + _optional = false + + _default: any + + _services: object + + constructor(name: string, lazy: boolean, optional: boolean, def, services: object) { + super(); + argumentNotEmptyString(name, "name"); + this._name = name; + this._lazy = Boolean(lazy); + this._optional = Boolean(optional); + this._default = def; + this._services = services; + } + + activate(context: ActivationContext, name: string) { + var me = this; + + context.enter(name, this, true); + + // добавляем сервисы + if (me._services) { + for (var p in me._services) { + var sv = me._services[p]; + context.register(p, sv instanceof Descriptor ? sv : new AggregateDescriptor(sv)); + } + } + + if (me._lazy) { + // сохраняем контекст активации + context = context.clone(); + return function (cfg) { + // защищаем контекст на случай исключения в процессе + // активации + var ct = context.clone(); + try { + if (cfg) + each(cfg, function (v, k) { + ct.register(k, v instanceof Descriptor ? v : new AggregateDescriptor(v)); + }); + return me._optional ? ct.getService(me._name, me._default) : ct + .getService(me._name); + } catch (error) { + throw new ActivationError(me._name, ct.getStack(), error); + } + }; + } + + var v = me._optional ? + context.getService(me._name, me._default) : + context.getService(me._name); + + context.leave(); + return v; + } + + isInstanceCreated() { + return false; + } + + getInstance() { + throw new Error("The reference descriptor doesn't allowed to hold an instance"); + } + + toString() { + var opts = []; + if (this._optional) + opts.push("optional"); + if (this._lazy) + opts.push("lazy"); + + var parts = [ + "@ref " + ]; + if (opts.length) { + parts.push("{"); + parts.push(opts.join()); + parts.push("} "); + } + + parts.push(this._name); + + if (!isNull(this._default)) { + parts.push(" = "); + parts.push(this._default); + } + + return parts.join(""); + } +} \ No newline at end of file diff --git a/src/ts/safe.ts b/src/ts/safe.ts --- a/src/ts/safe.ts +++ b/src/ts/safe.ts @@ -45,7 +45,7 @@ export function isNullOrEmptyString(str) return true; } -export function isNotEmptyArray(arg) { +export function isNotEmptyArray(arg): arg is Array { return (arg instanceof Array && arg.length > 0); } @@ -61,7 +61,7 @@ export function isNotEmptyArray(arg) { * @returns Результат вызова функции cb, либо undefined * если достигнут конец массива. */ -export function each(obj, cb, thisArg) { +export function each(obj, cb, thisArg?) { argumentNotNull(cb, "cb"); var i, x; if (obj instanceof Array) { @@ -81,11 +81,55 @@ export function each(obj, cb, thisArg) { } } +/** Copies property values from a source object to the destination and returns + * the destination onject. + * + * @param dest The destination object into which properties from the source + * object will be copied. + * @param source The source of values which will be copied to the destination + * object. + * @param template An optional parameter specifies which properties should be + * copied from the source and how to map them to the destination. If the + * template is an array it contains the list of property names to copy from the + * source to the destination. In case of object the templates contains the map + * where keys are property names in the source and the values are property + * names in the destination object. If the template isn't specified then the + * own properties of the source are entirely copied to the destination. + * + */ +export function mixin(dest: T, source: S, template?: string[] | object) : T & S { + argumentNotNull(dest, "to"); + let _res = dest; + + if (template instanceof Array) { + for(let i = 0; i < template.length; i++) { + let p = template[i]; + if (p in source) + _res[p] = source[p]; + } + } else if (template) { + let keys = Object.keys(source); + for(let i = 0; i < keys.length; i++) { + let p = keys[i]; + if (p in template) + _res[template[p]] = source[p]; + } + } else { + let keys = Object.keys(source); + for(let i = 0; i < keys.length; i++) { + let p = keys[i]; + _res[p] = source[p]; + } + } + + return _res; +} + /** Wraps the specified function to emulate an asynchronous execution. * @param{Object} thisArg [Optional] Object which will be passed as 'this' to the function. * @param{Function|String} fn [Required] Function wich will be wrapped. */ -export function async(_fn: (...args: any[]) => any, thisArg) : (...args: any[]) => PromiseLike { +export function async(_fn: (...args: any[]) => any, thisArg): (...args: any[]) => PromiseLike { let fn = _fn; if (arguments.length == 2 && !(fn instanceof Function)) @@ -94,7 +138,7 @@ export function async(_fn: (...args: any if (fn == null) throw new Error("The function must be specified"); - function wrapresult(x, e?) : PromiseLike { + function wrapresult(x, e?): PromiseLike { if (e) { return { then: function (cb, eb) { @@ -129,19 +173,18 @@ export function async(_fn: (...args: any }; } -export function delegate(target, _method: (string | Function)) { - let method : Function; +export function delegate(target: T, _method: (K | Function)) { + let method; if (!(_method instanceof Function)) { argumentNotNull(target, "target"); method = target[_method]; + if (!(method instanceof Function)) + throw new Error("'method' argument must be a Function or a method name"); } else { method = _method; } - if (!(method instanceof Function)) - throw new Error("'method' argument must be a Function or a method name"); - return function () { return method.apply(target, arguments); }; diff --git a/tsc.json b/tsc.json deleted file mode 100644 --- a/tsc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "module": "amd", - "sourceMap": true, - "outDir" : "build/dist", - "declaration": true, - "lib": [ - "ES2015" - ] - }, - "include" : [ - "src/ts/**/*.ts" - ] -} \ No newline at end of file diff --git a/tsc.test.json b/tsc.test.json deleted file mode 100644 --- a/tsc.test.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "module": "amd", - "sourceMap": true, - "outDir" : "build/test", - "moduleResolution": "node", - "lib": [ - "ES2015" - ] - }, - "include" : [ - "test/ts/**/*.ts" - ] -} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "amd", + "sourceMap": true, + "outDir" : "build/dist", + "declaration": true, + "lib": [ + "es2015" + ] + }, + "include" : [ + "src/ts/**/*.ts" + ] +} \ No newline at end of file diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "amd", + "sourceMap": true, + "outDir" : "build/test", + "moduleResolution": "node", + "lib": [ + "ES2015" + ] + }, + "include" : [ + "test/ts/**/*.ts" + ] +} \ No newline at end of file