Skip to content

Commit

Permalink
feat(resolve-map): add Unresolved type & type checking
Browse files Browse the repository at this point in the history
BREAKING CHANGE: add type checking to `resolve()`.
This MIGHT require additional type generics (of the result object type)
to be added to any call sites. See tests for examples.
  • Loading branch information
postspectacular committed May 2, 2022
1 parent efa9adb commit a997fd2
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 36 deletions.
32 changes: 22 additions & 10 deletions packages/resolve-map/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { NumOrString } from "@thi.ng/api";
import type { Fn, 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";
Expand All @@ -11,6 +11,15 @@ import { exists } from "@thi.ng/paths/path";

const RE_ARGS = /^(function\s+\w+)?\s*\(\{([\w\s,:]+)\}/;

export type Unresolved<T> = {
[K in keyof T]:
| Unresolved<T[K]>
| Fn<T, T[K]>
| Fn<ResolveFn, T[K]>
| Function
| string;
};

export type ResolveFn = (path: string) => any;

export type LookupPath = NumOrString[];
Expand All @@ -21,6 +30,7 @@ export type LookupPath = NumOrString[];
* references are not allowed and will throw an error. However, refs pointing to
* other refs are recursively resolved (again, provided there are no cycles).
*
* @remarks
* Reference values are special strings representing lookup paths of other
* values in the object and are prefixed with given `prefix` string (default:
* `@`) for relative refs or `@/` for absolute refs and both using `/` as path
Expand Down Expand Up @@ -95,17 +105,19 @@ export type LookupPath = NumOrString[];
* @param root -
* @param prefix -
*/
export const resolve = (root: any, prefix = "@") => {
export function resolve<T>(root: Unresolved<T>, prefix?: string): T;
export function resolve<T>(root: Unresolved<T[]>, prefix?: string): T[];
export function resolve(root: any, prefix = "@") {
if (isPlainObject(root)) {
return resolveMap(root, prefix);
} else if (isArray(root)) {
return resolveArray(root, prefix);
}
return root;
};
}

const resolveMap = (
obj: any,
const resolveMap = <T>(
obj: Unresolved<T>,
prefix: string,
root?: any,
path: LookupPath = [],
Expand All @@ -116,22 +128,22 @@ const resolveMap = (
for (let k in obj) {
_resolve(root, [...path, k], resolved, stack, prefix);
}
return obj;
return <T>obj;
};

const resolveArray = (
arr: any[],
const resolveArray = <T>(
arr: Unresolved<T[]>,
prefix: string,
root?: any,
path: LookupPath = [],
resolved: any = {},
stack: string[] = []
) => {
): T[] => {
root = root || arr;
for (let k = 0, n = arr.length; k < n; k++) {
_resolve(root, [...path, k], resolved, stack, prefix);
}
return arr;
return <any>arr;
};

/**
Expand Down
117 changes: 91 additions & 26 deletions packages/resolve-map/test/index.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,64 @@
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, ResolveFn } from "../src/index.js";

group("resolve-map", {
simple: () => {
assert.deepStrictEqual(resolve({ a: 1, b: "@a" }), { a: 1, b: 1 });
assert.deepStrictEqual(
resolve<{
a: number;
b: number;
}>({ a: 1, b: "@a" }),
{
a: 1,
b: 1,
}
);
},

"linked refs": () => {
assert.deepStrictEqual(resolve({ a: "@c", b: "@a", c: 1 }), {
a: 1,
b: 1,
c: 1,
});
assert.deepStrictEqual(
resolve<{
a: number;
b: number;
c: number;
}>({ a: "@c", b: "@a", c: 1 }),
{
a: 1,
b: 1,
c: 1,
}
);
},

"array refs": () => {
assert.deepStrictEqual(resolve({ a: "@c/1", b: "@a", c: [1, 2] }), {
a: 2,
b: 2,
c: [1, 2],
});
assert.deepStrictEqual(
resolve<{ a: number; b: number; c: number[] }>({
a: "@c/1",
b: "@a",
c: [1, 2],
}),
{
a: 2,
b: 2,
c: [1, 2],
}
);
},

"abs vs rel refs": () => {
interface Inner {
b: number;
c: number;
}
assert.deepStrictEqual(
resolve({
resolve<{
a1: Inner;
a2: Inner;
a3: Inner;
}>({
a1: { b: 1, c: "@b" },
a2: { b: 2, c: "@b" },
a3: { b: 3, c: "@/a1/b" },
Expand All @@ -37,7 +69,10 @@ group("resolve-map", {

"rel parent refs": () => {
assert.deepStrictEqual(
resolve({
resolve<{
a: { b: { c: number; d: number; e: number }; c: { d: number } };
c: { d: number };
}>({
a: { b: { c: "@../c/d", d: "@c", e: "@/c/d" }, c: { d: 1 } },
c: { d: 10 },
}),
Expand All @@ -55,14 +90,22 @@ group("resolve-map", {

"function refs": () => {
assert.deepStrictEqual(
resolve({
resolve<{
a: number;
b: { c: number; d: number };
e: number;
}>({
a: (x: ResolveFn) => x("b/c") * 10,
b: { c: "@d", d: "@/e" },
e: () => 1,
}),
{ a: 10, b: { c: 1, d: 1 }, e: 1 }
);
const res = resolve({
const res = resolve<{
a: number;
b: { c: number; d: number };
e: Fn0<number>;
}>({
a: (x: ResolveFn) => x("b/c")() * 10,
b: { c: "@d", d: "@/e" },
e: () => () => 1,
Expand All @@ -76,7 +119,7 @@ group("resolve-map", {
"function resolves only once": () => {
let n = 0;
assert.deepStrictEqual(
resolve({
resolve<{ a: number; b: { c: number; d: number }; e: number }>({
a: (x: ResolveFn) => x("b/c"),
b: { c: "@d", d: "@/e" },
e: () => (n++, 1),
Expand All @@ -88,7 +131,11 @@ group("resolve-map", {

"deep resolve of yet unknown refs": () => {
assert.deepStrictEqual(
resolve({
resolve<{
a: number;
b: { c: { d: { e: number } } };
x: number;
}>({
a: "@b/c/d",
b: ($: ResolveFn) => ({ c: { d: { e: $("/x") } } }),
x: 1,
Expand Down Expand Up @@ -148,22 +195,37 @@ group("resolve-map", {
},

"destructures w/ local renames": () => {
assert.deepStrictEqual(resolve({ a: 1, b: ({ a: aa }: any) => aa }), {
a: 1,
b: 1,
});
assert.deepStrictEqual(
resolve<{
a: number;
b: number;
}>({ a: 1, b: ({ a: aa }: any) => aa }),
{
a: 1,
b: 1,
}
);
},

"destructures w/ trailing comma": () => {
interface Test {
a: number;
b: number;
c: number;
}
assert.deepStrictEqual(
// since prettier is running over this file
// build function dynamically to force trailing comma
resolve({ a: 1, b: 2, c: new Function("{a,b,}", "return a + b") }),
resolve<Test>({
a: 1,
b: 2,
c: new Function("{a,b,}", "return a + b"),
}),
{ a: 1, b: 2, c: 3 },
"comma only"
);
assert.deepStrictEqual(
resolve({
resolve<Test>({
a: 1,
b: 2,
c: new Function("{ a, b, }", "return a + b"),
Expand All @@ -172,7 +234,7 @@ group("resolve-map", {
"comma & whitespaces"
);
assert.deepStrictEqual(
resolve({
resolve<Test>({
a: 1,
b: 2,
c: new Function("{ a, b: bb, }", "return a + bb"),
Expand All @@ -184,7 +246,10 @@ group("resolve-map", {

"custom prefix": () => {
assert.deepStrictEqual(
resolve(
resolve<{
a: { b: { c: number; d: number; e: number }; c: { d: number } };
c: { d: number };
}>(
{
a: {
b: { c: ">>>../c/d", d: ">>>c", e: ">>>/c/d" },
Expand Down

0 comments on commit a997fd2

Please sign in to comment.