Select Git revision
mermaid-init.js
pathfinder.mjs 10.42 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 { 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];
}
}