// Import required bits here.
const
{
B, S,
root, isObj, needObj, def, nonEmptyArray, notNil,
doesDescriptorTemplate, console,
} = require('../types');
/**
* Need a String or Array
* @alias module:@lumjs/core/obj.SOA
*/
function SOA(name, err=true)
{
const msg = (typeof name === S)
? name + ' ' + SOA.message
: SOA.message;
return err ? (new TypeError(msg)) : msg;
}
SOA.message = "must be a string or non-empty array";
def(SOA, 'toString', function() { return this.message; });
exports.SOA = SOA;
/**
* Get a namespace path string.
*
* If it's already a string, return it as is.
* If it's an array of strings, join the elements with a `.` character.
*
* @param {(string|string[])} ns - A dotted string, or array of paths.
* @param {*} name - Name to use in the SOA error
* @returns {string}
* @alias module:@lumjs/core/obj.nsString
*/
function nsString(ns, name='Namespace')
{
if (nonEmptyArray(ns))
{
return ns.join('.');
}
else if (typeof ns !== S)
{
throw SOA(name);
}
return ns;
}
exports.nsString = nsString;
/**
* Get a namespace path array.
*
* If it's already an array, return it as is.
* If it's a string, split it into an array, with the `.` delimiter.
*
* @param {(string|string[])} ns - A dotted string, or array of paths.
* @param {*} name - Name to use in the SOA error
* @returns {string[]}
* @alias module:@lumjs/core/obj.nsArray
*/
function nsArray(ns, name='Namespace')
{
if (typeof ns === S)
{
return ns.split('.');
}
else if (!nonEmptyArray(ns))
{
throw SOA(name);
}
return ns;
}
exports.nsArray = nsArray;
/**
* Get a (nested) property from an object with a given path.
*
* @param {(object|function)} obj - Object we're looking in.
* @param {(string|Array)} proppath - Property path we're looking for.
* Generally a string of dot (`.`) separated nested property names.
* @param {(object|boolean)} [opts] Options changing the behaviours.
* If this is a `boolean` it's assumed to be the `opts.log` option.
* @param {boolean} [opts.log=false] Log errors for missing namespaces?
* @param {*} [opts.default] A default value if the namespace is not found.
*
* @return {*} The property if found, or `opts.default` if not.
* @alias module:@lumjs/core/obj.getObjectPath
*/
function getObjectPath(obj, proppath, opts={})
{
needObj(obj, true);
if (typeof opts === B)
opts = {log: opts};
else if (!isObj(opts))
opts = {};
proppath = nsArray(proppath);
for (let p = 0; p < proppath.length; p++)
{
const propname = proppath[p];
if (obj[propname] === undefined)
{ // End of search, sorry.
if (opts.log)
{
console.error("Object property path not found",
propname, p, proppath, obj);
}
return opts.default;
}
obj = obj[propname];
}
return obj;
}
exports.getObjectPath = getObjectPath;
/**
* Create a nested property path if it does not exist.
*
* @param {(object|function)} obj - Object the property path is for.
* @param {(string|Array)} proppath - Property path to create.
* @param {object} [opts] Options changing the behaviours.
* @param {*} [opts.value] A value to assign to the last property path.
* @param {boolean} [opts.overwrite=false] Allow overwriting property paths.
* Only applicable if `opts.value` was also specified.
* @param {object} [opts.desc] Descriptor rules for defining the properties.
* Must NOT contain `value`, `get`, or `set` properties.
* Only, `configurable`, `enumerable`, and `writable` are supported.
* Will be ignored if `opts.assign` is `true`.
* @param {boolean} [opts.assign=false] Use direct assignment instead of `def()`.
* @param {boolean} [opts.returnThis=false] Return `this` variable.
* @param {boolean} [opts.returnObj=false] Return the `obj` parameter.
* @return {*} Generally the last object in the nested property paths.
* Unless one of `opts.returnThis` or `opts.returnObj` was `true`.
* @alias module:@lumjs/core/obj.setObjectPath
*/
function setObjectPath(obj, proppath, opts={})
{
needObj(obj, true, 'obj parameter must be an object or function');
needObj(opts, 'opts parameter must be an object');
proppath = nsArray(proppath);
let assign;
if (doesDescriptorTemplate(opts.desc))
{ // An explicit descriptor.
assign = (o,p,v={}) =>
{
const desc = clone(opts.desc);
desc.value = v;
def(o,p,desc);
}
}
else if (opts.assign)
{ // Use direct property assignment.
assign = (o,p,v={}) => o[p] = v;
}
else
{ // Use def with default descriptor.
assign = (o,p,v={}) => def(o,p,v);
}
let cns = obj;
const nsc = proppath.length;
const lastns = nsc - 1;
//console.debug("setObjectPath", {obj, proppath, opts, nsc, arguments});
for (let n = 0; n < nsc; n++)
{
const ns = proppath[n];
//console.debug("setObjectPath:loop", {n, ns, cns});
if (cns[ns] === undefined)
{ // Nothing currently here. Let's fix that.
if (n == lastns && notNil(opts.value))
{ // We're at the end and have a value to assign.
assign(cns, ns, opts.value);
}
else
{ // Create a new empty object.
assign(cns, ns);
}
}
else if (opts.overwrite && n == lastns && notNil(opts.value))
{ // We have a value, and overwrite mode is on.
assign(cns, ns, opts.value);
}
cns = cns[ns];
}
if (opts.returnThis)
{
return this;
}
else if (opts.returnObj)
{
return obj;
}
else
{ // Default is to return the last namespace object.
return cns;
}
}
exports.setObjectPath = setObjectPath;
/**
* Get a global namespace path if it exists.
*
* This literally just calls `getObjectPath()` on the `root` object.
*
* @param {(string|Array)} proppath - Property path we're looking for.
* Generally a string of dot (`.`) separated nested property names.
* @param {object} [opts] See `getObjectPath()` for details.
* @return {*} The property if found, or `opts.default` if not.
* @alias module:@lumjs/core/obj.getNamespace
* @see module:@lumjs/core/obj.getObjectPath
*/
function getNamespace(namespaces, opts={})
{
return getObjectPath(root, namespaces, opts);
}
exports.getNamespace = getNamespace;
/**
* Create a global namespace path if it does not exist.
*
* This literally just calls `setObjectPath()` on the `root` object.
*
* @param {(string|Array)} proppath - Property path to create.
* @param {object} [opts] See `setObjectPath()` for details.
* @return {*} See `setObjectPath()` for details.
* @alias module:@lumjs/core/obj.setNamespace
* @see module:@lumjs/core/obj.setObjectPath
*/
function setNamespace(namespaces, opts={})
{
return setObjectPath(root, namespaces, opts);
}
exports.setNamespace = setNamespace;