thi-ng/umbrella

View on GitHub
packages/paths/src/path.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import type { NumOrString, Path } from "@thi.ng/api";
import { isArray } from "@thi.ng/checks/is-array";
import { isNumber } from "@thi.ng/checks/is-number";
import { isProtoPath } from "@thi.ng/checks/is-proto-path";
import { isString } from "@thi.ng/checks/is-string";
import { illegalArgs } from "@thi.ng/errors/illegal-arguments";

/**
 * Converts the given key path to canonical form (array).
 *
 * @remarks
 * If given path is an array, performs a safety check to ensure that all path
 * items are strings or numbers and that illegal paths like `[["__proto__"],
 * "foo"]` will be disallowed (throws an error).
 *
 * Also see {@link disallowProtoPath}.
 *
 * ```ts tangle:../export/to-path.ts
 * import { toPath } from "@thi.ng/paths";
 *
 * console.log(toPath("a.b.c"));
 * // ["a", "b", "c"]
 *
 * console.log(toPath(0));
 * // [0]
 *
 * console.log(toPath(["a", "b", "c"]));
 * // ["a", "b", "c"]
 * ```
 *
 * @param path -
 */
export const toPath = (path: Path): readonly NumOrString[] => {
    if (isArray(path)) {
        if (!path.every((x) => isString(x) || isNumber(x))) __illegal(path);
        return <any[]>path;
    } else {
        return isString(path)
            ? path.length > 0
                ? path.split(".")
                : []
            : path != null
            ? <any[]>[path]
            : [];
    }
};

/**
 * Takes an arbitrary object and lookup path. Descends into object along
 * path and returns true if the full path exists (even if final leaf
 * value is `null` or `undefined`). Checks are performed using
 * `hasOwnProperty()`.
 *
 * @param obj -
 * @param path -
 */
export const exists = (obj: any, path: Path) => {
    if (obj == null) {
        return false;
    }
    path = toPath(path);
    for (let n = path.length - 1, i = 0; i <= n; i++) {
        const k = path[i];
        if (!obj.hasOwnProperty(k)) {
            return false;
        }
        obj = obj[k];
        if (obj == null && i < n) {
            return false;
        }
    }
    return true;
};

/**
 * Helper function. First converts given `path` using {@link toPath} and then
 * analyzes it via
 * [`isProtoPath()`](https://docs.thi.ng/umbrella/checks/functions/isProtoPath.html).
 * Throws an error if path contains any property which might lead to prototype
 * poisoning. Returns converted path if valid.
 *
 * @remarks
 * The following properties are considered illegal.
 *
 * - `__proto__`
 * - `prototype`
 * - `constructor`
 *
 * @param path -
 */
export const disallowProtoPath = (path: Path): readonly NumOrString[] => {
    const $path = toPath(path);
    if (isProtoPath($path)) __illegal(path);
    return $path;
};

/** @internal */
const __illegal = (path: any) =>
    illegalArgs(`illegal path: ${JSON.stringify(path)}`);