diff --git a/packages/resolve-map/README.md b/packages/resolve-map/README.md index 83bb581fab..f6cf8f7a87 100644 --- a/packages/resolve-map/README.md +++ b/packages/resolve-map/README.md @@ -19,6 +19,7 @@ This project is part of the - [Theme configuration](#theme-configuration) - [API](#api) - [`resolve(obj)`](#resolveobj) + - [Protecting values](#protecting-values) - [Authors](#authors) - [License](#license) @@ -75,7 +76,7 @@ node --experimental-repl-await > const resolveMap = await import("@thi.ng/resolve-map"); ``` -Package sizes (gzipped, pre-treeshake): ESM: 929 bytes +Package sizes (gzipped, pre-treeshake): ESM: 1.03 KB ## Dependencies @@ -297,6 +298,15 @@ res.e(2); // 20 ``` +#### Protecting values + +Values can be protected from further resolution attempts by wrapping them via +[`resolved()`](https://docs.thi.ng/umbrella/resolve-map/modules.html#resolved). +The wrapped value can be later obtained via the standard [`IDeref` +interface/mechanism](https://docs.thi.ng/umbrella/api/interfaces/IDeref.html). +In lookup/resolution functions, the unwrapped value will be supplied, no +`.deref()` necessary there. + ## Authors Karsten Schmidt diff --git a/packages/resolve-map/src/index.ts b/packages/resolve-map/src/index.ts index a9779777e8..528590025e 100644 --- a/packages/resolve-map/src/index.ts +++ b/packages/resolve-map/src/index.ts @@ -1,11 +1,10 @@ -import type { Fn, NumOrString } from "@thi.ng/api"; +import type { Fn, IDeref, NumOrString } from "@thi.ng/api"; import { SEMAPHORE } from "@thi.ng/api/api"; import { isArray } from "@thi.ng/checks/is-array"; import { isFunction } from "@thi.ng/checks/is-function"; import { isPlainObject } from "@thi.ng/checks/is-plain-object"; import { isString } from "@thi.ng/checks/is-string"; import { illegalArgs } from "@thi.ng/errors/illegal-arguments"; -import { getInUnsafe } from "@thi.ng/paths/get-in"; import { mutInUnsafe } from "@thi.ng/paths/mut-in"; import { exists } from "@thi.ng/paths/path"; @@ -14,6 +13,7 @@ const RE_ARGS = /^(function\s+\w+)?\s*\(\{([\w\s,:]+)\}/; export type Unresolved = { [K in keyof T]: | Unresolved + | Resolved | Fn | Fn | Function @@ -39,6 +39,11 @@ export type LookupPath = NumOrString[]; * access any parent levels. Absolute refs are always resolved from the root * level (the original object passed to this function). * + * Values can be protected from further resolution attempts by wrapping them via + * {@link resolved}. The wrapped value can be later obtained via the standard + * {@link @thi.ng/api#IDeref} interface/mechanism. In lookup functions, the + * unwrapped value will be supplied, no `.deref()` necessary there. + * * @example * ```ts * // `c` references sibling `d` @@ -169,8 +174,12 @@ const _resolve = ( illegalArgs(`cyclic references not allowed: ${pathID}`); } // console.log(pp, resolved[pp], stack); - let v = getInUnsafe(root, path); + let [v, isResolved] = getInUnsafe(root, path); if (!resolved[pathID]) { + if (isResolved) { + resolved[pathID] = true; + return v; + } let res = SEMAPHORE; stack.push(pathID); if (isPlainObject(v)) { @@ -343,3 +352,49 @@ export const absPath = ( !curr.length && illegalArgs(`invalid lookup path: ${path}`); return curr; }; + +/** + * Value wrapper to protect from future recursive resolution attempts. See + * {@link resolved} for further details. + */ +export class Resolved implements IDeref { + constructor(protected _value: T) {} + + deref() { + return this._value; + } +} + +/** + * Factory function for {@link Resolved} to wrap & protect values from further + * resolution attempts. The wrapped value can be later obtained via the standard + * {@link @thi.ng/api#IDeref} interface/mechanism. In lookup functions, the + * unwrapped value will be supplied, no `.deref()` necessary there. + * + * @param val + */ +export const resolved = (val: T) => new Resolved(val); + +/** + * Special version of {@link @thi.ng/paths#getInUnsafe} with extra support for + * intermediate wrapped {@link Resolved} values and returning tuple of: + * `[val,isResolved]`. + * + * @param obj + * @param path + * + * @internal + */ +const getInUnsafe = (obj: any, path: LookupPath) => { + const n = path.length - 1; + let res = obj; + let isResolved = obj instanceof Resolved; + for (let i = 0; res != null && i <= n; i++) { + res = res[path[i]]; + if (res instanceof Resolved) { + isResolved = true; + res = res.deref(); + } + } + return [res, isResolved]; +}; diff --git a/packages/resolve-map/test/index.ts b/packages/resolve-map/test/index.ts index b84b4fedc7..1e679cb160 100644 --- a/packages/resolve-map/test/index.ts +++ b/packages/resolve-map/test/index.ts @@ -2,7 +2,7 @@ import type { Fn0 } from "@thi.ng/api"; import { group } from "@thi.ng/testament"; import * as tx from "@thi.ng/transducers"; import * as assert from "assert"; -import { resolve, ResolveFn } from "../src/index.js"; +import { resolve, Resolved, resolved, ResolveFn } from "../src/index.js"; group("resolve-map", { simple: () => { @@ -262,4 +262,22 @@ group("resolve-map", { { a: { b: { c: 1, d: 1, e: 10 }, c: { d: 1 } }, c: { d: 10 } } ); }, + + resolved: () => { + interface Foo { + a: Resolved<{ x: () => number; y: (() => number)[] }>; + b: number; + c: () => number; + } + const res = resolve({ + a: ({ b }: Foo) => resolved({ x: () => b, y: [() => 1] }), + b: () => 2, + c: "@a/y/0", + }); + assert.ok(res.a instanceof Resolved); + assert.strictEqual(res.a.deref().x(), 2); + assert.strictEqual(res.a.deref().y[0](), 1); + assert.strictEqual(res.b, 2); + assert.strictEqual(res.c(), 1); + }, }); diff --git a/packages/resolve-map/tpl.readme.md b/packages/resolve-map/tpl.readme.md index 6690c63234..db245a817c 100644 --- a/packages/resolve-map/tpl.readme.md +++ b/packages/resolve-map/tpl.readme.md @@ -248,6 +248,15 @@ res.e(2); // 20 ``` +#### Protecting values + +Values can be protected from further resolution attempts by wrapping them via +[`resolved()`](https://docs.thi.ng/umbrella/resolve-map/modules.html#resolved). +The wrapped value can be later obtained via the standard [`IDeref` +interface/mechanism](https://docs.thi.ng/umbrella/api/interfaces/IDeref.html). +In lookup/resolution functions, the unwrapped value will be supplied, no +`.deref()` necessary there. + ## Authors ${authors}