Skip to content

Commit

Permalink
refactor(resolve-map): fix #21
Browse files Browse the repository at this point in the history
BREAKING CHANGE: update lookup path prefix & separators

- lookup paths now are prefixed with `@` instead of `->`
- all path segments must be separated by `/`
- update readme & tests
  • Loading branch information
postspectacular committed May 9, 2018
1 parent 68ca46d commit 5d2a3fe
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 40 deletions.
46 changes: 25 additions & 21 deletions packages/resolve-map/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,37 +33,41 @@ 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. Relative refs are resolved from currently
visited object and support "../" prefixes to access parent levels.
Absolute refs are always resolved from the root level (the original
object passed to this function).
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).

```ts
resolveMap({a: 1, b: {c: "->d", d: "->/a"} })
resolveMap({a: 1, b: {c: "@d", d: "@/a"} })
// { a: 1, b: { c: 1, d: 1 } }
```

If a value is a function, it is called with a single arg `resolve`, a
function which accepts a path (**without `->` prefix**) to look up other
function which accepts a path (**without `@` prefix**) to look up other
values. The return value of the user provided function is used as final
value for that key. This mechanism can be used to compute derived values
of other values stored in the object. Function values will always be
called only once. Therefore, in order to associate a function as value
to a key, it needs to be wrapped with an additional function, as shown
for the `e` key in the example below.
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 value to a key, it needs to 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
res = resolveMap({
a: (resolve) => resolve("b.c") * 100,
b: { c: "->d.0", d: [2, (resolve) => resolve("../../e")(2) ] },
a: (resolve) => resolve("b/c") * 100,
b: { c: "@d/0", d: [2, (resolve) => resolve("../../e")(2) ] },
e: () => (x) => x * 10,
f: () => "@foo",
})
// { a: 200, b: { c: 2, d: [ 2, 20 ] }, e: [Function] }
// { a: 200, b: { c: 2, d: [ 2, 20 ] }, e: [Function], f: "@foo" }

res.e(2);
// 20
Expand All @@ -90,18 +94,18 @@ resolveMap({
fontsizes: [12, 16, 20]
},
button: {
bg: "->/colors.text",
label: "->/colors.bg",
bg: "@/colors/text",
label: "@/colors/bg",
// resolve with abs path inside fn
fontsize: (resolve) => `${resolve("/main.fontsizes.0")}px`,
fontsize: (resolve) => `${resolve("/main/fontsizes/0")}px`,
},
buttonPrimary: {
bg: "->/colors.selected",
label: "->/button.label",
bg: "@/colors/selected",
label: "@/button/label",
// resolve with relative path inside fn
fontsize: (resolve) => `${resolve("../main.fontsizes.2")}px`,
fontsize: (resolve) => `${resolve("../main/fontsizes/2")}px`,
}
})
});
// {
// colors: {
// bg: "white",
Expand Down
12 changes: 6 additions & 6 deletions packages/resolve-map/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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 { getIn, mutIn, toPath } from "@thi.ng/paths";
import { getIn, mutIn } from "@thi.ng/paths";

const SEMAPHORE = Symbol("SEMAPHORE");

Expand Down Expand Up @@ -84,9 +84,9 @@ const resolveArray = (arr: any[], root?: any, path: PropertyKey[] = [], resolved

const _resolve = (root: any, path: PropertyKey[], resolved: any) => {
let v = getIn(root, path), rv = SEMAPHORE;
const pp = path.join(".");
const pp = path.join("/");
if (!resolved[pp]) {
if (isString(v) && v.indexOf("->") === 0) {
if (isString(v) && v.charAt(0) === "@") {
rv = _resolve(root, absPath(path, v), resolved);
} else if (isPlainObject(v)) {
resolveMap(v, root, path, resolved);
Expand All @@ -104,9 +104,9 @@ const _resolve = (root: any, path: PropertyKey[], resolved: any) => {
return v;
}

const absPath = (curr: PropertyKey[], q: string, idx = 2): PropertyKey[] => {
const absPath = (curr: PropertyKey[], q: string, idx = 1): PropertyKey[] => {
if (q.charAt(idx) === "/") {
return toPath(q.substr(idx + 1));
return q.substr(idx + 1).split("/");
}
curr = curr.slice(0, curr.length - 1);
const sub = q.substr(idx).split("/");
Expand All @@ -115,7 +115,7 @@ const absPath = (curr: PropertyKey[], q: string, idx = 2): PropertyKey[] => {
!curr.length && illegalArgs(`invalid lookup path`);
curr.pop();
} else {
return curr.concat(toPath(sub[i]));
return curr.concat(sub.slice(i));
}
}
!curr.length && illegalArgs(`invalid lookup path`);
Expand Down
26 changes: 13 additions & 13 deletions packages/resolve-map/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,53 @@ describe("resolve-map", () => {

it("simple", () => {
assert.deepEqual(
resolveMap({ a: 1, b: "->a" }),
resolveMap({ a: 1, b: "@a" }),
{ a: 1, b: 1 }
);
});

it("linked refs", () => {
assert.deepEqual(
resolveMap({ a: "->c", b: "->a", c: 1 }),
resolveMap({ a: "@c", b: "@a", c: 1 }),
{ a: 1, b: 1, c: 1 }
);
});

it("array refs", () => {
assert.deepEqual(
resolveMap({ a: "->c.1", b: "->a", c: [1, 2] }),
resolveMap({ a: "@c/1", b: "@a", c: [1, 2] }),
{ a: 2, b: 2, c: [1, 2] }
);
});

it("abs vs rel refs", () => {
assert.deepEqual(
resolveMap({ a1: { b: 1, c: "->b" }, a2: { b: 2, c: "->b" }, a3: { b: 3, c: "->/a1.b" } }),
resolveMap({ a1: { b: 1, c: "@b" }, a2: { b: 2, c: "@b" }, a3: { b: 3, c: "@/a1/b" } }),
{ a1: { b: 1, c: 1 }, a2: { b: 2, c: 2 }, a3: { b: 3, c: 1 } }
);
});

it("rel parent refs", () => {
assert.deepEqual(
resolveMap({ a: { b: { c: "->../c.d", d: "->c", e: "->/c.d" }, c: { d: 1 } }, c: { d: 10 } }),
resolveMap({ a: { b: { c: "@../c/d", d: "@c", e: "@/c/d" }, c: { d: 1 } }, c: { d: 10 } }),
{ a: { b: { c: 1, d: 1, e: 10 }, c: { d: 1 } }, c: { d: 10 } }
);
})

it("cycles", () => {
assert.throws(() => resolveMap({ a: "->a" }));
assert.throws(() => resolveMap({ a: { b: "->b" } }));
assert.throws(() => resolveMap({ a: { b: "->/a" } }));
assert.throws(() => resolveMap({ a: { b: "->/a.b" } }));
assert.throws(() => resolveMap({ a: "->b", b: "->a" }));
assert.throws(() => resolveMap({ a: "@a" }));
assert.throws(() => resolveMap({ a: { b: "@b" } }));
assert.throws(() => resolveMap({ a: { b: "@/a" } }));
assert.throws(() => resolveMap({ a: { b: "@/a/b" } }));
assert.throws(() => resolveMap({ a: "@b", b: "@a" }));
});

it("function refs", () => {
assert.deepEqual(
resolveMap({ a: (x) => x("b.c") * 10, b: { c: "->d", d: "->/e" }, e: () => 1 }),
resolveMap({ a: (x) => x("b/c") * 10, b: { c: "@d", d: "@/e" }, e: () => 1 }),
{ a: 10, b: { c: 1, d: 1 }, e: 1 }
);
const res = resolveMap({ a: (x) => x("b.c")() * 10, b: { c: "->d", d: "->/e" }, e: () => () => 1 });
const res = resolveMap({ a: (x) => x("b/c")() * 10, b: { c: "@d", d: "@/e" }, e: () => () => 1 });
assert.equal(res.a, 10);
assert.strictEqual(res.b.c, res.e);
assert.strictEqual(res.b.d, res.e);
Expand All @@ -61,7 +61,7 @@ describe("resolve-map", () => {
it("function resolves only once", () => {
let n = 0;
assert.deepEqual(
resolveMap({ a: (x) => x("b.c"), b: { c: "->d", d: "->/e" }, e: () => (n++ , 1) }),
resolveMap({ a: (x) => x("b/c"), b: { c: "@d", d: "@/e" }, e: () => (n++ , 1) }),
{ a: 1, b: { c: 1, d: 1 }, e: 1 }
);
assert.equal(n, 1);
Expand Down

0 comments on commit 5d2a3fe

Please sign in to comment.