Select Git revision
buildmap.js

Volker Schukai authored
buildmap.js 12.38 KiB
'use strict';
/**
* @author schukai GmbH
*/
import {assignToNamespace, Monster} from '../namespace.js';
import {isFunction, isObject, isString} from "../types/is.js";
import {validateString} from "../types/validate.js";
import {clone} from "../util/clone.js";
import {DELIMITER, Pathfinder, WILDCARD} from "./pathfinder.js";
/**
* With the help of the function `buildMap()`, maps can be easily created from data objects.
*
* Either a simple definition `a.b.c` or a template `${a.b.c}` can be specified as the path.
* Key and value can be either a definition or a template. The key does not have to be defined.
*
* You can call the method via the monster namespace `Monster.Data.buildMap()`.
*
* ```
* <script type="module">
* import {Monster} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.18.0/dist/modules/data/buildmap.js';
* console.log(Monster.Data.buildMap())
* </script>
* ```
*
* Alternatively, you can also integrate this function individually.
*
* ```
* <script type="module">
* import {buildMap} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.18.0/dist/modules/data/buildmap.js';
* console.log(buildMap())
* </script>
* ```
*
* The templates determine the appearance of the keys and the value of the map. Either a single value `id` can be taken or a composite key `${id} ${name}` can be used.
*
* If you want to access values of the parent data set, you have to use the `^` character `${id} ${^.name}`.
*
* @example
*
* import {buildMap} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.18.0/dist/modules/data/buildmap.js';
* // a typical data structure as reported by an api
*
* let map;
* let obj = {
* "data": [
* {
* "id": 10,
* "name": "Cassandra",
* "address": {
* "street": "493-4105 Vulputate Street",
* "city": "Saumur",
* "zip": "52628"
* }
* },
* {
* "id": 20,
* "name": "Holly",
* "address": {
* "street": "1762 Eget Rd.",
* "city": "Schwalbach",
* "zip": "952340"
* }
* },
* {
* "id": 30,
* "name": "Guy",
* "address": {
* "street": "957-388 Sollicitudin Avenue",
* "city": "Panchià",
* "zip": "420729"
* }
* }
* ]
* };
*
* // The function is passed this data structure and with the help of the selector `'data.*'` the data to be considered are selected.
* // The key is given by a simple definition `'id'` and the value is given by a template `'${name} (${address.zip} ${address.city})'`.
* map = buildMap(obj, 'data.*', '${name} (${address.zip} ${address.city})', 'id');
* console.log(map);
*
* // ↦ Map(3) {
* // '10' => 'Cassandra (52628 Saumur)',
* // '20' => 'Holly (952340 Schwalbach)',
* // '30' => 'Guy (420729 Panchià)'
* // }
*
* // If no key is specified, the key from the selection, here the array index, is taken.
* map = buildMap(obj, 'data.*', '${name} (${address.zip} ${address.city})');
* console.log(map);
*
* // ↦ Map(3) {
* // '0' => 'Cassandra (52628 Saumur)',
* // '1' => 'Holly (952340 Schwalbach)',
* // '2' => 'Guy (420729 Panchià)'
* // }
*
* // a filter (function(value, key) {}) can be specified to accept only defined entries.
* map = buildMap(obj, 'data.*', '${name} (${address.zip} ${address.city})', 'id', function (value, key) {
* return (value['id'] >= 20) ? true : false
* });
* console.log(map);
*
* // ↦ Map(2) {
* // 20 => 'Holly (952340 Schwalbach)',
* // 30 => 'Guy (420729 Panchià)'
* // }
*
* @param {*} subject
* @param {string|Monster.Data~exampleSelectorCallback} selector
* @param {string} [valueTemplate]
* @param {string} [keyTemplate]
* @param {Monster.Data~exampleFilterCallback} [filter]
* @return {*}
* @memberOf Monster.Data
* @throws {TypeError} value is neither a string nor a function
* @throws {TypeError} the selector callback must return a map
*/
function buildMap(subject, selector, valueTemplate, keyTemplate, filter) {
const result = new Map
let map;
if (isFunction(selector)) {
map = selector(subject)
if (!(map instanceof Map)) {
throw new TypeError('the selector callback must return a map');
}
} else if (isString(selector)) {
map = new Map;
buildFlatMap.call(map, subject, selector);
} else {
throw new TypeError('value is neither a string nor a function')
}
if (!(map instanceof Map)) {
return result;
}
map.forEach((v, k, m) => {
if (isFunction(filter)) {
if (filter.call(m, v, k) !== true) return;
}
k = build(v, keyTemplate, k);
v = build(v, valueTemplate);
result.set(k, v);
});
return result;
}
/**
* @private
* @param subject
* @param selector
* @param key
* @param parentMap
* @return {*}
*/
function buildFlatMap(subject, selector, key, parentMap) {
const result = this;
const currentMap = new Map;
const resultLength = result.size;
if (key === undefined) key = [];
let parts = selector.split(DELIMITER);
let current = "", currentPath = [];
do {
current = parts.shift();
currentPath.push(current);
if (current === WILDCARD) {
let finder = new Pathfinder(subject);
let map
try {
map = finder.getVia(currentPath.join(DELIMITER));
} catch (e) {
let a = e;
map = new Map();
}
for (const [k, o] of map) {
let copyKey = clone(key);
currentPath.map((a) => {
copyKey.push((a === WILDCARD) ? k : a)
})
let kk = copyKey.join(DELIMITER);
let sub = buildFlatMap.call(result, o, parts.join(DELIMITER), copyKey, o);
if (isObject(sub)) {
sub['^'] = parentMap;
}
currentMap.set(kk, sub);
}
}
} while (parts.length > 0);
// no set in child run
if (resultLength === result.size) {
for (const [k, o] of currentMap) {
result.set(k, o);
}
}
return subject;
}
/**
* With the help of this filter callback, values can be filtered out. Only if the filter function returns true, the value is taken for the map.
*
* @callback Monster.Data~exampleFilterCallback
* @param {*} value Value
* @param {string} key Key
* @memberOf Monster.Data
* @see Monster.Data.buildMap
*/
/**
* Alternatively to a string selector a callback can be specified. this must return a map.
*
* @example
* import {buildMap} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.18.0/dist/modules/data/buildmap.js';
*
* let obj = {
* "data": [
* {
* "id": 10,
* "name": "Cassandra",
* "enrichment": {
* variants: [
* {
* sku: 1, label: "XXS", price: [
* {vk: '12.12 €'},
* {vk: '12.12 €'}
* ]
* },
* {
* sku: 2, label: "XS", price: [
* {vk: '22.12 €'},
* {vk: '22.12 €'}
* ]
* },
* {
* sku: 3, label: "S", price: [
* {vk: '32.12 €'},
* {vk: '32.12 €'}
* ]
* },
* {
* sku: 4, label: "L", price: [
* {vk: '42.12 €'},
* {vk: '42.12 €'}
* ]
* }
* ]
*
* }
* },
* {
* "id": 20,
* "name": "Yessey!",
* "enrichment": {
* variants: [
* {
* sku: 1, label: "XXS", price: [
* {vk: '12.12 €'},
* {vk: '12.12 €'}
* ]
* },
* {
* sku: 2, label: "XS", price: [
* {vk: '22.12 €'},
* {vk: '22.12 €'}
* ]
* },
* {
* sku: 3, label: "S", price: [
* {vk: '32.12 €'},
* {vk: '32.12 €'}
* ]
* },
* {
* sku: 4, label: "L", price: [
* {vk: '42.12 €'},
* {vk: '42.12 €'}
* ]
* }
* ]
*
* }
* }
* ]
* };
*
* let callback = function (subject) {
* let m = new Map;
*
* for (const [i, b] of Object.entries(subject.data)) {
*
* let key1 = i;
*
* for (const [j, c] of Object.entries(b.enrichment.variants)) {
* let key2 = j;
*
* for (const [k, d] of Object.entries(c.price)) {
*
* let key3 = k;
*
* d.name = b.name;
* d.label = c.label;
* d.id = [key1, key2, key3].join('.');
*
* m.set(d.id, d);
* }
*
* }
* }
* return m;
* }
*
* let map = buildMap(obj, callback, '${name} ${vk}', '${id}')
*
* // ↦ Map(3) {
* // "0.0.0":"Cassandra 12.12 €",
* // "0.0.1":"Cassandra 12.12 €",
* // "0.1.0":"Cassandra 22.12 €",
* // "0.1.1":"Cassandra 22.12 €",
* // "0.2.0":"Cassandra 32.12 €",
* // "0.2.1":"Cassandra 32.12 €",
* // "0.3.0":"Cassandra 42.12 €",
* // "0.3.1":"Cassandra 42.12 €",
* // "1.0.0":"Yessey! 12.12 €",
* // "1.0.1":"Yessey! 12.12 €",
* // "1.1.0":"Yessey! 22.12 €",
* // "1.1.1":"Yessey! 22.12 €",
* // "1.2.0":"Yessey! 32.12 €",
* // "1.2.1":"Yessey! 32.12 €",
* // "1.3.0":"Yessey! 42.12 €",
* // "1.3.1":"Yessey! 42.12 €"
* // }
*
* @callback Monster.Data~exampleSelectorCallback
* @param {*} subject subject
* @return Map
* @since 1.17.0
* @memberOf Monster.Data
* @see Monster.Data.buildMap
*/
/**
* @private
* @param {*} subject
* @param {string|undefined} definition
* @param {*} defaultValue
* @return {*}
*/
function build(subject, definition, defaultValue) {
if (definition === undefined) return defaultValue ? defaultValue : subject;
validateString(definition);
const regexp = /(?<placeholder>\${(?<path>[a-z\^A-Z.\-_0-9]*)})/gm
const array = [...definition.matchAll(regexp)];
let finder = new Pathfinder(subject);
if (array.length === 0) {
return finder.getVia(definition);
}
array.forEach((a) => {
let groups = a?.['groups'];
let placeholder = groups?.['placeholder']
if (placeholder === undefined) return;
let path = groups?.['path']
let v = finder.getVia(path);
if (v === undefined) v = defaultValue;
definition = definition.replaceAll(placeholder, v);
})
return definition;
}
assignToNamespace('Monster.Data', buildMap);
export {Monster, buildMap}