Skip to content

Commit

Permalink
feat(resolve-map): add support for protected values
Browse files Browse the repository at this point in the history
- add `Resolved` wrapper & factory fn for protecting values from
  future/duplicate resolution attempts
- add tests
- update docs/readme
  • Loading branch information
postspectacular committed May 23, 2022
1 parent 97285e4 commit 6280510
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 5 deletions.
12 changes: 11 additions & 1 deletion packages/resolve-map/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
61 changes: 58 additions & 3 deletions packages/resolve-map/src/index.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -14,6 +13,7 @@ const RE_ARGS = /^(function\s+\w+)?\s*\(\{([\w\s,:]+)\}/;
export type Unresolved<T> = {
[K in keyof T]:
| Unresolved<T[K]>
| Resolved<T[K]>
| Fn<T, T[K]>
| Fn<ResolveFn, T[K]>
| Function
Expand All @@ -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`
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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<T> implements IDeref<T> {
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 = <T>(val: T) => new Resolved<T>(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];
};
20 changes: 19 additions & 1 deletion packages/resolve-map/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => {
Expand Down Expand Up @@ -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<Foo>({
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);
},
});
9 changes: 9 additions & 0 deletions packages/resolve-map/tpl.readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down

0 comments on commit 6280510

Please sign in to comment.