/*
JUL jul-data-mapper v1.1.3
Copyright (c) 2021 The Zonebuilder <zone.builder@gmx.com>
https://www.npmjs.com/package/jul-data-mapper
Licenses: GNU GPL2 or later; GNU LGPLv3 or later
*/
/**
* @fileOverview Use this tool to map a JavaScript object from a data schema to another data schema<br>
* Use with parsed JSON responses or any native JavaScript object structures.<br>
* Code example:
* <code><pre>
* 'use strict';
* const {mapper} = require('jul-data-mapper');
*
* const oSrc = {
* server: 'express',
* items: [
* {id: 101, name: 'Ana'},
* {id: 102, name: 'Bell'},
* {id: 103, name: 'Kevin'}
* ],
* pref: {perPage: 25, filter: false},
* grid: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
* };
* const oDest = {version: '1.0.0'};
* const oMap = {
* 'server': 'result.source',
* 'items[$u].id': 'result.entries[$u].uid',
* 'items[$u].name': 'result.entries[$u].fullName',
* 'grid[$i][$j]': 'map[$i][$j].value',
* pref: 'result.show'
* };
*
* // source -> destination
* console.info(
* mapper(oDest, oSrc, oMap)
* );
*
* // destination -> source
* const oReverseMap = Object.fromEntries(Object.entries(oMap).map(aItem => aItem.reverse()));
* console.info(
* mapper({}, oDest, oReverseMap)
* );
*
* // using a more compact form of mapping
* const oCompactMap = {
* 'server': 'result.source',
* 'items[$u]': {
* _mapTo: 'result.entries[$u]',
* id: 'uid',
* name: 'fullName'
* },
* 'grid[$i][$j]': 'map[$i][$j].value',
* pref: 'result.show'
* };
* console.info(
* mapper({}, oSrc, oCompactMap, {prefixProp: '_mapTo'})
* );
* </code></pre>
* @author {@link https://www.google.com/search?hl=en&num=50&start=0&safe=0&filter=0&nfpr=1&q=The+Zonebuilder+web+development+programming+IT+society+philosophy+politics|The Zonebuilder}
* @version 1.1.3
*/
/**
* Converts an object to another using a namespace path mapping.<br>
* Isomorphic: works both in backends and in frontends via e.g. webpack
* @module jul-data-mapper
*/
'use strict';
const jul = require('jul');
/**
* Cached regexs for internal use
* @type Object
* @private
*/
const rexs = {
dot: /(\.|\[|\])/g,
nat: /^(\d|[1-9]\d+)$/
};
/**
* Gets the array of segments for a JavaScript path<br>
* e.g. <code>'a.b.c[1][2].d' => ['a', 'b', 'c', '1', '2', 'd'] </code>
* @param {String} sNs A JavaScript (namespace) path
* @returns {Array} An array of path segments from left to right
* @private
*/
const segments = sNs => sNs || sNs === 0 || sNs === false ?
jul._square2dots(sNs.toString(), ':::::').split('.').map(sItem => sItem.replace(/:{5}/g, '.')) : [];
/**
* Makes a dotted path from an array of path segments<br>
* e.g. <code>['a', 'b', 1, 0, 'c'] => 'a.b.1.0.c'</code>
* @param {Array} aSeg An array of path segments
* @returns {String} A dotted (namespace) path
* @private
*/
const dotted = aSeg => jul.trim(aSeg.map(sItem => sItem.toString().replace(rexs.dot, '\\$1')).join('.'), '.', true);
/**
* Gets the intersection of two arrays of path segments
* @param {Array} aNs1 First array to intersect
* @param {Array} aNs2 Second array to intersect
* @returns {Array} Empty array or array of common segments from the beginning
* @private
*/
const intersect = (aNs1, aNs2) => {
const aNs = aNs2.length > aNs1.length ? aNs1 : aNs2;
const aPair = aNs === aNs1 ? aNs2 : aNs1;
const aRes = [];
for (let i = 0; i < aNs.length; i++) {
const sa = aNs[i].toString();
const sb = aPair[i].toString();
const eq = sa === sb;
if (aRes.length && !eq) { return aRes; }
if (eq) { aRes.push(sa); }
}
return aRes;
};
/**
* Compacts a mapping (key:value) object into a tree-like structure
* @param {Object} oMap Key-value object to compact
* @returns {Object} The compacted object
*/
const compact = oMap => {
const oRes = Object.keys(oMap).reduce((oRes, sKey) => {
const oMapVal = oMap[sKey];
if (sKey === '.') {
oRes[sKey] = oMapVal;
return oRes;
}
const aNs1 = segments(sKey);
const nIntersect = Object.keys(oRes).reduce((nIntersect, sItem) => {
if (nIntersect) { return nIntersect; }
if (sItem === '.') { return 0; }
const aNs2 = segments(sItem);
const aIntersect = intersect(aNs2, aNs1);
nIntersect = aIntersect.length;
if (!nIntersect) { return 0; }
const oVal = oRes[sItem];
const aRest1 = aNs1.slice(nIntersect);
const aRest2 = aNs2.slice(nIntersect);
if (aRest1.length || aRest2.length) {
const sCommon = dotted(aIntersect);
if (!oRes[sCommon] || typeof oRes[sCommon] !== 'object') { oRes[sCommon] = {}; }
if (aRest2.length) {
oRes = Object.keys(oRes).reduce((oAcc, s) => {
if (s === sItem) {
oAcc[sCommon] = oRes[sCommon];
oAcc[sCommon][dotted(aRest2)] = oVal;
}
else {
oAcc[s] = oRes[s];
}
return oAcc;
}, {});
}
else {
if (typeof oVal !== 'object') { oRes[sCommon]['.'] = oVal; }
}
if (aRest1.length) { oRes[sCommon][dotted(aRest1)] = oMapVal; }
else { oRes[sCommon][ '.'] = oMapVal; }
}
else {
if (!oVal || typeof oVal !== 'object') { oRes[sItem] = oMapVal; }
else { oRes[sItem]['.'] = oMapVal; }
}
return nIntersect;
}, 0);
if (!nIntersect) { oRes[dotted(aNs1)] = oMapVal; }
return oRes;
}, {'.': '.'});
Object.keys(oRes).forEach(sKey => {
const oVal = oRes[sKey];
if (oVal && typeof oVal === 'object') { oRes[sKey] = compact(oVal); }
else if (typeof oVal === 'string' && oVal !== '.') { oRes[sKey] = jul._square2dots(oVal); }
});
return oRes;
};
/**
* Performs a mapping with a given object from a data schema to another data schema
* @param {Object} oDest The destination object where the mapped values are applied over
* @param {Object} oSrc The source object
* @param {Object} oMap A key-value hash (mapping) between namespace paths in the source and those in the destination
* @param {Object} [oConfig] Optional configuration object with any of the following options:<ul>
* <li><code>uint {RegExp|String}</code> - regular expression or string to match an array index
* placeholder e.g. <code>'$n'</code>. <br>Defaults to <code>/\$[a-z]/</code></li>
* <li><code>prefixProp {String}</code> - name of a property of the mapping the will be used
* as a prefix when computing the destination namespace for the current siblings.
* <br>Defaults to <code>'_mapToPrefix'</code></li>
* <li><code>strict {Boolean}</code> - performs checkings of not overwriting descendant values
* that are already mapped. <br>Defaults to <code>false</code></li>
* </ul>
* @returns {Object} The destination object
*/
const mapper = (oDest, oSrc, oMap, oConfig) => {
oConfig = oConfig || {};
const bStart = !oConfig._level;
if (bStart) { oConfig = Object.assign({}, oConfig); }
oConfig.uint = oConfig.uint || /\$[a-z]/;
if (typeof oConfig.uint !== 'object') {
oConfig.uint = new RegExp(oConfig.uint.toString().split('').map(c => {
return '\\x' + c.charCodeAt(0).toString(16);
}).join(''));
}
oConfig.prefixProp = oConfig.prefixProp || '_mapToPrefix';
oConfig.strict = oConfig.strict || false;
oConfig._level = oConfig._level || {
instSrc: jul.instance({nsRoot: oSrc}),
instDest: jul.instance({nsRoot: oDest}),
indexes: [],
depth: 0,
splits: 0,
hash: []
};
const oLevel = oConfig._level;
if (typeof oMap['.'] === 'undefined') { oMap = compact(oMap); }
const oDotVal = oMap['.'];
const aIx = oDotVal && typeof oDotVal === 'object' ? oDotVal : null;
const bKeep = oConfig.strict && !aIx && typeof oDotVal !== 'undefined';
const sMapPrefix = oMap[oConfig.prefixProp];
if (!aIx) { oMap = Object.assign({}, oMap); }
delete oMap[oConfig.prefixProp];
if (oConfig.strict || aIx) { delete oMap['.']; }
const aKeys = aIx ? aIx[1] : Object.keys(oMap);
if (bKeep) {
oMap['.'] = oDotVal;
aKeys.push('.');
}
aKeys.forEach((sKey, ix) => {
const oMapVal = typeof oMap[sKey] !== 'object' ? jul.trim(!sMapPrefix || sKey === '.' ?
oMap[sKey].toString() : sMapPrefix + '.' + oMap[sKey], '.', true) : oMap[sKey];
if (!oMapVal) { return; }
if (aIx) { oLevel.indexes[oLevel.depth - 1] = aIx[0][ix]; }
if (oLevel.splits !== 1 && (oLevel.splits || oConfig.uint.test(sKey))) {
const aRex = sKey.split(oConfig.uint);
const sParent = dotted(segments(aRex[0]).filter(sItem => sItem));
const oTo = oLevel.instSrc.get(sParent);
if (!oTo || typeof oTo !== 'object') { return; }
const aIv = [[], []];
const oNewMap = {};
const aIterate = typeof oTo.keys === 'function' ? oTo.keys() : Object.keys(oTo);
for (const i of aIterate) {
const x = i.toString().replace(rexs.dot, '\\$1');
const s = sKey.replace(oConfig.uint, x);
aIv[0].push(x);
aIv[1].push(s);
oNewMap[s] = oMapVal;
}
oNewMap['.'] = aIv;
oLevel.splits = aRex.length - 1;
oLevel.depth++;
oLevel.indexes.length = oLevel.depth;
mapper(oDest, oSrc, oNewMap, oConfig);
oLevel.depth--;
oLevel.splits = 0;
}
else {
oSrc = oLevel.instSrc.get(jul.trim(sKey, '.', true));
if (typeof oSrc === 'undefined') { return; }
if (typeof oMapVal === 'object') {
oLevel.splits = 0;
const oInst = oLevel.instSrc;
oLevel.instSrc = jul.instance({nsRoot: oSrc});
mapper(oDest, oSrc, oMapVal, oConfig);
oLevel.instSrc = oInst;
}
else {
if (oConfig.strict) {
const aPath = segments(oMapVal).map(sItem => oConfig.uint.test(sItem) ? '$$n' : sItem);
for (var j = 0; j < oLevel.hash.length; j++) {
const aItem = oLevel.hash[j];
const aIntersect = intersect(aPath, aItem);
if (aIntersect.length === aPath.length && aIntersect.length !== aItem.length) { return; }
else if (aIntersect.length === aPath.length && aPath.length === aItem.length) { j = oLevel.hash.length + 1; }
else if (aIntersect.length === aItem.length && aIntersect.length !== aPath.length) {
oLevel.hash[j] = aPath;
j = oLevel.hash.length + 1;
}
}
if (j === oLevel.hash.length) { oLevel.hash.push(aPath); }
}
const sMapTo = !oLevel.depth ? oMapVal :
oLevel.indexes.reduce((sNs, n) => {
return sNs.replace(oConfig.uint, n);
}, oMapVal);
oLevel.instDest.ns(sMapTo, oSrc);
}
}
});
if (bStart) {
oLevel.hash.length = 0;
oLevel.indexes.length = 0;
}
return oDest;
};
module.exports = {mapper, compact};