data/pathfinder.js

'use strict';

/**
 * @author schukai GmbH
 */

import {Monster} from '../namespace.js';
import {isObject, isArray, isInteger} from '../types/is.js';
import {validateString, validateInteger} from '../types/validate.js';
import {Base} from '../types/base.js';
import {Stack} from "../types/stack.js";

/**
 * path separator
 *
 * @private
 * @type {string}
 */
const DELIMITER = '.';

/**
 * Pathfinder class
 *
 * you can call the method via the monster namespace `new Monster.Data.Pathfinder()`.
 *
 * ```
 * <script type="module">
 * import {Monster} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.5.0/dist/modules/data/pathfinder.js';
 * console.log(new Monster.Data.Pathfinder())
 * </script>
 * ```
 *
 * Alternatively, you can also integrate this function individually.
 *
 * ```
 * <script type="module">
 * import {Pathfinder} from 'https://cdn.jsdelivr.net/npm/@schukai/monster@1.5.0/dist/modules/data/pathfinder.js';
 * console.log(new Pathfinder())
 * </script>
 * ```
 *
 * With the help of the pathfinder, values can be read and written from an object construct.
 *
 * ```
 * new Pathfinder({
 * a: {
 *     b: {
 *         f: [
 *             {
 *                 g: false,
 *             }
 *         ],
 *     }
 * }
 * }).getVia("a.b.f.0.g"); // ↦ false
 * ```
 *
 * if a value is not present or has the wrong type, a corresponding exception is thrown.
 *
 * ```
 * new Pathfinder({}).getVia("a.b.f.0.g"); // ↦ Error
 * ```
 *
 * The `Pathfinder.exists()` method can be used to check whether access to the path is possible.
 *
 * ```
 * new Pathfinder({}).exists("a.b.f.0.g"); // ↦ false
 * ```
 *
 * pathfinder can also be used to build object structures. to do this, the `Pathfinder.setVia()` method must be used.
 *
 * ```
 * obj = {};
 * new Pathfinder(obj).setVia('a.b.0.c', true); // ↦ {a:{b:[{c:true}]}}
 * ```
 *
 * @since 1.4.0
 * @copyright schukai GmbH
 * @memberOf Monster/Data
 */
class Pathfinder extends Base {

    /**
     * @param {array|object|Map|Set} value
     * @since 1.4.0
     **/
    constructor(object) {
        super();
        this.object = object;
    }

    /**
     *
     * @param {string} path
     * @since 1.4.0
     * @returns {*}
     * @throws {TypeError} unsupported type
     * @throws {Error} the journey is not at its end
     * @throws {TypeError} value is not a string
     * @throws {TypeError} value is not an integer
     */
    getVia(path) {
        validateString(path);
        return getValueViaPath(this.object, path);
    }

    /**
     *
     * @param {string} path
     * @param {*} value
     * @returns {Pathfinder}
     * @since 1.4.0
     * @throws {TypeError} unsupported type
     * @throws {TypeError} value is not a string
     * @throws {TypeError} value is not an integer
     */
    setVia(path, value) {
        validateString(path);
        setValueViaPath(this.object, path, value);
        return this;
    }

    /**
     *
     * @param {string} path
     * @return {bool}
     * @throws {TypeError} unsupported type
     * @throws {TypeError} value is not a string
     * @throws {TypeError} value is not an integer
     * @since 1.4.0
     */
    exists(path) {
        validateString(path);
        try {
            getValueViaPath(this.object, path, true);
            return true;
        } catch (e) {

        }

        return false;
    }

}

Monster.assignToNamespace('Monster.Data', Pathfinder);
export {Monster, Pathfinder}

/**
 *
 * @param {*} object
 * @param [string} path
 * @param [boolean} check 
 * @returns {*}
 * @throws {TypeError} unsupported type
 * @throws {Error} the journey is not at its end
 */
function getValueViaPath(object, path, check) {

    if (path === "") {
        return object;
    }

    let parts = path.split(DELIMITER)
    let current = parts.shift();

    if (isObject(object) || isArray(object)) {

        let anchor;
        if (object instanceof Map) {
            anchor = object.get(current);
        } else if (object instanceof Set) {
            current = parseInt(current);
            validateInteger(current)
            anchor = [...object]?.[current];
        } else if (isArray(object)) {
            current = parseInt(current);
            validateInteger(current)
            anchor = object?.[current];
        } else {
            anchor = object?.[current];
        }

        if (isObject(anchor) || isArray(anchor)) {
            return getValueViaPath(anchor, parts.join(DELIMITER), check)
        }

        if (parts.length > 0) {
            throw Error("the journey is not at its end (" + parts.join(DELIMITER) + ")");
        }

        if (check === true && !object.hasOwnProperty(current)) {
            throw Error('unknown value');
        }

        return anchor;

    }

    throw TypeError("unsupported type")

}

/**
 *
 * @param object
 * @param path
 * @param value
 * @returns {undefined|*}
 * @throws {TypeError} unsupported type
 * @throws {TypeError} unsupported type
 * @throws {Error} the journey is not at its end
 */
function setValueViaPath(object, path, value) {

    let parts = path.split(DELIMITER)
    let last = parts.pop();
    let subpath = parts.join(DELIMITER);

    let stack = new Stack()
    let current = subpath;
    while (true) {

        try {
            getValueViaPath(object, current, true)
            break;
        } catch (e) {

        }

        stack.push(current);
        parts.pop();
        current = parts.join(DELIMITER);

        if (current === "") break;
    }

    while (!stack.isEmpty()) {
        current = stack.pop();
        let obj = {};

        if (!stack.isEmpty()) {
            let n = stack.peek().split(DELIMITER).pop();
            if (isInteger(parseInt(n))) {
                obj = [];
            }

        }


        setValueViaPath(object, current, obj);
    }

    let anchor = getValueViaPath(object, subpath);

    if (!isObject(object) && !isArray(object)) {
        throw TypeError("unsupported type: " + typeof object);
    }

    if (anchor instanceof Map) {
        anchor.set(last, value);
    } else if (anchor instanceof Set) {
        anchor.append(value)
    } else if (isArray(anchor)) {
        last = parseInt(last);
        validateInteger(last)
        anchor[last] = value;
    } else {
        anchor[last] = value;
    }

    return anchor;

}