Skip to content
Snippets Groups Projects
Select Git revision
  • 6307dd3c11ac630394f26639f3b78ee809d51e4a
  • master default protected
  • 1.31
  • 4.38.7
  • 4.38.6
  • 4.38.5
  • 4.38.4
  • 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
23 results

buildmap.js

Blame
  • Volker Schukai's avatar
    Volker Schukai authored
    6307dd3c
    History
    buildmap.js 12.38 KiB
    'use strict';
    
    /**
     * @author schukai GmbH
     */
    
    import {assignToNamespace, Monster} from '../namespace.js';
    import {isFunction, isObject, isString} from "../types/is.js";
    import {validateString} from "../types/validate.js";
    import {clone} from "../util/clone.js";
    import {DELIMITER, Pathfinder, WILDCARD} from "./pathfinder.js";
    
    /**
     * 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.
     *
     * You can call the method via the monster namespace `Monster.Data.buildMap()`.
     *
     * ```
     * <script type="module">
     * import {Monster} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.18.0/dist/modules/data/buildmap.js';
     * console.log(Monster.Data.buildMap())
     * </script>
     * ```
     *
     * Alternatively, you can also integrate this function individually.
     *
     * ```
     * <script type="module">
     * import {buildMap} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.18.0/dist/modules/data/buildmap.js';
     * 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@1.18.0/dist/modules/data/buildmap.js';
     * // 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) {
    
        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('value 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;
            }
    
            k = build(v, keyTemplate, k);
            v = build(v, valueTemplate);
    
            result.set(k, v);
        });
    
        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)) {
                        sub['^'] = 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 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@1.18.0/dist/modules/data/buildmap.js';
     *
     * 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 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;
    
    }
    
    
    assignToNamespace('Monster.Data', buildMap);
    export {Monster, buildMap}