Skip to content
Snippets Groups Projects
Select Git revision
  • b96c4d10615d673c51c26197d23bb26285598040
  • master default protected
  • 1.2.4
  • 1.2.3
  • 1.2.2
  • 1.2.1
  • 1.2.0
  • v1.1.0
8 results

target-init-go-lib.mk

Blame
  • 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;
    }