import { iife } from "./Function.js"; import { type ExtractKey } from "./Types.js"; /** * extract a nested property by a structured key * @example * * ```typescript * extract({a: {b: {c: 1}}}, "a.b.c") // 1 * ``` */ export const extract = ( value: Value, key: Key, ): ExtractKey => { const parts = key.replaceAll("[", ".[").split("."); let cursor = value; // caution! Typescript lies ahead but typing intermediate levels of Value is completely unnecessary for (const part of parts) { if (cursor == null) { return undefined as ExtractKey; } if (part.startsWith("[")) { cursor = cursor?.[part.slice(1, -1) as keyof Value] as Value; } else { cursor = cursor?.[part as keyof Value] as Value; } } return cursor as ExtractKey; }; /** * tries to parse the key as a number. * If the key is not a number it is returned unchanged. */ const tryParse = (key: string) => { const asNumber = Number(key); return Number.isNaN(asNumber) ? key : asNumber; }; /** * inflate a nested structure from a flat list of keys * * @example * * ```ts * const nested = inflate([["a.b[0].c", 1]]) * // ^? = {a: {b: [{c: 1}]}} * ``` */ export const inflate = ( entries: Iterable<[string, unknown]>, ): Target => { // accessing either indices or keys but both with keyof leads to broken inference // fixing this isn't worth the effort considering we can unit test this // and the type doesn't leak outside this fn definition // eslint-disable-next-line @typescript-eslint/no-explicit-any type DisjunctHack = any; const data = {} as Target; // for every key value pair of entries... for (const [key, value] of entries) { // ... start with the root object ... let cursor = data as DisjunctHack; // ... then split the structured key into its parts... const parts = key .split(/[.[\]]/) .filter((part) => part !== "") .map(tryParse); // ...remove the last part and store it... const leaf = parts.pop(); if (leaf == null) { // empty key, ignore continue; } // ...move along the parts of the structured key... for (let i = 0; i < parts.length; i++) { const part = parts[i]!; const next = parts[i + 1] ?? leaf; // ...and traverse down the cursors properties, building it along the way according to the key... cursor = cursor[part] ??= typeof next === "number" ? [] : {}; } // ...at the end the cursor is the bottom-most object and we can just set the value on the leaf part. cursor[leaf] = cursor[leaf] == null ? value : iife(() => { // aggregate multiple values into an array cursor[leaf] = Array.isArray(cursor[leaf]) ? cursor[leaf] : [cursor[leaf]]; cursor[leaf].push(value); return cursor[leaf]; }); } return data; };