UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

458 lines (404 loc) 10.7 kB
/** * 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 { Base } from "../types/base.mjs"; import { isArray, isInteger, isObject, isPrimitive, isString, } from "../types/is.mjs"; import { Stack } from "../types/stack.mjs"; import { validateInteger, validateBoolean, validateString, } from "../types/validate.mjs"; export { Pathfinder, DELIMITER, WILDCARD }; /** * path separator * * @private * @type {string} */ const DELIMITER = "."; /** * @private * @type {string} */ const WILDCARD = "*"; /** * Pathfinder is a class to find a path to an object. * * 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}]}} * ``` * * @example /examples/libraries/pathfinder/example-1/ Example 1 * @example /examples/libraries/pathfinder/example-2/ Example 2 * * @license AGPLv3 * @since 1.4.0 * @copyright schukai GmbH * @summary Pathfinder is a class to find a path to an object. */ class Pathfinder extends Base { /** * Creates a new instance of the constructor. * * @param {object} object - The object parameter for the constructor. * * @throws {Error} Throws an error if the provided object parameter is a simple type. */ constructor(object) { super(); if (isPrimitive(object)) { throw new Error("the parameter must not be a simple type"); } this.object = object; this.wildCard = WILDCARD; } /** * set wildcard * * @param {string} wildcard * @return {Pathfinder} * @since 1.7.0 */ setWildCard(wildcard) { validateString(wildcard); this.wildCard = wildcard; return this; } /** * * @param {string|array} path * @since 1.4.0 * @return {*} * @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 * @throws {Error} unsupported action for this data type */ getVia(path) { return getValueViaPath.call(this, this.object, path); } /** * * @param {string|array} path * @param {*} value * @return {Pathfinder} * @since 1.4.0 * @throws {TypeError} unsupported type * @throws {TypeError} value is not a string * @throws {TypeError} value is not an integer * @throws {Error} unsupported action for this data type */ setVia(path, value) { setValueViaPath.call(this, this.object, path, value); return this; } /** * Delete Via Path * * @param {string|array} path * @return {Pathfinder} * @since 1.6.0 * @throws {TypeError} unsupported type * @throws {TypeError} value is not a string * @throws {TypeError} value is not an integer * @throws {Error} unsupported action for this data type */ deleteVia(path) { deleteValueViaPath.call(this, this.object, path); return this; } /** * * @param {string|array} 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) { try { getValueViaPath.call(this, this.object, path, true); return true; } catch (e) {} return false; } } /** * * @param {*} subject * @param {string|array} path * @param {boolean} check * @return {Map} * @throws {TypeError} unsupported type * @throws {Error} the journey is not at its end * @throws {Error} unsupported action for this data type * @private */ function iterate(subject, path, check) { if (check === undefined) { check = false; } validateBoolean(check); const result = new Map(); if (isArray(path)) { path = path.join(DELIMITER); } if (isObject(subject) || isArray(subject)) { for (const [key, value] of Object.entries(subject)) { result.set(key, getValueViaPath.call(this, value, path, check)); } } else { const key = path.split(DELIMITER).shift(); result.set(key, getValueViaPath.call(this, subject, path, check)); } return result; } /** * * @param subject * @param path * @param check * @return {V|*|Map} * @throws {TypeError} unsupported type * @throws {Error} the journey is not at its end * @throws {Error} unsupported action for this data type */ function getValueViaPath(subject, path, check) { if (check === undefined) { check = false; } validateBoolean(check); if (!(isArray(path) || isString(path))) { throw new Error( "type error: a path must be a string or an array in getValueViaPath", ); } let parts; if (isString(path)) { if (path === "") { return subject; } parts = path.split(DELIMITER); } let current = parts.shift(); if (current === this.wildCard) { return iterate.call(this, subject, parts.join(DELIMITER), check); } if (isObject(subject) || isArray(subject)) { let anchor; if (subject instanceof Map || subject instanceof WeakMap) { anchor = subject.get(current); } else if (subject instanceof Set || subject instanceof WeakSet) { current = parseInt(current); validateInteger(current); anchor = [...subject]?.[current]; } else if (typeof WeakRef === "function" && subject instanceof WeakRef) { throw Error("unsupported action for this data type (WeakRef)"); } else if (isArray(subject)) { current = parseInt(current); validateInteger(current); anchor = subject?.[current]; } else { anchor = subject?.[current]; } if (isObject(anchor) || isArray(anchor)) { return getValueViaPath.call(this, anchor, parts.join(DELIMITER), check); } if (parts.length > 0) { throw Error(`the journey is not at its end (${parts.join(DELIMITER)})`); } if (check === true) { const descriptor = Object.getOwnPropertyDescriptor( Object.getPrototypeOf(subject), current, ); if (!subject.hasOwnProperty(current) && descriptor === undefined) { throw Error("unknown value " + current); } } return anchor; } throw TypeError(`unsupported type ${typeof subject} for path ${path}`); } /** * * @param {object} subject * @param {string|array} path * @param {*} value * @return {void} * @throws {TypeError} unsupported type * @throws {TypeError} unsupported type * @throws {Error} the journey is not at its end * @throws {Error} unsupported action for this data type * @private */ function setValueViaPath(subject, path, value) { if (!(isArray(path) || isString(path))) { throw new Error("type error: a path must be a string or an array"); } let parts; if (isArray(path)) { if (path.length === 0) { return; } parts = path; } else { parts = path.split(DELIMITER); } let last = parts.pop(); const subpath = parts.join(DELIMITER); const stack = new Stack(); let current = subpath; while (true) { try { getValueViaPath.call(this, subject, 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()) { const n = stack.peek().split(DELIMITER).pop(); if (isInteger(parseInt(n))) { obj = []; } } setValueViaPath.call(this, subject, current, obj); } const anchor = getValueViaPath.call(this, subject, subpath); if (!(isObject(subject) || isArray(subject))) { throw TypeError(`unsupported type: ${typeof subject} in setValueViaPath`); } if (anchor instanceof Map || anchor instanceof WeakMap) { anchor.set(last, value); } else if (anchor instanceof Set || anchor instanceof WeakSet) { anchor.append(value); } else if (typeof WeakRef === "function" && anchor instanceof WeakRef) { throw Error("unsupported action for this data type in setValueViaPath"); } else if (isArray(anchor)) { last = parseInt(last); validateInteger(last); assignProperty(anchor, "" + last, value); } else { assignProperty(anchor, last, value); } } /** * @private * @param {object} object * @param {string} key * @param {*} value */ function assignProperty(object, key, value) { if (!object.hasOwnProperty(key)) { object[key] = value; return; } if (value === undefined) { delete object[key]; } object[key] = value; } /** * * @param {object} subject * @param {string} path * @return {void} * @throws {TypeError} unsupported type * @throws {TypeError} unsupported type * @throws {Error} the journey is not at its end * @throws {Error} unsupported action for this data type * @license AGPLv3 * @since 1.6.0 * @private */ function deleteValueViaPath(subject, path) { if (!(isArray(path) || isString(path))) { throw new Error( "type error: a path must be a string or an array in deleteValueViaPath", ); } let parts; if (isArray(path)) { if (path.length === 0) { return; } parts = path; } else { parts = path.split(DELIMITER); } let last = parts.pop(); const subPath = parts.join(DELIMITER); const anchor = getValueViaPath.call(this, subject, subPath); if (anchor instanceof Map) { anchor.delete(last); } else if ( anchor instanceof Set || anchor instanceof WeakMap || anchor instanceof WeakSet || (typeof WeakRef === "function" && anchor instanceof WeakRef) ) { throw Error("unsupported action for this data type in deleteValueViaPath"); } else if (isArray(anchor)) { last = parseInt(last); validateInteger(last); delete anchor[last]; } else { delete anchor[last]; } }