/** * @author schukai GmbH */ 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. * * ``` * <script type="module"> * import {buildMap} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@latest/source/data/buildmap.mjs'; * 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@latest/source/data/buildmap.mjs'; * // 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) { 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 = "", 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 'https://cdn.jsdelivr.net/npm/@schukai/monster@latest/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 * @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; }