Something went wrong on our end
Select Git revision
target-init-go-utilities.mk
-
Volker Schukai authoredVolker Schukai authored
buildmap.mjs 12.88 KiB
/**
* Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
* Node module: @schukai/monster
*
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
*
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
* For more information about purchasing a commercial license, please contact schukai GmbH.
*
* SPDX-License-Identifier: AGPL-3.0
*/
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 = "^";
/**
* Maps can be easily created from data objects with the help of the function `buildMap()`.
*
* The path can be specified as either a simple definition a.b.c or a template ${a.b.c}.
* 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 the values of the parent data set, you have to use the ^ character, for example, ${id} ${^.name}.
*
* @externalExample ../../example/data/buildmap.mjs
*
* @param {*} subject - The data object from which the map will be created
* @param {string|Monster.Data~exampleSelectorCallback} selector - The path to the data object, or a callback that returns a map.
* @param {string} [valueTemplate] - A template for the value of the map.
* @param {string} [keyTemplate] - A template for the key of the map.
* @param {Monster.Data~exampleFilterCallback} [filter] - A callback function to filter out values.
* @return {*} - The created map.
* @memberOf Monster.Data
* @throws {TypeError} - If the value is neither a string nor a function.
* @throws {TypeError} - If the selector callback does not 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);
});
}
/**
* The assembleParts function is a private function that helps in building a map from a subject object based on a provided
* selector. The selector can either be a string or a callback function. This function is meant to be used as a
* helper function by other functions in the module.
*
* The function takes four parameters:
*
* subject: The subject object from which the map is to be built
* selector: The selector to determine the structure of the map. It can be a string or a callback function.
* filter (optional): A callback function that can be used to filter values based on some criteria.
* callback: A function to be called for each element in the map.
* If the selector parameter is a callback function, it is executed passing the subject as its argument,
* and the resulting value must be an instance of Map. Otherwise, if the selector parameter is a string,
* buildFlatMap is called to build a flat map with keys and values extracted from the subject object based on the selector.
*
* If the filter parameter is provided, it will be used to filter out certain elements from the map, based on some
* criteria. The callback will be passed the value, key, and map object, and if it returns false, the element will be skipped.
*
* For each element in the map, the callback function is called with the following parameters:
*
* v: The value of the element
* k: The key of the element
* m: The map object
* The function returns a new map with the processed values. If map is not an instance of Map, an empty map will be returned.
*
* Example Usage:
*
* ```javascript
* const obj = {
* name: "John",
* age: 30,
* address: {
* city: "New York",
* state: "NY",
* country: "USA",
* },
* };
*
* const selector = "address";
*
* const map = assembleParts(obj, selector, null, function (v, k, m) {
* this.set(k, v);
* });
*
* console.log(map);
* // Output: Map(3) {
* // "address.city" => "New York",
* // "address.state" => "NY",
* // "address.country" => "USA"
* // }
* ```
*
*
* @private
* @param {*} subject - The subject object from which the map is to be built.
* @param {string|Monster.Data~exampleSelectorCallback} selector - The selector to determine the structure of the map. It can be a string or a callback function.
* @param {Monster.Data~exampleFilterCallback} [filter] - A callback function that can be used to filter values based on some criteria.
* @param {function} callback - A function to be called for each element in the map.
* @return {Map} - A new map with the processed values.
* @throws {TypeError} - When selector is neither a string nor a function.
* @memberOf Monster.Data
*/
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 currentMap = new Map();
const resultLength = this.size;
if (key === undefined) key = [];
const parts = selector.split(DELIMITER);
let current = "";
const currentPath = [];
do {
current = parts.shift();
currentPath.push(current);
if (current === WILDCARD) {
const finder = new Pathfinder(subject);
let map;
try {
map = finder.getVia(currentPath.join(DELIMITER));
} catch (e) {
const a = e;
map = new Map();
}
for (const [k, o] of map) {
const copyKey = clone(key);
currentPath.map((a) => {
copyKey.push(a === WILDCARD ? k : a);
});
const kk = copyKey.join(DELIMITER);
const sub = buildFlatMap.call(
this,
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 === this.size) {
for (const [k, o] of currentMap) {
this.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)];
const finder = new Pathfinder(subject);
if (array.length === 0) {
return finder.getVia(definition);
}
array.forEach((a) => {
const groups = a?.["groups"];
const placeholder = groups?.["placeholder"];
if (placeholder === undefined) return;
const path = groups?.["path"];
let v = finder.getVia(path);
if (v === undefined) v = defaultValue;
definition = definition.replaceAll(placeholder, v);
});
return definition;
}