UNPKG

@oazmi/kitchensink

Version:

a collection of personal utility functions

157 lines (156 loc) 6.61 kB
/** utility functions for querying/traversing a nested object through the use of either: * - a dot-separated string query (`dot-path`) * - or an array of keys (`key-path`) * * this submodule is made to be somewhat strictly typed, * so you can expect to get type hints when accessing narrowly typed data via `key-path` or `dot-path` get/set functions. * think of it as analogous to `xpath` for xml, and `document.querySelector` for html. * * TODO: consider allowing `getKeyPath` and `setKeyPath` to accept `create_missing: boolean = false` option to create missing intermediate keys/entires * * @module */ import "./_dnt.polyfills.js"; import { number_parseInt } from "./alias.js"; /** get value of nested `obj` at a given `key-path`. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * const data = { kill: { your: { self: [0, 1, { 0: 0, 1: { noice: "YAHAHA", 0: "you found me!" } }] } } } as const * * // weakly typed variant * const * keypath_to_noice_parent = ["kill", "your", "self", 2, 1], // note: adding `as const` will not make it strongly typed because of a limitation/bug in `KeyPathsOf` * noice_parent = getKeyPath(data, keypath_to_noice_parent) // type: `unknown` * * assertEquals(noice_parent, { noice: "YAHAHA", 0: "you found me!" }) * * // strongly typed variant * const * strong_keypath_to_noice_parent: ["kill", "your", "self", 2, 1] = ["kill", "your", "self", 2, 1], * strong_noice_parent: { noice: "YAHAHA", 0: "you found me!" } = getKeyPath(data, strong_keypath_to_noice_parent) * * assertEquals(strong_noice_parent, { noice: "YAHAHA", 0: "you found me!" }) * ``` */ // @ts-expect-error: `KP` is not bound to extend `KeyPath`, which in turn breaks `KeyPathValue<T, KP>` (due to its unboundedness). export const getKeyPath = (obj, kpath) => { let value = obj; for (const k of kpath) { value = value[k]; } return value; }; /** set the value of nested `obj` at a given `key-path`, and then returns the object back. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * const data = { kill: { your: { self: [0, 1, { 0: 0, 1: { noice: "YAHAHA", 0: "you found me!" } }] } } } as const * * // weakly typed variant * // note: adding `as const` will not make it strongly typed because of a limitation/bug in `KeyPathsOf` * const keypath_to_yahaha = ["kill", "your", "self", 2, 1, "noice"] * setKeyPath(data, keypath_to_yahaha, "YEEEE") * * assertEquals(getKeyPath(data, keypath_to_yahaha), "YEEEE") * * // strongly typed variant * const strong_keypath_to_yahaha: ["kill", "your", "self", 2, 1, "noice"] = ["kill", "your", "self", 2, 1, "noice"] * // @ts-ignore: since `data` is declared as `const`, we cannot technically assign the value `"YOSHIII"` to the original `"YAHAHA"`. * setKeyPath(data, strong_keypath_to_yahaha, "YOSHIII") * * assertEquals(getKeyPath(data, strong_keypath_to_yahaha), "YOSHIII") * ``` */ // @ts-expect-error: `KP` is not bound to extend `KeyPath`, which in turn breaks `KeyPathValue<T, KP>` (due to its unboundedness). export const setKeyPath = (obj, kpath, value) => { const child_key = kpath.pop(), parent = getKeyPath(obj, kpath); kpath.push(child_key); parent[child_key] = value; return obj; }; /** similar to {@link bindDotPathTo}, but for `key-path`s. */ export const bindKeyPathTo = (bind_to) => ([ (kpath) => getKeyPath(bind_to, kpath), (kpath, value) => setKeyPath(bind_to, kpath, value) ]); /** convert a `dot-path` to `key-path`. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * const * my_dotpath = "kill.your.self.2abc.1.tasukete", * my_keypath = dotPathToKeyPath(my_dotpath) * * assertEquals(my_keypath, ["kill", "your", "self", "2abc", 1, "tasukete"]) * ``` */ export const dotPathToKeyPath = (dpath) => dpath .split(".") .map((key) => ( // the reason why we explicitly check if the value is finite before parsing as an integer, // is because there are cases where `parseInt` would produce a finite number out of an incorrect string. // for instance: `parseInt("20abc") === 20` (one would expect it to be `NaN`, but it isn't). // however, `isFinite` solves this by _only_ being truthy when its input is purely numerical. // in other words: `isFinite("20abc") === false` (just as one would expect). // one more thing: notice that we use `globalThis.isFinite` instead of `Number.isFinite`. this is because the two are apparently not the same. // `globalThis.isFinite` is capable of parsing string inputs, while `Number.isFinite` strictly takes numeric inputs, and will return a false on string inputs. // you can verify it yourself: `Number.isFinite !== globalThis.isFinite`, but `Number.parseInt === globalThis.parseInt`, and `Number.parseFloat === globalThis.parseFloat`. isFinite(key) ? number_parseInt(key) : key)); /** get value of nested `obj` at a given `dot-path`. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * const * data = { kill: { your: { self: [0, 1, { 0: 0, 1: { noice: "YAHAHA", 0: "you found me!" } }] } } }, * dotpath_to_noice_parent = "kill.your.self.2.1" * * assertEquals(getDotPath(data, dotpath_to_noice_parent), { noice: "YAHAHA", 0: "you found me!" }) * ``` */ export const getDotPath = (obj, dpath) => getKeyPath(obj, dotPathToKeyPath(dpath)); /** set the value of nested `obj` at a given `dot-path`. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * const * data = { kill: { your: { self: [0, 1, { 0: 0, 1: { noice: "YAHAHA", 0: "you found me!" } }] } } }, * dotpath_to_yahaha = "kill.your.self.2.1.noice" * * setDotPath(data, dotpath_to_yahaha, "REEE") * * assertEquals(getDotPath(data, dotpath_to_yahaha), "REEE") * ``` */ export const setDotPath = (obj, dpath, value) => setKeyPath(obj, dotPathToKeyPath(dpath), value); /** generate a `dot-path` getter and setter for a specific object given by `bind_to`. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * const * data = { kill: { your: { self: [0, 1, { 0: 0, 1: { noice: "YAHAHA", 0: "you found me!" } }] } } }, * [getData, setData] = bindDotPathTo(data) * * assertEquals(getData("kill.your.self.2.1"), {0: "you found me!", noice: "YAHAHA"}) * * setData("kill.your.self.2.1.noice", ["arr", "ree", "eek"]) * * assertEquals(getData("kill.your.self.2.1"), {0: "you found me!", noice: ["arr", "ree", "eek"]}) * ``` */ export const bindDotPathTo = (bind_to) => ([ (dpath) => getDotPath(bind_to, dpath), (dpath, value) => setDotPath(bind_to, dpath, value) ]);