Skip to content
Snippets Groups Projects
Select Git revision
  • 0f105d329bdeb3947f58daa2c61b0f94ad09a5fc
  • master default protected
  • 1.31
  • 4.38.3
  • 4.38.2
  • 4.38.1
  • 4.38.0
  • 4.37.2
  • 4.37.1
  • 4.37.0
  • 4.36.0
  • 4.35.0
  • 4.34.1
  • 4.34.0
  • 4.33.1
  • 4.33.0
  • 4.32.2
  • 4.32.1
  • 4.32.0
  • 4.31.0
  • 4.30.1
  • 4.30.0
  • 4.29.1
23 results

buildmap.mjs

Blame
  • Volker Schukai's avatar
    0f105d32
    History
    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;
    }