injektor
Version:
The simple dependency injection for Devebot
474 lines (464 loc) • 12.3 kB
JavaScript
;
const chores = require("./chores");
const errors = require("./errors");
const Validator = require("./validator");
const debugx = require("./pinbug")("injektor:engine");
const util = require("util");
const noop = function () {};
const ACCEPTED_SEPARATORS = ["/", ":", "$"];
const NAME_PATTERN_TMPL = "^[a-zA-Z]{1}[a-zA-Z0-9&\\-_%s]*$";
const defaultConfig = {
argumentSchemaLabel: "argumentSchema",
argumentFieldsLabel: "argumentProperties",
referenceArrayLabel: "referenceArray",
namePatternTemplate: NAME_PATTERN_TMPL,
nameResolvingStrictMode: false,
allowLookupDuplicateNames: false,
detectCyclicDependencies: true,
separator: "/",
logger: {
debug: noop,
trace: noop,
error: noop
}
};
function Injektor(a = {}) {
debugx.enabled && debugx(" + constructor start ...");
const b = {};
Object.keys(defaultConfig).forEach(function (c) {
switch (c) {
case "separator":
if (ACCEPTED_SEPARATORS.indexOf(a[c]) >= 0) {
b[c] = a[c];
} else {
b[c] = defaultConfig[c];
}
break;
default:
if (c in a) {
b[c] = a[c];
} else {
b[c] = defaultConfig[c];
}
}
});
const c = util.format(b.namePatternTemplate, b.separator);
const d = new RegExp(c, "g");
debugx.enabled && debugx(" - namePattern: %s", c);
const e = {};
const f = {};
const g = new Validator();
const h = {
config: b,
dependencies: e,
namestore: f,
validator: g
};
/**
* Get the dependency. If the call is the first time, copy it (for object) or
* instantiate if (for constructor).
*/
function j(a, c, d) {
c = c || {};
d = d || [];
const f = s(a, c, d);
if (typeof f === "string") {
c = extractScope({
config: b
}, f, c);
}
const g = e[f];
if (g == null) {
const b = new errors.DependencyNotFoundError("No dependency registered with name: " + a);
d.push(b);
return undefined;
}
b.detectCyclicDependencies && q(f, c);
g["cache"] = g["cache"] || k(f, c, d);
b.detectCyclicDependencies && r(f, c);
return g["cache"];
}
/**
* Converts the dependency from definition into an instance (object or service).
*/
function k(a, b, c) {
let d;
const f = e[a];
if (f["type"] == "service") {
if (f["error"] == null) {
try {
const a = l(f["value"], b, c);
if (c.length === 0) d = a;
} catch (b) {
debugx.enabled && debugx("convert(\"%s\") has failed: %s/%s", a, b.name, b.message);
f["error"] = b;
}
}
if (f["error"]) {
const a = f["error"];
c.push(a);
if (t(a)) throw a;
}
} else {
d = f["value"];
}
return d;
}
/**
* Instantiates a constructor
*/
function l(a, b, c) {
const d = function (b) {
return a.apply(this, b);
};
d.prototype = a.prototype;
return new d(m(a, b, c));
}
/**
* Extracts the dependencies of a constructor
*/
function m(a, c, d) {
if (a[b.argumentSchemaLabel]) {
const e = a[b.argumentSchemaLabel];
if (e instanceof Object && !(e instanceof Array) && (e.properties == null || e.properties instanceof Object) && (e.properties == null || !(e.properties instanceof Array))) {
const a = {};
const b = Object.keys(e.properties || {});
for (let e = 0; e < b.length; e++) {
a[b[e]] = j(b[e], c, d);
}
const f = g.validate(a, e);
if (!f.ok) {
throw new errors.ValidatingArgumentError("Constructor argument validation is failed");
}
return [a];
} else {
throw new errors.InvalidArgumentSchemaError("Constructor has invalid argumentSchema descriptor");
}
} else if (a[b.argumentFieldsLabel]) {
const e = a[b.argumentFieldsLabel];
if (e instanceof Array) {
const a = {};
for (let b = 0; b < e.length; b++) {
a[e[b]] = j(e[b], c, d);
}
return [a];
} else {
throw new errors.InvalidArgumentSchemaError("Constructor has invalid argumentFields descriptor");
}
} else if (a[b.referenceArrayLabel]) {
const e = a[b.referenceArrayLabel];
if (e instanceof Array) {
const a = [];
for (let b = 0; b < e.length; b++) {
a.push(j(e[b], c, d));
}
return a;
} else {
throw new errors.InvalidArgumentSchemaError("Constructor has invalid referenceArray descriptor");
}
} else {
const b = n(a);
return b.map(function (a) {
return j(a, c, d);
});
}
}
function n(a) {
const b = a.toString().replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s))/mg, "").match(/^function\s*[^(]*\(\s*([^)]*)\)/m)[1].split(/,/);
if (b.length == 1 && b[0] == "") return [];
return b;
}
function o(a) {
if (typeof a === "function") {
return {
serviceInit: a
};
}
if (a instanceof Array) {
if (a.length > 0) {
a = a.slice();
const b = a.pop();
if (typeof b === "function") {
let c = {
serviceInit: b
};
if (a.length > 0) {
c.referenceNames = a;
}
return c;
}
}
}
return {};
}
function p(a) {
if (a.absoluteName != a.relativeName) {
f[a.relativeName] = f[a.relativeName] || [];
if (f[a.relativeName].indexOf(a.absoluteName) < 0) {
f[a.relativeName].push(a.absoluteName);
}
}
}
function q(a, b) {
b = b || {};
b.stacktrace = b.stacktrace || [];
b.stacktrace.push(a);
if (b.stacktrace.length == 1) {
debugx.enabled && debugx(" - (%s) => %s", a, JSON.stringify(b.stacktrace));
} else {
debugx.enabled && debugx(" - (%s) -> %s", a, JSON.stringify(b.stacktrace));
}
const c = b.stacktrace.indexOf(a);
if (c != b.stacktrace.length - 1) {
throw new errors.DependencyCycleDetectedError(util.format("dependency cycle detected for \"%s\" at %s and %s in %s", a, b.stacktrace.length - 1, c, JSON.stringify(b.stacktrace)));
}
}
function r(a, b) {
b = b || {};
b.stacktrace = b.stacktrace || [];
b.stacktrace.pop();
if (b.stacktrace.length > 0) {
debugx.enabled && debugx(" - (%s) <- %s", a, JSON.stringify(b.stacktrace));
} else {
debugx.enabled && debugx(" - (%s) <= %s", a, JSON.stringify(b.stacktrace));
}
}
function s(a, c, d) {
c = c || {};
let g = null;
if (c.scope) {
const d = [c.scope, a].join(b.separator);
if (e[d]) g = d;
}
if (g == null && e[a]) g = a;
if (g == null && f[a] && f[a].length > 0) {
if (f[a].length === 1 || b.nameResolvingStrictMode !== true) {
g = f[a][0];
}
if (b.allowLookupDuplicateNames != true && f[a].length > 1) {
const b = new errors.DuplicatedRelativeNameError("name [" + a + "] is duplicated");
if (d instanceof Array) {
d.push(b);
} else {
throw b;
}
}
}
return g;
}
function t(a) {
return a instanceof errors.DependencyNotFoundError || a instanceof errors.DependencyCycleDetectedError;
}
/**
* Registering an object as a dependency
*/
this.registerObject = function (a, c, f) {
validateName({
namePattern: d
}, a);
const g = parseName({
config: b
}, a, f);
e[g.absoluteName] = {
type: "object",
value: c
};
p(g);
return this;
};
/**
* Registering a singleton-service constructor as a dependency
*/
this.defineService = function (a, c, f) {
validateName({
namePattern: d
}, a);
const g = o(c);
if (!g.serviceInit) {
throw new errors.InvalidMethodArgumentError("invalid service descriptor");
}
if (g.referenceNames) {
g.serviceInit.referenceArray = g.referenceNames;
}
const h = parseName({
config: b
}, a, f);
e[h.absoluteName] = {
type: "service",
value: g.serviceInit
};
p(h);
return this;
};
this.parseName = function (a, c) {
const d = Object.assign({}, c);
const e = d.exceptions;
delete d.exceptions;
return parseName({
config: b
}, a, d, e);
};
this.resolve = function (a, b) {
const c = Object.assign({}, b);
const d = c.exceptions;
delete c.exceptions;
return s(a, c, d);
};
this.resolveName = this.resolve;
this.suggest = function (a) {
if (e[a]) return [a];
if (f[a]) {
if (f[a].length === 0) {
return [];
} else {
return f[a].slice();
}
}
return null;
};
this.suggestName = this.suggest;
/**
* Lookups a dependency (object or service) from store
*/
this.lookup = function (a, b) {
validateName({
namePattern: d
}, a);
let c = false;
let e = [];
// extract exceptions from options
if (chores.isArray(b)) {
c = true;
e = b;
b = {};
} else if (chores.isObject(b)) {
c = chores.isArray(b.exceptions);
e = b.exceptions = b.exceptions || [];
}
// retrieve object from storage
const f = j(a, b, e);
if (!c && e.length > 0) {
chores.printExceptions(e);
if (e.length === 1) throw e[0];
throw new errors.DependencyCombinationError("lookup() is failed", e);
}
return f;
};
/**
* Collects all dependencies of a service, pass them to and run the service
*/
this.invoke = function (a) {
if (!chores.isArray(a)) {
a = Array.prototype.slice.call(arguments);
}
var b = o(a);
if (!b.serviceInit) {
throw new errors.InvalidMethodArgumentError("invalid callback descriptor");
}
b.serviceInit.referenceArray = b.referenceNames;
var c = {};
var d = [];
var e = m(b.serviceInit, c, d);
if (d.length > 0) {
chores.printExceptions(d);
if (d.length === 1) throw d[0];
throw new errors.DependencyCombinationError("invoke() is failed", d);
}
return b.serviceInit.apply(null, e);
};
Object.defineProperty(this, "separator", {
get: function () {
return b.separator;
},
set: function (a) {}
});
debugx.enabled && debugx(" - constructor end");
}
function validateName(a, b) {
let {
namePattern: c
} = a;
if (typeof b !== "string" || b.length === 0) {
throw new errors.InvalidMethodArgumentError("dependency name is empty or not a string");
}
if (b.match(c) == null) {
throw new errors.InvalidMethodArgumentError("dependency name does not match the pattern");
}
}
function extractScope(a, b, c) {
c = c || {};
const d = chores.cloneObject(c);
const e = splitScopeWithName(a, b);
if (e.scope) {
d.scope = e.scope;
}
return d;
}
function parseName(a, b, c) {
const {
config: d
} = a;
const e = [];
c = c || {};
const f = splitScopeWithName(a, b);
if (f.name) {
e.push(f.scope, f.name);
} else {
e.push(b);
}
if (e.length === 1) {
if (c.scope) e.unshift(c.scope);
}
if (e.length === 0 || e.length > 2) {
throw new errors.InvalidMethodArgumentError("invalid name format");
}
var g = {
name: b,
absoluteName: e.join(d.separator),
relativeName: e[e.length - 1],
scope: e[e.length - 2]
};
return g;
}
let splitScopeWithName = function (a, b) {
const {
config: c
} = a || {};
let {
separator: d
} = c;
//
const e = util.format("^(?<scope>.+)%s(?<name>[a-zA-Z]{1}[a-zA-Z0-9-_#]*)$", d);
const f = new RegExp(e);
//
const g = b.match(f);
const h = {};
if (g && g.groups) {
for (const a in g.groups) {
h[a] = g.groups[a];
}
}
return h;
};
/* eslint-disable no-unused-vars */
function parseNameLegacy(a, b, c) {
const {
config: d
} = a;
c = c || {};
var e = b.split(d.separator);
if (e.length === 1) {
if (c.scope) e.unshift(c.scope);
}
if (e.length === 0 || e.length > 2) {
throw new errors.InvalidMethodArgumentError("invalid name format");
}
var f = {
name: b,
absoluteName: e.join(d.separator),
relativeName: e[e.length - 1],
scope: e[e.length - 2]
};
return f;
}
module.exports = Injektor;