Skip to content

Commit

Permalink
feat(resolve-map): add cycle detection, fix edge cases
Browse files Browse the repository at this point in the history
- add `resolve()` as main user function
- make `resolveMap()` private
- update _resolve() to keep track of active lookups
- implement cycle detection
- add markResolved() helper
- update resolveFunction() to mark all of its nested
  values as resolved
- refactor resolvePath()
- update docs/readme
- add/update tests

BREAKING CHANGE: `resolveMap()` renamed to `resolve()`, update docs
  • Loading branch information
postspectacular committed Jun 7, 2018
1 parent 57f1ed5 commit e61c3b5
Show file tree
Hide file tree
Showing 3 changed files with 229 additions and 157 deletions.
107 changes: 56 additions & 51 deletions packages/resolve-map/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,68 +25,74 @@ supported.

## API

### `resolveMap(obj)`

Visits all key-value pairs in depth-first order for given object or
array, expands any reference values, mutates the original object and
returns it. Cyclic references are not allowed or checked for and if
present will cause a stack overflow error. However, refs pointing to
other refs are recursively resolved (again, provided there are no
cycles).

Reference values are special strings representing lookup paths of other
values in the object and are prefixed with `@` for relative refs or
`@/` for absolute refs and both using `/` as path separator (Note:
trailing slashes are NOT allowed!). Relative refs are resolved from
currently visited object and support "../" prefixes to access any parent
levels. Absolute refs are always resolved from the root level (the
original object passed to this function).
### `resolve(obj)`

Visits all key-value pairs or array items in depth-first order,
expands any reference values, mutates the original object and returns
it. Cyclic references are not allowed and will throw an error.
However, refs pointing to other refs are recursively resolved (again,
provided there are no cycles).
Reference values are special strings representing lookup paths of
other values in the object and are prefixed with `@` for relative
refs or `@/` for absolute refs and both using `/` as path separator
(Note: trailing slashes are NOT allowed!). Relative refs are resolved
from the currently visited object and support "../" prefixes to
access any parent levels. Absolute refs are always resolved from the
root level (the original object passed to this function).

```ts
resolveMap({a: 1, b: {c: "@d", d: "@/a"} })
// `c` references sibling `d`
// `d` references parent `a`
resolve({a: 1, b: {c: "@d", d: "@/a"} })
// { a: 1, b: { c: 1, d: 1 } }
```

If a value is a function, it is called using two possible conventions:
Any function values are called using two possible conventions:

1) If the user function uses ES6 object destructuring for its first
argument, the given object keys are resolved prior to calling the
function and the resolved values provided as first argument and a
general `resolve` function as second argument.
function and the resolved values provided as first argument (object)
and a general `resolve` function as second argument.
2) If no de-structure form is found in the function's arguments, the
function is only called with `resolve` as argument.

**Important:** Since ES6 var names can't contain special characters,
destructured keys are ALWAYS looked up as siblings of the currently
processed value.
destructured keys can ALWAYS only be looked up as siblings of the
currently processed key.

The `resolve` function provided as arg to the user function accepts a
path (**without `@` prefix**) to look up any other values in the root
object.

```ts
// `c` uses ES6 destructuring form to look up `a` & `b` values
{a: 1, b: 2, c: ({a,b}) => a + b }
=>
// { a: 1, b: 2, c: 3 }
// `d` uses provided resolve fn arg `$` to look up `c`
resolve({a: 1, b: 2, c: ({a,b}) => a + b, d: ($) => $("c") })
// { a: 1, b: 2, c: 3, d: 3 }

// last item references item @ index = 2
resolve([1,2, ($) => $("0") + $("1"), "@2"])
// [1, 2, 3, 3]
```

The single arg `resolve` function accepts a path (**without `@`
prefix**) to look up any other values in the object. The return value of
the user provided function is used as final value for that key in the
object. This mechanism can be used to compute derived values of other
values stored anywhere in the root object. **Function values will always
be called only once.** Therefore, in order to associate a function as
final value to a key, it MUST be wrapped with an additional function, as
shown for the `e` key in the example below. Similarly, if an actual
string value should happen to start with `@`, it needs to be wrapped in
a function (see `f` key below).
The return value of the user provided function is used as final value
for that key in the object. This mechanism can be used to compute
derived values of other values stored anywhere in the root object.
**Function values will always be called only once.** Therefore, in order
to associate a function as final value to a key, it MUST be wrapped with
an additional function, as shown for the `e` key in the example below.
Similarly, if an actual string value should happen to start with `@`, it
needs to be wrapped in a function (see `f` key below).

```ts
// `a` is derived from 1st array element in `b.d`
// `b.c` is looked up from `b.d[0]`
// `b.d[1]` is derived from calling `e(2)`
// `e` is a wrapped function
// `f` is wrapped to ignore `@` prefix
res = resolveMap({
a: (resolve) => resolve("b/c") * 100,
b: { c: "@d/0", d: [2, (resolve) => resolve("../../e")(2) ] },
res = resolve({
a: ($) => $("b/c") * 100,
b: { c: "@d/0", d: [2, ($) => $("../../e")(2) ] },
e: () => (x) => x * 10,
f: () => "@foo",
})
Expand Down Expand Up @@ -115,17 +121,16 @@ yarn add @thi.ng/resolve-map
In this example we construct a graph to compute a number of statistical
properties for some numeric input array. The graph is a plain object of
possibly dependent functions, which can be specified in any order. Each
function accepts a "resolver" function as argument (`$`) to look up and
execute other computations. Each computation is only executed once.
function uses ES6 object destructuring to look up and execute other
computations in the graph. Each computation is only executed once.

```ts
import { resolveMap } from "@thi.ng/resolve-map";
import { resolve } from "@thi.ng/resolve-map";
import * as tx from "@thi.ng/transducers";

// define object of interrelated computations
// the `$` arg passed to each fn is the resolver
// the `src` key is still missing here and will be
// provided later
// define object of interrelated computations to be executed later
// the `src` key used by most functions is still missing here and
// will be injected later as well
const stats = {
// sequence average
mean: ({src}) => tx.reduce(tx.mean(), src),
Expand Down Expand Up @@ -161,8 +166,8 @@ const stats = {

// Note 2: If the `stats` graph is meant to be re-usable in
// the future you MUST use the spread operator to create a
// shallow copy, because `resolveMap` mutates the given object
resolveMap({...stats, src: () => [ 1, 6, 7, 2, 4, 11, -3 ]})
// shallow copy, because `resolve` mutates the given object
resolve({...stats, src: () => [ 1, 6, 7, 2, 4, 11, -3 ]})
// {
// mean: 4,
// range: 14,
Expand All @@ -178,9 +183,9 @@ resolveMap({...stats, src: () => [ 1, 6, 7, 2, 4, 11, -3 ]})
### Theme configuration

```typescript
import { resolveMap } from "@thi.ng/resolve-map";
import { resolve } from "@thi.ng/resolve-map";

resolveMap({
resolve({
colors: {
bg: "white",
text: "black",
Expand All @@ -193,13 +198,13 @@ resolveMap({
bg: "@/colors/text",
label: "@/colors/bg",
// resolve with abs path inside fn
fontsize: (resolve) => `${resolve("/main/fontsizes/0")}px`,
fontsize: ($) => `${$("/main/fontsizes/0")}px`,
},
buttonPrimary: {
bg: "@/colors/selected",
label: "@/button/label",
// resolve with relative path inside fn
fontsize: (resolve) => `${resolve("../main/fontsizes/2")}px`,
fontsize: ($) => `${$("../main/fontsizes/2")}px`,
}
});
// {
Expand Down
Loading

0 comments on commit e61c3b5

Please sign in to comment.