/*
* Author: Vlad Seryakov vseryakov@gmail.com
* backendjs 2018
*/
const util = require('util');
const fs = require('fs');
const path = require('path');
const modules = require(__dirname + '/../modules');
const app = require(__dirname + '/../app');
const lib = require(__dirname + '/../lib');
const logger = require(__dirname + '/../logger');
/**
* Parse config lines for the file or other place,
* @param {string} data - data from a config file
* @param {int} pass - a number representing a pass phase
* @param {string} [file] - file name where parameters came from
* @examples
* tag=T, runMode=M, role=R, roles=R, instance.tag=T, instance.roles=dev, aws.region=R, aws.tags=T, env.NAME=V
* @memberOf module:app
* @method parseConfig
*/
app.parseConfig = function(data, pass, file)
{
var context = ["role", "roles", "tag", "instance", "runMode",
"version", "config", "arch", "host", "platform" ].reduce((a, b) => {
a[b] = app.instance[b] || app[b];
return a;
}, {});
context.env = process.env;
var argv = lib.configParse(data, context);
if (argv.length) this.parseArgs(argv, pass, file);
}
/**
* Parse command line arguments
* @param {string[]} argv - a list of config parameters in the form [ "-param", "value" ,...]
* @param {int} pass - a number representing a pass phase
* @param {string} [file] - file name where parameters came from
* @memberOf module:app
* @method parseArgs
*/
app.parseArgs = function(argv, pass, file)
{
if (!Array.isArray(argv) || !argv.length) return;
logger.dev('parseArgs:', this.role, file, argv.join(' '));
// Run registered handlers for each module
for (const p in modules) {
this.processArgs(modules[p], argv, pass, file);
}
}
/**
* @param {object} mod - run for module's args only
* @param {string[]} argv - a list of config parameters in the form [ "-param", "value" ,...]
* @param {int} pass - a number representing a pass phase
* @param {string} [file] - file name where parameters came from
* @memberOf module:app
* @method processArgs
*/
app.processArgs = function(mod, argv, pass, file)
{
if (!Array.isArray(mod?.args) || !lib.isArray(argv)) return;
for (let i = 0; i < argv.length; i++) {
var key = String(argv[i]);
if (!key || key[0] != "-") continue;
var val = argv[i + 1] || null;
if (val) {
val = String(val);
// Numbers can start with the minus and be the argument value
if (val[0] == "-" && !/^[0-9-]+$/.test(val)) val = null; else i++;
}
var opts = _findArg(mod, key, val, pass, file);
if (opts) this.processArg(opts);
}
}
/**
* Process parameters from env variables
* @memberOf module:app
* @method processEnvArgs
*/
app.processEnvArgs = function()
{
var args = this.args.filter((x) => (x.env && process.env[x.env] !== undefined)).map((x) => ([this, x, process.env[x.env]]));
for (const p in modules) {
args.push(...lib.isArray(modules[p].args, []).filter((x) => (x.env && process.env[x.env] !== undefined)).map((x) => ([modules[p], x, process.env[x.env]])));
}
for (const a of args) {
this.processArg({ mod: a[0], arg: a[1], key: a[1].name, val: a[2], file: "env" });
}
}
/**
* Process a single argument
* @param {object} options are supposed to be returned by _findArg or prepared accordingly
* @memberOf module:app
* @method processArg
*/
app.processArg = function(options)
{
var mod = options.mod;
var context = options.mod;
var arg = options.arg;
var val = options.val;
var key = options.key;
var o = Object.assign({ errnull: 1 }, arg);
o.name = o.key || key;
o.matches = options.matches;
o._conf = options.file;
o._pass = options.pass;
o._val = options.val;
// Preprocess the parse config if necessary and value
if (typeof arg.onparse == "function") {
val = arg.onparse.call(mod, val, o);
}
try {
// Make name from the matched pieces
if (o.make) {
o.name = o.make;
if (o.name.includes("$")) {
for (let j = 1; j < o.matches?.length; j++) {
o.name = o.name.replace("$" + j, o.matches[j] || "");
}
}
}
// Place inside the object
if (o.obj) {
// Compound name, no camel
if (o.obj.includes(".")) {
const obj = o.obj.split(".");
// Substitutions from the matched key
for (const i in obj) {
if (!obj[i].includes("$")) continue;
for (let j = 1; j < o.matches?.length; j++) {
obj[i] = obj[i].replace("$" + j, o.matches[j] || "");
}
}
context = lib.objGet(mod, obj.concat(o.name), { owner: 1 });
if (!context) lib.objSet(mod, obj, context = {});
o.obj = obj.join(".");
} else {
// Substitutions from the matched key
if (o.obj.includes("$")) {
for (let j = 1; j < o.matches?.length; j++) {
o.obj = o.obj.replace("$" + j, o.matches[j] || "");
}
}
if (!o.nocamel || o.obj.includes("-")) {
o.obj = lib.toCamel(o.obj, o.camel || "-");
}
if (!mod[o.obj]) mod[o.obj] = {};
context = mod[o.obj];
// Strip the prefix if starts with the same name
if (o.name.startsWith(arg.obj + "-")) {
o.name = o.name.substr(arg.obj.length + 1);
}
}
}
// Name transforms
if (o.strip) o.name = o.name.replace(o.strip, "");
if (!o.nocamel) o.name = lib.toCamel(o.name, o.camel || "-");
if (o.upper) o.name = o.name.replace(o.upper, (v) => (v.toUpperCase()));
if (o.lower) o.name = o.name.replace(o.lower, (v) => (v.toLowerCase()));
for (const r in o.nreplace) o.name = o.name.replaceAll(r, o.nreplace[r]);
if (o.nametype) o.name = lib.toValue(o.name, o.nametype);
if (lib.isArray(o.names) && !lib.isFlag(o.names, o.name)) return false;
if (o.existing && typeof context[o.name] == "undefined") return false;
// Use defaults only for the first time
if (val == null && typeof context[o.name] == "undefined") {
if (typeof o.dflt != "undefined") val = o.dflt;
}
// Explicit empty value
if (val == "''" || val == '""') val = "";
// Only some types allow no value case
var type = (o.type || "").trim();
if (val == null && type != "bool" && type != "callback" && type != "none") return false;
// Can be set only once
if (arg.once) {
if (!arg._once) arg._once = {};
if (arg._once[o.name]) return;
arg._once[o.name] = 1;
}
// Freeze the command line value if pass is set
if (arg.pass == 2) {
if (options.pass == 2 && !arg._pass) arg._pass = 1; else
if (arg._pass) return;
}
// Set the actual config variable names for further reference and easy access to the value
if (val != null) {
if (!arg._name) arg._name = [];
const _n = (o.obj ? o.obj + "." : "") + o.name;
if (!arg._name.includes(_n)) arg._name.push(_n);
if (!arg._key) arg._key = [];
if (!arg._key.includes(key)) arg._key.push(key);
}
// Explicit clear
if (val == "<null>" || val == "~") val = null;
// Explicit clear for complex objects like regexpobj/map
if (val && val[0] == "~" && val[1] == "~") {
val = val.substr(2);
o.set = 1;
}
// Value transforms
if (typeof val == "string") {
for (const r in o.vreplace) val = val.replaceAll(r, o.vreplace[r]);
if (o.trim) val = val.trim();
}
if (o.noempty && lib.isEmpty(val)) return false;
if (val === o.novalue || Array.isArray(o.novalue) && o.novalue.includes(val)) return false;
// Autodetect type
if (o.autotype && val) type = lib.autoType(val) || type;
o._type = type;
o._val = val;
logger.debug("processArg:", app.role, options.file, mod.name, type || "str", o.obj, o.name, "(" + key + ")", "=", val === null ? "null" : val);
logger.dev(key, "=", val, o);
switch (type) {
case "none":
break;
case "bool":
case "int":
case "real":
case "number":
case "map":
case "set":
case "list":
case "rlist":
case "regexp":
case "regexpobj":
case "regexpmap":
case "url":
case "json":
case "js":
val = _processArg(context, o.name, val, o, arg.reverse, type);
break;
case "path":
// Check if it starts with local path, use the actual path not the current dir for such cases
for (const p in this.path) {
if (val && val.substr(0, p.length + 1) == p + "/") {
val = this.path[p] + val.substr(p.length);
break;
}
}
_processArg(context, o.name, val, o, arg.reverse, type);
break;
case "file":
if (!val) break;
try {
_processArg(context, o.name, fs.readFileSync(path.resolve(val)), arg);
} catch (e) {
logger.error('processArg:', app.role, options.file, mod.name, o.name, val, e);
}
break;
case "callback":
if (!arg.callback) break;
o.context = context;
if (typeof arg.callback == "string" && typeof mod[arg.callback] == "function") {
mod[arg.callback](val, o, options.pass);
} else
if (typeof arg.callback == "function") {
arg.callback.call(mod, val, o, options.pass);
}
delete o.context;
break;
default:
val = _processArg(context, o.name, val, o, arg.reverse);
}
// Notify about the update via custom function or the module method
if (arg.onupdate) {
o.context = context;
if (typeof arg.onupdate == "function") arg.onupdate.call(mod, val, o); else
if (typeof arg.onupdate == "string" && typeof mod[arg.onupdate] == "function") mod[arg.onupdate](val, o);
delete o.context;
}
} catch (e) {
logger.error("processArg:", app.role, options.file, mod.name, o.name, val, e.stack);
}
}
function _findArg(mod, key, val, pass, file)
{
var prefix = "-" + mod?.name?.replaceAll(".", "-") + "-";
if (!key || !key.startsWith(prefix)) return;
key = key.substr(prefix.length);
for (const i in mod.args) {
var arg = mod.args[i];
if (!arg?.name) continue;
// Process only equal to the given pass phase or process mode
if ((pass && !arg.pass) || (arg.primary && app.isWorker) || (arg.worker && app.isPrimary)) continue;
// Early value validation
if (util.types.isRegExp(arg.regexp) && !arg.regexp.test(val)) continue;
// Name can be a regexp
if (!arg._rx) arg._rx = new RegExp("^" + arg.name + "$");
var matches = key.match(arg._rx);
if (matches) return { mod, arg, key, val, pass, file, matches };
}
}
function _processArg(obj, key, val, arg, reverse, type)
{
function warn() {
logger.warn("processArg:", "function", app.role, arg._conf, key, val);
}
if (reverse) {
var v = val;
val = key;
key = v;
}
switch (type) {
case "bool":
val = !val ? true : lib.toBool(val);
break;
case "int":
case "real":
case "number":
val = lib.toNumber(val, arg);
// Number transformations
if (arg.multiplier) val *= arg.multiplier;
if (arg.ceil) val = Math.ceil(val);
if (arg.floor) val = Math.floor(val);
break;
case "regexp":
if (!val) break;
val = lib.toRegexp(val, arg.regexp);
if (!val) return;
if (val.test("") && !arg.empty) return warn();
break;
case "regexpobj":
val = lib.toRegexpObj(obj[key], val, arg);
if (!val) return;
if (val.rx?.test("") && !arg.empty) return warn();
if (!val.rx) val = null;
break;
case "regexpmap":
val = lib.toRegexpMap(obj[key], val, arg);
if (!val) return;
for (const i in val) {
if (val[i]?.rx?.test("") && !arg.empty) return warn();
}
break;
case "set":
arg.unique = true;
case "list":
if (val === null && arg.array) break;
val = lib.split(val, arg.separator, arg);
if (arg.max && val.length > arg.max) val = val.slice(0, arg.max);
if (arg.min == 1 && val.length == arg.min) val = val[0];
break;
case "rlist":
if (val === null && arg.array) break;
var k = key;
arg.unique = 1;
key = lib.split(val, arg.separator, arg);
val = lib.split(k, arg.separator, arg);
arg.array = 1;
for (const i in key) _processArg(obj, key[i], val, arg);
break;
case "map":
if (!arg.maptype) arg.maptype = "auto";
val = lib.toValue(val, "map", arg);
break;
case "url":
if (!val) break;
val = URL.parse(val);
if (!val) return;
break;
case "js":
case "json":
if (!val) break;
val = lib.jsonParse(val, arg);
if (!val) return;
break;
case "path":
val = val ? path.resolve(val) : val;
break;
default:
if (arg.valuetype) val = lib.toValue(val, arg.valuetype);
}
if (arg.ephemeral) return val;
if (arg.flatten && Array.isArray(val)) {
for (const i in val) _processArg(obj, val[i], key, arg);
} else
if (Array.isArray(key)) {
for (const i in key) _processArg(obj, key[i], val, arg);
} else
if (arg.merge) {
if (typeof obj == "function") return warn();
switch (type) {
case "json":
if (!obj || arg.set) for (const p in obj) delete obj[p];
for (const p in val) {
if (typeof obj[p] == "function") continue;
if (val[p] === arg.novalue || Array.isArray(arg.novalue) && arg.novalue.includes(val[p])) continue;
if (arg.noempty && lib.isEmpty(val[p])) continue;
obj[p] = val[p];
}
break;
case "map":
if (!obj || arg.set) for (const p in obj) delete obj[p];
for (const p in val) {
if (typeof obj[p] == "function") continue;
if (val[p] === arg.novalue || Array.isArray(arg.novalue) && arg.novalue.includes(val[p])) continue;
if (arg.noempty && lib.isEmpty(val[p])) continue;
obj[p] = val[p];
}
break;
}
} else
if (arg.array) {
if (typeof obj[key] == "function") return warn();
if (val == null) {
obj[key] = [];
} else {
if (!Array.isArray(obj[key]) || arg.set) obj[key] = [];
if (Array.isArray(val)) {
for (let y of val) {
if (typeof y == "string" && arg.trim) y = y.trim();
if (typeof y == "string" && y[0] == "!" && y[1] == "!") {
var i = obj[key].indexOf(y.substr(2));
if (i > -1) obj[key].splice(i, 1);
} else {
if (!obj[key].includes(y)) obj[key][arg.push ? "push": "unshift"](y);
}
}
} else {
if (!obj[key].includes(val)) obj[key][arg.push ? "push": "unshift"](val);
}
}
} else {
if (typeof obj[key] == "function") return warn();
if (val == null) {
delete obj[key];
} else
if (arg.dot && key.includes(".")) {
lib.objSet(obj, key, val);
} else {
obj[key] = val;
}
}
if (Array.isArray(obj[key]) && arg.sort) {
obj[key] = obj[key].sort();
}
return val;
}
/**
* Add custom config parameters to be understood and processed by the config parser
* @param {string} - a module name to add these params to
* @param {object[]} args - a list of objects in the format: { name: N, type: T, descr: D, min: M, max: M, array: B... }, all except name are optional.
* @returns {object} a module object where args added or undefined if not
* @memberOf module:app
* @method describeArgs
*
* @example
* app.describeArgs("api", [ { name: "num", type: "int", descr: "int param" }, { name: "list", array: 1, descr: "list of words" } ]);
* app.describeArgs("app", [ { name: "list", array: 1, descr: "list of words" } ]);
*/
app.describeArgs = function(name, args)
{
if (typeof name != "string" || !Array.isArray(args)) return;
var ctx = modules[name];
if (!ctx) return;
if (!ctx.args) ctx.args = [];
ctx.args.push(...args.filter((x) => (x.name)));
return ctx;
}