Select Git revision
buildmap.mjs

Volker Schukai authored
buildmap.mjs 10.31 KiB
/**
* Copyright schukai GmbH and contributors 2022. All Rights Reserved.
* Node module: @schukai/monster
* This file is licensed under the AGPLv3 License.
* License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
*/
import { isFunction, isObject, isString } from "../types/is.mjs";
import { validateString } from "../types/validate.mjs";
import { clone } from "../util/clone.mjs";
import { DELIMITER, Pathfinder, WILDCARD } from "./pathfinder.mjs";
export { buildMap, PARENT, assembleParts };
/**
* @type {string}
* @memberOf Monster.Data
*/
const PARENT = "^";
/**
* 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.
*
* 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}`.
*
* @externalExample ../../example/data/buildmap.mjs
* @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) {
return assembleParts(subject, selector, filter, function (v, k, m) {
k = build(v, keyTemplate, k);
v = build(v, valueTemplate);
this.set(k, v);
});
}
/**
* @private
* @param {*} subject
* @param {string|Monster.Data~exampleSelectorCallback} selector
* @param {Monster.Data~exampleFilterCallback} [filter]
* @param {function} callback
* @return {Map}
* @throws {TypeError} selector is neither a string nor a function
*/
function assembleParts(subject, selector, filter, callback) {
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("selector 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;
}
callback.call(result, v, k, m);
});
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 = "";
let 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) && parentMap !== undefined) {
sub[PARENT] = 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 {@link Monster.Data.buildMap}
*/
/**
* Alternatively to a string selector a callback can be specified. this must return a map.
*
* @example
* import {buildMap} from '@schukai/monster/source/data/buildmap.mjs';
*
* 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
* @license AGPLv3
* @since 1.17.0
* @memberOf Monster.Data
* @see {@link 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;
}