@weborigami/async-tree 0.6.9 → 0.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/package.json +1 -1
  2. package/shared.js +2 -1
  3. package/src/TraverseError.js +7 -3
  4. package/src/drivers/CalendarMap.js +12 -4
  5. package/src/operations/addNextPrevious.js +2 -2
  6. package/src/operations/assign.js +7 -3
  7. package/src/operations/cache.js +4 -4
  8. package/src/operations/child.js +2 -2
  9. package/src/operations/clear.js +3 -3
  10. package/src/operations/deepEntries.js +3 -3
  11. package/src/operations/deepMap.js +2 -2
  12. package/src/operations/deepMerge.js +15 -2
  13. package/src/operations/deepReverse.js +4 -2
  14. package/src/operations/deepTake.js +3 -2
  15. package/src/operations/deepText.js +5 -4
  16. package/src/operations/deepValuesIterator.js +3 -3
  17. package/src/operations/delete.js +4 -5
  18. package/src/operations/entries.js +2 -2
  19. package/src/operations/filter.js +2 -2
  20. package/src/operations/first.js +2 -2
  21. package/src/operations/forEach.js +6 -2
  22. package/src/operations/from.js +5 -2
  23. package/src/operations/globKeys.js +2 -2
  24. package/src/operations/groupBy.js +5 -4
  25. package/src/operations/has.js +2 -2
  26. package/src/operations/indent.js +9 -3
  27. package/src/operations/inners.js +2 -2
  28. package/src/operations/invokeFunctions.js +2 -2
  29. package/src/operations/json.js +2 -2
  30. package/src/operations/keys.js +2 -2
  31. package/src/operations/length.js +2 -2
  32. package/src/operations/map.js +22 -18
  33. package/src/operations/mapExtension.js +3 -3
  34. package/src/operations/mapReduce.js +4 -4
  35. package/src/operations/mask.js +6 -6
  36. package/src/operations/match.js +7 -18
  37. package/src/operations/merge.js +13 -2
  38. package/src/operations/paginate.js +3 -2
  39. package/src/operations/parent.js +2 -2
  40. package/src/operations/paths.js +2 -2
  41. package/src/operations/plain.js +2 -2
  42. package/src/operations/reduce.js +2 -2
  43. package/src/operations/regExpKeys.js +5 -3
  44. package/src/operations/reverse.js +2 -2
  45. package/src/operations/root.js +2 -2
  46. package/src/operations/scope.js +5 -4
  47. package/src/operations/set.js +2 -2
  48. package/src/operations/shuffle.js +2 -2
  49. package/src/operations/size.js +2 -2
  50. package/src/operations/sort.js +4 -4
  51. package/src/operations/sync.js +2 -2
  52. package/src/operations/take.js +3 -2
  53. package/src/operations/toFunction.js +2 -2
  54. package/src/operations/traverse.js +3 -1
  55. package/src/operations/traverseOrThrow.js +40 -8
  56. package/src/operations/traversePath.js +5 -3
  57. package/src/operations/values.js +2 -2
  58. package/src/operations/visit.js +2 -2
  59. package/src/operations/withKeys.js +5 -3
  60. package/src/utilities/args.js +128 -0
  61. package/src/utilities/interop.js +9 -0
  62. package/src/utilities/setParent.js +1 -1
  63. package/test/operations/root.test.js +2 -2
  64. package/test/operations/traverse.test.js +1 -32
  65. package/test/operations/traverseOrThrow.test.js +73 -0
  66. package/src/utilities/getMapArgument.js +0 -38
@@ -1,7 +1,9 @@
1
1
  import * as trailingSlash from "../trailingSlash.js";
2
2
  import TraverseError from "../TraverseError.js";
3
+ import isPacked from "../utilities/isPacked.js";
3
4
  import isUnpackable from "../utilities/isUnpackable.js";
4
5
  import from from "./from.js";
6
+ import isMaplike from "./isMaplike.js";
5
7
 
6
8
  /**
7
9
  * Return the value at the corresponding path of keys. Throw if any interior
@@ -11,11 +13,13 @@ import from from "./from.js";
11
13
  *
12
14
  * @param {Maplike} maplike
13
15
  * @param {...any} keys
16
+ * @returns {Promise<any>}
14
17
  */
15
18
  export default async function traverseOrThrow(maplike, ...keys) {
16
- // Start our traversal at the root of the tree.
17
- /** @type {any} */
18
19
  let value = maplike;
20
+
21
+ // For error reporting
22
+ let lastValue = value;
19
23
  let position = 0;
20
24
 
21
25
  // Process all the keys.
@@ -23,16 +27,31 @@ export default async function traverseOrThrow(maplike, ...keys) {
23
27
  let key;
24
28
  while (remainingKeys.length > 0) {
25
29
  if (value == null) {
26
- throw new TraverseError("A null or undefined value can't be traversed", {
27
- tree: maplike,
30
+ throw new TraverseError("A path hit a null or undefined value.", {
31
+ head: maplike,
32
+ lastValue,
28
33
  keys,
29
34
  position,
30
35
  });
31
36
  }
32
37
 
38
+ lastValue = value;
39
+
33
40
  // If the value is packed and can be unpacked, unpack it.
34
- if (isUnpackable(value)) {
35
- value = await value.unpack();
41
+ if (isPacked(value)) {
42
+ if (typeof (/** @type {any} */ (value).unpack) === "function") {
43
+ value = await value.unpack();
44
+ } else {
45
+ throw new TraverseError(
46
+ "A path hit binary file data that can't be unpacked.",
47
+ {
48
+ head: maplike,
49
+ lastValue,
50
+ keys,
51
+ position,
52
+ },
53
+ );
54
+ }
36
55
  }
37
56
 
38
57
  if (value instanceof Function) {
@@ -60,8 +79,21 @@ export default async function traverseOrThrow(maplike, ...keys) {
60
79
  }
61
80
 
62
81
  // If last key ended in a slash and value is unpackable, unpack it.
63
- if (key && trailingSlash.has(key) && isUnpackable(value)) {
64
- value = await value.unpack();
82
+ if (key && trailingSlash.has(key) && !isMaplike(value)) {
83
+ if (isUnpackable(value)) {
84
+ value = await value.unpack();
85
+ } else {
86
+ const message =
87
+ value === undefined
88
+ ? "A path tried to unpack a value that doesn't exist."
89
+ : "A path tried to unpack data that's already unpacked.";
90
+ throw new TraverseError(message, {
91
+ head: maplike,
92
+ lastValue,
93
+ keys,
94
+ position,
95
+ });
96
+ }
65
97
  }
66
98
 
67
99
  return value;
@@ -1,3 +1,4 @@
1
+ import * as args from "../utilities/args.js";
1
2
  import keysFromPath from "../utilities/keysFromPath.js";
2
3
  import traverse from "./traverse.js";
3
4
 
@@ -7,10 +8,11 @@ import traverse from "./traverse.js";
7
8
  *
8
9
  * @typedef {import("../../index.ts").Maplike} Maplike
9
10
  *
10
- * @param {Maplike} tree
11
+ * @param {Maplike} maplike
11
12
  * @param {string} path
12
13
  */
13
- export default async function traversePath(tree, path) {
14
+ export default async function traversePath(maplike, path) {
15
+ const map = await args.map(maplike, "Tree.traversePath");
14
16
  const keys = keysFromPath(path);
15
- return traverse(tree, ...keys);
17
+ return traverse(map, ...keys);
16
18
  }
@@ -1,4 +1,4 @@
1
- import getMapArgument from "../utilities/getMapArgument.js";
1
+ import * as args from "../utilities/args.js";
2
2
 
3
3
  /**
4
4
  * Return the values in the map.
@@ -8,7 +8,7 @@ import getMapArgument from "../utilities/getMapArgument.js";
8
8
  * @param {Maplike} maplike
9
9
  */
10
10
  export default async function values(maplike) {
11
- const map = await getMapArgument(maplike, "values");
11
+ const map = await args.map(maplike, "Tree.values");
12
12
  let result;
13
13
  /** @type {any} */
14
14
  let iterable = map.values();
@@ -1,4 +1,4 @@
1
- import getMapArgument from "../utilities/getMapArgument.js";
1
+ import * as args from "../utilities/args.js";
2
2
  import reduce from "./reduce.js";
3
3
 
4
4
  /**
@@ -9,6 +9,6 @@ import reduce from "./reduce.js";
9
9
  * @param {Maplike} source
10
10
  */
11
11
  export default async function visit(source) {
12
- const tree = await getMapArgument(source, "visit", { deep: true });
12
+ const tree = await args.map(source, "Tree.visit", { deep: true });
13
13
  return reduce(tree, () => undefined);
14
14
  }
@@ -1,5 +1,5 @@
1
1
  import AsyncMap from "../drivers/AsyncMap.js";
2
- import getMapArgument from "../utilities/getMapArgument.js";
2
+ import * as args from "../utilities/args.js";
3
3
  import values from "./values.js";
4
4
 
5
5
  /**
@@ -13,10 +13,12 @@ import values from "./values.js";
13
13
  * @returns {Promise<AsyncMap>}
14
14
  */
15
15
  export default async function withKeys(maplike, keysMaplike) {
16
- const source = await getMapArgument(maplike, "withKeys", { position: 0 });
17
- const keysMap = await getMapArgument(keysMaplike, "withKeys", {
16
+ const source = await args.map(maplike, "Tree.withKeys", {
18
17
  position: 1,
19
18
  });
19
+ const keysMap = await args.map(keysMaplike, "Tree.withKeys", {
20
+ position: 2,
21
+ });
20
22
 
21
23
  let keys;
22
24
 
@@ -0,0 +1,128 @@
1
+ import from from "../operations/from.js";
2
+ import isUnpackable from "./isUnpackable.js";
3
+ import toFunction from "./toFunction.js";
4
+ import toString from "./toString.js";
5
+
6
+ /**
7
+ * Runtime argument checking.
8
+ *
9
+ * These return a particular kind of argument or throw an error.
10
+ *
11
+ * Operations can use these to validate the arguments and provide more helpful
12
+ * error messages.
13
+ */
14
+
15
+ /**
16
+ * Check a function argument.
17
+ */
18
+ export function fn(arg, operation, options = {}) {
19
+ if (typeof arg !== "function") {
20
+ /** @type {any} */
21
+ const error = new TypeError(`${operation}: Expected a function argument.`);
22
+ error.position = options.position ?? 1;
23
+ throw error;
24
+ }
25
+ return arg;
26
+ }
27
+
28
+ /**
29
+ * Check an invocable argument and return it as a function.
30
+ *
31
+ * @param {import("../../index.ts").Invocable} arg
32
+ * @param {string} operation
33
+ * @returns {Function}
34
+ */
35
+ export function invocable(arg, operation, options = {}) {
36
+ const fn = toFunction(arg);
37
+ if (!fn) {
38
+ /** @type {any} */
39
+ const error = new TypeError(`${operation}: Expected a function argument.`);
40
+ error.position = options.position ?? 1;
41
+ throw error;
42
+ }
43
+ return fn;
44
+ }
45
+
46
+ /**
47
+ * Check a maplike argument and return it as a Map or AsyncMap.
48
+ *
49
+ * @typedef {import("../../index.ts").AsyncMap} AsyncMap
50
+ * @typedef {import("../../index.ts").Maplike} Maplike
51
+ * @typedef {import("../../index.ts").Unpackable} Unpackable
52
+ *
53
+ * @param {Maplike|Unpackable} arg
54
+ * @param {string} operation
55
+ * @param {{ deep?: boolean, position?: number }} [options]
56
+ * @returns {Promise<Map|AsyncMap>}
57
+ */
58
+ export async function map(arg, operation, options = {}) {
59
+ const deep = options.deep;
60
+ const position = options.position ?? 1;
61
+
62
+ if (isUnpackable(arg)) {
63
+ arg = await arg.unpack();
64
+ }
65
+
66
+ let map;
67
+ try {
68
+ map = from(arg, { deep });
69
+ } catch (/** @type {any} */ error) {
70
+ let message = error.message ?? error;
71
+ message = `${operation}: ${message}`;
72
+ const newError = new error.constructor(message);
73
+ /** @type {any} */ (newError).position = position;
74
+ throw newError;
75
+ }
76
+ return map;
77
+ }
78
+
79
+ /**
80
+ * Check a number argument.
81
+ *
82
+ * @param {number} arg
83
+ * @param {string} operation
84
+ */
85
+ export function number(arg, operation, options = {}) {
86
+ if (typeof arg !== "number" || Number.isNaN(arg)) {
87
+ /** @type {any} */
88
+ const error = new TypeError(`${operation}: Expected a number argument.`);
89
+ error.position = options.position ?? 1;
90
+ throw error;
91
+ }
92
+ return arg;
93
+ }
94
+
95
+ /**
96
+ * Check a string argument.
97
+ *
98
+ * @param {string} arg
99
+ * @param {string} operation
100
+ */
101
+ export function string(arg, operation, options = {}) {
102
+ if (typeof arg !== "string") {
103
+ /** @type {any} */
104
+ const error = new TypeError(`${operation}: Expected a string argument.`);
105
+ error.position = options.position ?? 1;
106
+ throw error;
107
+ }
108
+ return arg;
109
+ }
110
+
111
+ /**
112
+ * Check a stringlike argument.
113
+ *
114
+ * @param {import("../../index.ts").Stringlike} arg
115
+ * @param {string} operation
116
+ */
117
+ export function stringlike(arg, operation, options = {}) {
118
+ const result = toString(arg);
119
+ if (!result) {
120
+ /** @type {any} */
121
+ const error = new TypeError(
122
+ `${operation}: Expected a stringlike argument.`,
123
+ );
124
+ error.position = options.position ?? 1;
125
+ throw error;
126
+ }
127
+ return result;
128
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Functions that can be configured by async-tree consumers for interoperability.
3
+ */
4
+
5
+ export default {
6
+ warn(...args) {
7
+ console.warn(...args);
8
+ },
9
+ };
@@ -9,7 +9,7 @@ import * as symbols from "../symbols.js";
9
9
  * @typedef {import("../../index.ts").SyncOrAsyncMap} SyncOrAsyncMap
10
10
  *
11
11
  * @param {*} child
12
- * @param {SyncOrAsyncMap|null} parent
12
+ * @param {any} parent
13
13
  */
14
14
  export default function setParent(child, parent) {
15
15
  if (isMap(child)) {
@@ -14,10 +14,10 @@ describe("root", () => {
14
14
  },
15
15
  },
16
16
  },
17
- { deep: true }
17
+ { deep: true },
18
18
  );
19
19
  const c = await traverse(tree, "a", "b", "c");
20
- const r = await root(c);
20
+ const r = c ? await root(c) : undefined;
21
21
  assert.strictEqual(r, tree);
22
22
  });
23
23
  });
@@ -18,38 +18,7 @@ describe("traverse", () => {
18
18
  assert.equal(await traverse(tree), tree);
19
19
  assert.equal(await traverse(tree, "a1"), 1);
20
20
  assert.equal(await traverse(tree, "a2", "b2", "c2"), 4);
21
+ // Should return undefined instead of throwing
21
22
  assert.equal(await traverse(tree, "a2", "doesntexist", "c2"), undefined);
22
23
  });
23
-
24
- test("traverses a function with fixed number of arguments", async () => {
25
- const tree = (a, b) => ({
26
- c: "Result",
27
- });
28
- assert.equal(await traverse(tree, "a", "b", "c"), "Result");
29
- });
30
-
31
- test("traverses from one tree into another", async () => {
32
- const tree = new ObjectMap({
33
- a: {
34
- b: new Map([
35
- ["c", "Hello"],
36
- ["d", "Goodbye"],
37
- ]),
38
- },
39
- });
40
- assert.equal(await traverse(tree, "a", "b", "c"), "Hello");
41
- });
42
-
43
- test("unpacks last value if key ends in a slash", async () => {
44
- const tree = new ObjectMap({
45
- a: {
46
- b: Object.assign(new String("packed"), {
47
- unpack() {
48
- return "unpacked";
49
- },
50
- }),
51
- },
52
- });
53
- assert.equal(await traverse(tree, "a/", "b/"), "unpacked");
54
- });
55
24
  });
@@ -0,0 +1,73 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import ObjectMap from "../../src/drivers/ObjectMap.js";
4
+ import traverseOrThrow from "../../src/operations/traverseOrThrow.js";
5
+
6
+ describe("traverseOrThrow", () => {
7
+ test("traverses a path of keys", async () => {
8
+ const tree = new ObjectMap({
9
+ a1: 1,
10
+ a2: {
11
+ b1: 2,
12
+ b2: {
13
+ c1: 3,
14
+ c2: 4,
15
+ },
16
+ },
17
+ });
18
+ assert.equal(await traverseOrThrow(tree), tree);
19
+ assert.equal(await traverseOrThrow(tree, "a1"), 1);
20
+ assert.equal(await traverseOrThrow(tree, "a2", "b2", "c2"), 4);
21
+ assert.rejects(
22
+ async () => await traverseOrThrow(tree, "a2", "doesntexist", "c2"),
23
+ {
24
+ name: "TraverseError",
25
+ message: "A path hit a null or undefined value.",
26
+ },
27
+ );
28
+ });
29
+
30
+ test("traverses a function with fixed number of arguments", async () => {
31
+ const tree = (a, b) => ({
32
+ c: "Result",
33
+ });
34
+ assert.equal(await traverseOrThrow(tree, "a", "b", "c"), "Result");
35
+ });
36
+
37
+ test("traverses from one tree into another", async () => {
38
+ const tree = new ObjectMap({
39
+ a: {
40
+ b: new Map([
41
+ ["c", "Hello"],
42
+ ["d", "Goodbye"],
43
+ ]),
44
+ },
45
+ });
46
+ assert.equal(await traverseOrThrow(tree, "a", "b", "c"), "Hello");
47
+ });
48
+
49
+ test("unpacks last value if key ends in a slash", async () => {
50
+ const tree = new ObjectMap({
51
+ a: {
52
+ b: Object.assign(new String("packed"), {
53
+ unpack() {
54
+ return "unpacked";
55
+ },
56
+ }),
57
+ },
58
+ });
59
+ assert.equal(await traverseOrThrow(tree, "a/", "b/"), "unpacked");
60
+ });
61
+
62
+ test("throws if the last key ends in a slash but the value can't be unpacked", async () => {
63
+ const tree = new ObjectMap({
64
+ a: {
65
+ b: 1,
66
+ },
67
+ });
68
+ await assert.rejects(async () => await traverseOrThrow(tree, "a/", "b/"), {
69
+ name: "TraverseError",
70
+ message: "A path tried to unpack data that's already unpacked.",
71
+ });
72
+ });
73
+ });
@@ -1,38 +0,0 @@
1
- import from from "../operations/from.js";
2
- import isUnpackable from "./isUnpackable.js";
3
-
4
- /**
5
- * Convert the indicated argument to a map, or throw an exception.
6
- *
7
- * Tree operations can use this to validate the map argument and provide more
8
- * helpful error messages. This also unpacks a unpackable map argument.
9
- *
10
- * @typedef {import("../../index.ts").AsyncMap} AsyncMap
11
- * @typedef {import("../../index.ts").Maplike} Maplike
12
- * @typedef {import("../../index.ts").Unpackable} Unpackable
13
- *
14
- * @param {Maplike|Unpackable} maplike
15
- * @param {string} operation
16
- * @param {{ deep?: boolean, position?: number }} [options]
17
- * @returns {Promise<Map|AsyncMap>}
18
- */
19
- export default async function getMapArgument(maplike, operation, options = {}) {
20
- const deep = options.deep;
21
- const position = options.position ?? 0;
22
-
23
- if (isUnpackable(maplike)) {
24
- maplike = await maplike.unpack();
25
- }
26
-
27
- let map;
28
- try {
29
- map = from(maplike, { deep });
30
- } catch (/** @type {any} */ error) {
31
- let message = error.message ?? error;
32
- message = `${operation}: ${message}`;
33
- const newError = new TypeError(message);
34
- /** @type {any} */ (newError).position = position;
35
- throw newError;
36
- }
37
- return map;
38
- }