@valbuild/core 0.12.0 → 0.13.0

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 (71) hide show
  1. package/jest.config.js +4 -0
  2. package/package.json +1 -1
  3. package/src/Json.ts +4 -0
  4. package/src/expr/README.md +193 -0
  5. package/src/expr/eval.test.ts +202 -0
  6. package/src/expr/eval.ts +248 -0
  7. package/src/expr/expr.ts +91 -0
  8. package/src/expr/index.ts +3 -0
  9. package/src/expr/parser.test.ts +158 -0
  10. package/src/expr/parser.ts +229 -0
  11. package/src/expr/repl.ts +93 -0
  12. package/src/expr/tokenizer.test.ts +539 -0
  13. package/src/expr/tokenizer.ts +117 -0
  14. package/src/fetchVal.test.ts +164 -0
  15. package/src/fetchVal.ts +211 -0
  16. package/src/fp/array.ts +30 -0
  17. package/src/fp/index.ts +3 -0
  18. package/src/fp/result.ts +214 -0
  19. package/src/fp/util.ts +52 -0
  20. package/src/index.ts +55 -0
  21. package/src/initSchema.ts +45 -0
  22. package/src/initVal.ts +96 -0
  23. package/src/module.test.ts +170 -0
  24. package/src/module.ts +333 -0
  25. package/src/patch/deref.test.ts +300 -0
  26. package/src/patch/deref.ts +128 -0
  27. package/src/patch/index.ts +11 -0
  28. package/src/patch/json.test.ts +583 -0
  29. package/src/patch/json.ts +304 -0
  30. package/src/patch/operation.ts +74 -0
  31. package/src/patch/ops.ts +83 -0
  32. package/src/patch/parse.test.ts +202 -0
  33. package/src/patch/parse.ts +187 -0
  34. package/src/patch/patch.ts +46 -0
  35. package/src/patch/util.ts +67 -0
  36. package/src/schema/array.ts +52 -0
  37. package/src/schema/boolean.ts +38 -0
  38. package/src/schema/i18n.ts +65 -0
  39. package/src/schema/image.ts +70 -0
  40. package/src/schema/index.ts +46 -0
  41. package/src/schema/literal.ts +42 -0
  42. package/src/schema/number.ts +45 -0
  43. package/src/schema/object.ts +67 -0
  44. package/src/schema/oneOf.ts +60 -0
  45. package/src/schema/richtext.ts +417 -0
  46. package/src/schema/string.ts +49 -0
  47. package/src/schema/union.ts +62 -0
  48. package/src/selector/ExprProxy.test.ts +203 -0
  49. package/src/selector/ExprProxy.ts +209 -0
  50. package/src/selector/SelectorProxy.test.ts +172 -0
  51. package/src/selector/SelectorProxy.ts +237 -0
  52. package/src/selector/array.ts +37 -0
  53. package/src/selector/boolean.ts +4 -0
  54. package/src/selector/file.ts +14 -0
  55. package/src/selector/i18n.ts +13 -0
  56. package/src/selector/index.ts +159 -0
  57. package/src/selector/number.ts +4 -0
  58. package/src/selector/object.ts +22 -0
  59. package/src/selector/primitive.ts +17 -0
  60. package/src/selector/remote.ts +9 -0
  61. package/src/selector/selector.test.ts +453 -0
  62. package/src/selector/selectorOf.ts +7 -0
  63. package/src/selector/string.ts +4 -0
  64. package/src/source/file.ts +45 -0
  65. package/src/source/i18n.ts +60 -0
  66. package/src/source/index.ts +50 -0
  67. package/src/source/remote.ts +54 -0
  68. package/src/val/array.ts +10 -0
  69. package/src/val/index.ts +90 -0
  70. package/src/val/object.ts +13 -0
  71. package/src/val/primitive.ts +8 -0
@@ -0,0 +1,304 @@
1
+ import { result, array, pipe } from "../fp/";
2
+ import { JSONValue, Ops, PatchError, ReadonlyJSONValue } from "./ops";
3
+ import {
4
+ deepClone,
5
+ deepEqual,
6
+ isNotRoot,
7
+ parseAndValidateArrayIndex,
8
+ } from "./util";
9
+
10
+ type JSONOpsResult<T> = result.Result<T, PatchError>;
11
+
12
+ function parseAndValidateArrayInsertIndex(
13
+ key: string,
14
+ nodes: ReadonlyJSONValue[]
15
+ ): JSONOpsResult<number> {
16
+ if (key === "-") {
17
+ return result.ok(nodes.length);
18
+ }
19
+
20
+ return pipe(
21
+ parseAndValidateArrayIndex(key),
22
+ result.filterOrElse(
23
+ (index: number): boolean => index <= nodes.length,
24
+ () => new PatchError("Array index out of bounds")
25
+ )
26
+ );
27
+ }
28
+
29
+ function parseAndValidateArrayInboundsIndex(
30
+ key: string,
31
+ nodes: ReadonlyJSONValue[]
32
+ ): JSONOpsResult<number> {
33
+ return pipe(
34
+ parseAndValidateArrayIndex(key),
35
+ result.filterOrElse(
36
+ (index: number): boolean => index < nodes.length,
37
+ () => new PatchError("Array index out of bounds")
38
+ )
39
+ );
40
+ }
41
+
42
+ function replaceInNode(
43
+ node: JSONValue,
44
+ key: string,
45
+ value: JSONValue
46
+ ): JSONOpsResult<JSONValue> {
47
+ if (Array.isArray(node)) {
48
+ return pipe(
49
+ parseAndValidateArrayInboundsIndex(key, node),
50
+ result.map((index: number) => {
51
+ const replaced = node[index];
52
+ node[index] = value;
53
+ return replaced;
54
+ })
55
+ );
56
+ } else if (typeof node === "object" && node !== null) {
57
+ // Prototype pollution protection
58
+ if (Object.prototype.hasOwnProperty.call(node, key)) {
59
+ const replaced = node[key];
60
+ node[key] = value;
61
+ return result.ok(replaced);
62
+ } else {
63
+ return result.err(
64
+ new PatchError("Cannot replace object element which does not exist")
65
+ );
66
+ }
67
+ }
68
+
69
+ return result.err(new PatchError("Cannot replace in non-object/array"));
70
+ }
71
+
72
+ function replaceAtPath(
73
+ document: JSONValue,
74
+ path: string[],
75
+ value: JSONValue
76
+ ): JSONOpsResult<[document: JSONValue, replaced: JSONValue]> {
77
+ if (isNotRoot(path)) {
78
+ return pipe(
79
+ getPointerFromPath(document, path),
80
+ result.flatMap(([node, key]: Pointer) => replaceInNode(node, key, value)),
81
+ result.map((replaced: JSONValue) => [document, replaced])
82
+ );
83
+ } else {
84
+ return result.ok([value, document]);
85
+ }
86
+ }
87
+
88
+ function getFromNode(
89
+ node: JSONValue,
90
+ key: string
91
+ ): JSONOpsResult<JSONValue | undefined> {
92
+ if (Array.isArray(node)) {
93
+ return pipe(
94
+ parseAndValidateArrayIndex(key),
95
+ result.flatMap((index: number) => {
96
+ if (index >= node.length) {
97
+ return result.err(new PatchError("Array index out of bounds"));
98
+ } else {
99
+ return result.ok(node[index]);
100
+ }
101
+ })
102
+ );
103
+ } else if (typeof node === "object" && node !== null) {
104
+ // Prototype pollution protection
105
+ if (Object.prototype.hasOwnProperty.call(node, key)) {
106
+ return result.ok(node[key]);
107
+ } else {
108
+ return result.ok(undefined);
109
+ }
110
+ }
111
+
112
+ return result.err(new PatchError("Cannot access non-object/array"));
113
+ }
114
+
115
+ type Pointer = [node: JSONValue, key: string];
116
+ function getPointerFromPath(
117
+ node: JSONValue,
118
+ path: array.NonEmptyArray<string>
119
+ ): JSONOpsResult<Pointer> {
120
+ let targetNode: JSONValue = node;
121
+ let key: string = path[0];
122
+ for (let i = 0; i < path.length - 1; ++i, key = path[i]) {
123
+ const childNode = getFromNode(targetNode, key);
124
+ if (result.isErr(childNode)) {
125
+ return childNode;
126
+ }
127
+ if (childNode.value === undefined) {
128
+ return result.err(
129
+ new PatchError("Path refers to non-existing object/array")
130
+ );
131
+ }
132
+ targetNode = childNode.value;
133
+ }
134
+
135
+ return result.ok([targetNode, key]);
136
+ }
137
+
138
+ function getAtPath(node: JSONValue, path: string[]): JSONOpsResult<JSONValue> {
139
+ return pipe(
140
+ path,
141
+ result.flatMapReduce(
142
+ (node: JSONValue, key: string) =>
143
+ pipe(
144
+ getFromNode(node, key),
145
+ result.filterOrElse(
146
+ (childNode: JSONValue | undefined): childNode is JSONValue =>
147
+ childNode !== undefined,
148
+ () => new PatchError("Path refers to non-existing object/array")
149
+ )
150
+ ),
151
+ node
152
+ )
153
+ );
154
+ }
155
+
156
+ function removeFromNode(
157
+ node: JSONValue,
158
+ key: string
159
+ ): JSONOpsResult<JSONValue> {
160
+ if (Array.isArray(node)) {
161
+ return pipe(
162
+ parseAndValidateArrayInboundsIndex(key, node),
163
+ result.map((index: number) => {
164
+ const [removed] = node.splice(index, 1);
165
+ return removed;
166
+ })
167
+ );
168
+ } else if (typeof node === "object" && node !== null) {
169
+ // Prototype pollution protection
170
+ if (Object.prototype.hasOwnProperty.call(node, key)) {
171
+ const removed = node[key];
172
+ delete node[key];
173
+ return result.ok(removed);
174
+ }
175
+ }
176
+
177
+ return result.err(new PatchError("Cannot remove from non-object/array"));
178
+ }
179
+
180
+ function removeAtPath(
181
+ document: JSONValue,
182
+ path: array.NonEmptyArray<string>
183
+ ): JSONOpsResult<JSONValue> {
184
+ return pipe(
185
+ getPointerFromPath(document, path),
186
+ result.flatMap(([node, key]: Pointer) => removeFromNode(node, key))
187
+ );
188
+ }
189
+
190
+ function addToNode(
191
+ node: JSONValue,
192
+ key: string,
193
+ value: JSONValue
194
+ ): JSONOpsResult<JSONValue | undefined> {
195
+ if (Array.isArray(node)) {
196
+ return pipe(
197
+ parseAndValidateArrayInsertIndex(key, node),
198
+ result.map((index: number) => {
199
+ node.splice(index, 0, value);
200
+ return undefined;
201
+ })
202
+ );
203
+ } else if (typeof node === "object" && node !== null) {
204
+ let replaced: JSONValue | undefined;
205
+ // Prototype pollution protection
206
+ if (Object.prototype.hasOwnProperty.call(node, key)) {
207
+ replaced = node[key];
208
+ }
209
+ node[key] = value;
210
+ return result.ok(replaced);
211
+ }
212
+
213
+ return result.err(new PatchError("Cannot add to non-object/array"));
214
+ }
215
+
216
+ function addAtPath(
217
+ document: JSONValue,
218
+ path: string[],
219
+ value: JSONValue
220
+ ): JSONOpsResult<[document: JSONValue, replaced?: JSONValue]> {
221
+ if (isNotRoot(path)) {
222
+ return pipe(
223
+ getPointerFromPath(document, path),
224
+ result.flatMap(([node, key]: Pointer) => addToNode(node, key, value)),
225
+ result.map((replaced: JSONValue | undefined) => [document, replaced])
226
+ );
227
+ } else {
228
+ return result.ok([value, document]);
229
+ }
230
+ }
231
+
232
+ function pickDocument<
233
+ T extends readonly [document: ReadonlyJSONValue, ...result: unknown[]]
234
+ >([document]: T): T[0] {
235
+ return document;
236
+ }
237
+
238
+ export class JSONOps implements Ops<JSONValue, never> {
239
+ get(
240
+ document: JSONValue,
241
+ path: string[]
242
+ ): result.Result<JSONValue, PatchError> {
243
+ return getAtPath(document, path);
244
+ }
245
+ add(
246
+ document: JSONValue,
247
+ path: string[],
248
+ value: JSONValue
249
+ ): result.Result<JSONValue, PatchError> {
250
+ return pipe(addAtPath(document, path, value), result.map(pickDocument));
251
+ }
252
+ remove(
253
+ document: JSONValue,
254
+ path: array.NonEmptyArray<string>
255
+ ): result.Result<JSONValue, PatchError> {
256
+ return pipe(
257
+ removeAtPath(document, path),
258
+ result.map(() => document)
259
+ );
260
+ }
261
+ replace(
262
+ document: JSONValue,
263
+ path: string[],
264
+ value: JSONValue
265
+ ): result.Result<JSONValue, PatchError> {
266
+ return pipe(replaceAtPath(document, path, value), result.map(pickDocument));
267
+ }
268
+ move(
269
+ document: JSONValue,
270
+ from: array.NonEmptyArray<string>,
271
+ path: string[]
272
+ ): result.Result<JSONValue, PatchError> {
273
+ return pipe(
274
+ removeAtPath(document, from),
275
+ result.flatMap((removed: JSONValue) =>
276
+ addAtPath(document, path, removed)
277
+ ),
278
+ result.map(pickDocument)
279
+ );
280
+ }
281
+ copy(
282
+ document: JSONValue,
283
+ from: string[],
284
+ path: string[]
285
+ ): result.Result<JSONValue, PatchError> {
286
+ return pipe(
287
+ getAtPath(document, from),
288
+ result.flatMap((value: JSONValue) =>
289
+ addAtPath(document, path, deepClone(value))
290
+ ),
291
+ result.map(pickDocument)
292
+ );
293
+ }
294
+ test(
295
+ document: JSONValue,
296
+ path: string[],
297
+ value: JSONValue
298
+ ): result.Result<boolean, PatchError> {
299
+ return pipe(
300
+ getAtPath(document, path),
301
+ result.map((documentValue: JSONValue) => deepEqual(value, documentValue))
302
+ );
303
+ }
304
+ }
@@ -0,0 +1,74 @@
1
+ import { array } from "../fp";
2
+ import type { JSONValue } from "./ops";
3
+
4
+ /**
5
+ * Raw JSON patch operation.
6
+ */
7
+ export type OperationJSON =
8
+ | {
9
+ op: "add";
10
+ path: string;
11
+ value: JSONValue;
12
+ }
13
+ | {
14
+ op: "remove";
15
+ path: string;
16
+ }
17
+ | {
18
+ op: "replace";
19
+ path: string;
20
+ value: JSONValue;
21
+ }
22
+ | {
23
+ op: "move";
24
+ from: string;
25
+ path: string;
26
+ }
27
+ | {
28
+ op: "copy";
29
+ from: string;
30
+ path: string;
31
+ }
32
+ | {
33
+ op: "test";
34
+ path: string;
35
+ value: JSONValue;
36
+ };
37
+
38
+ /**
39
+ * Parsed form of JSON patch operation.
40
+ */
41
+ export type Operation =
42
+ | {
43
+ op: "add";
44
+ path: string[];
45
+ value: JSONValue;
46
+ }
47
+ | {
48
+ op: "remove";
49
+ path: array.NonEmptyArray<string>;
50
+ }
51
+ | {
52
+ op: "replace";
53
+ path: string[];
54
+ value: JSONValue;
55
+ }
56
+ | {
57
+ op: "move";
58
+ /**
59
+ * Must be non-root and not a proper prefix of "path".
60
+ */
61
+ // TODO: Replace with common prefix field
62
+ from: array.NonEmptyArray<string>;
63
+ path: string[];
64
+ }
65
+ | {
66
+ op: "copy";
67
+ from: string[];
68
+ path: string[];
69
+ }
70
+ | {
71
+ op: "test";
72
+ path: string[];
73
+ value: JSONValue;
74
+ };
@@ -0,0 +1,83 @@
1
+ import { result, array } from "../fp";
2
+
3
+ export class PatchError {
4
+ constructor(public message: string) {}
5
+ }
6
+
7
+ export type ReadonlyJSONValue =
8
+ | string
9
+ | number
10
+ | boolean
11
+ | null
12
+ | readonly ReadonlyJSONValue[]
13
+ | {
14
+ readonly [key: string]: ReadonlyJSONValue;
15
+ };
16
+
17
+ export type JSONValue =
18
+ | string
19
+ | number
20
+ | boolean
21
+ | null
22
+ | JSONValue[]
23
+ | {
24
+ [key: string]: JSONValue;
25
+ };
26
+
27
+ type ToMutableJSONArray<T extends readonly ReadonlyJSONValue[]> = {
28
+ [P in keyof T]: ToMutable<T[P]>;
29
+ };
30
+ export type ToMutable<T extends ReadonlyJSONValue> = JSONValue extends T
31
+ ? JSONValue
32
+ : T extends readonly ReadonlyJSONValue[]
33
+ ? ToMutableJSONArray<T>
34
+ : T extends { readonly [key: string]: ReadonlyJSONValue }
35
+ ? { -readonly [P in keyof T]: ToMutable<T[P]> }
36
+ : T;
37
+
38
+ type ToReadonlyJSONArray<T extends readonly ReadonlyJSONValue[]> = {
39
+ readonly [P in keyof T]: ToReadonly<T[P]>;
40
+ };
41
+ export type ToReadonly<T extends ReadonlyJSONValue> = JSONValue extends T
42
+ ? ReadonlyJSONValue
43
+ : T extends readonly ReadonlyJSONValue[]
44
+ ? ToReadonlyJSONArray<T>
45
+ : T extends { readonly [key: string]: ReadonlyJSONValue }
46
+ ? { readonly [P in keyof T]: ToReadonly<T[P]> }
47
+ : T;
48
+
49
+ /**
50
+ * NOTE: MAY mutate the input document.
51
+ */
52
+ export interface Ops<T, E> {
53
+ add(
54
+ document: T,
55
+ path: string[],
56
+ value: JSONValue
57
+ ): result.Result<T, E | PatchError>;
58
+ remove(
59
+ document: T,
60
+ path: array.NonEmptyArray<string>
61
+ ): result.Result<T, E | PatchError>;
62
+ replace(
63
+ document: T,
64
+ path: string[],
65
+ value: JSONValue
66
+ ): result.Result<T, E | PatchError>;
67
+ move(
68
+ document: T,
69
+ from: array.NonEmptyArray<string>,
70
+ path: string[]
71
+ ): result.Result<T, E | PatchError>;
72
+ copy(
73
+ document: T,
74
+ from: string[],
75
+ path: string[]
76
+ ): result.Result<T, E | PatchError>;
77
+ test(
78
+ document: T,
79
+ path: string[],
80
+ value: JSONValue
81
+ ): result.Result<boolean, E | PatchError>;
82
+ get(document: T, path: string[]): result.Result<JSONValue, E | PatchError>;
83
+ }
@@ -0,0 +1,202 @@
1
+ import {
2
+ formatJSONPointer,
3
+ parseJSONPointer,
4
+ parseOperation,
5
+ StaticPatchIssue,
6
+ } from "./parse";
7
+ import * as result from "../fp/result";
8
+ import { Operation, OperationJSON } from "./operation";
9
+ import { array } from "../fp";
10
+
11
+ describe("parseOperation", () => {
12
+ test.each<{
13
+ name: string;
14
+ value: OperationJSON;
15
+ expected: Operation;
16
+ }>([
17
+ {
18
+ name: "basic add operation",
19
+ value: {
20
+ op: "add",
21
+ path: "/",
22
+ value: null,
23
+ },
24
+ expected: {
25
+ op: "add",
26
+ path: [],
27
+ value: null,
28
+ },
29
+ },
30
+ {
31
+ name: "basic remove operation",
32
+ value: {
33
+ op: "remove",
34
+ path: "/foo",
35
+ },
36
+ expected: {
37
+ op: "remove",
38
+ path: ["foo"],
39
+ },
40
+ },
41
+ {
42
+ name: "basic replace operation",
43
+ value: {
44
+ op: "replace",
45
+ path: "/",
46
+ value: null,
47
+ },
48
+ expected: {
49
+ op: "replace",
50
+ path: [],
51
+ value: null,
52
+ },
53
+ },
54
+ {
55
+ name: "basic move operation",
56
+ value: {
57
+ op: "move",
58
+ from: "/foo",
59
+ path: "/bar",
60
+ },
61
+ expected: {
62
+ op: "move",
63
+ from: ["foo"],
64
+ path: ["bar"],
65
+ },
66
+ },
67
+ {
68
+ name: "basic copy operation",
69
+ value: {
70
+ op: "copy",
71
+ from: "/foo",
72
+ path: "/bar",
73
+ },
74
+ expected: {
75
+ op: "copy",
76
+ from: ["foo"],
77
+ path: ["bar"],
78
+ },
79
+ },
80
+ {
81
+ name: "basic test operation",
82
+ value: {
83
+ op: "test",
84
+ path: "/",
85
+ value: null,
86
+ },
87
+ expected: {
88
+ op: "test",
89
+ path: [],
90
+ value: null,
91
+ },
92
+ },
93
+ ])("$name is valid", ({ value, expected }) => {
94
+ const res = parseOperation(value);
95
+ expect(res).toEqual(result.ok(expected));
96
+ });
97
+
98
+ test.each<{
99
+ name: string;
100
+ value: OperationJSON;
101
+ errors: array.NonEmptyArray<string[]>;
102
+ }>([
103
+ {
104
+ name: "add operation with empty path",
105
+ value: {
106
+ op: "add",
107
+ path: "",
108
+ value: null,
109
+ },
110
+ errors: [["path"]],
111
+ },
112
+ {
113
+ name: "remove root",
114
+ value: {
115
+ op: "remove",
116
+ path: "/",
117
+ },
118
+ errors: [["path"]],
119
+ },
120
+ {
121
+ name: "move from root",
122
+ value: {
123
+ op: "move",
124
+ from: "/",
125
+ path: "/",
126
+ },
127
+ errors: [["from"]],
128
+ },
129
+ {
130
+ name: "move from prefix of path",
131
+ value: {
132
+ op: "move",
133
+ from: "/foo",
134
+ path: "/foo/bar",
135
+ },
136
+ errors: [["from"]],
137
+ },
138
+ ])("$name is invalid", ({ value, errors }) => {
139
+ expect(parseOperation(value)).toEqual(
140
+ result.err(
141
+ expect.arrayContaining<array.NonEmptyArray<StaticPatchIssue>>(
142
+ errors.map((path) =>
143
+ expect.objectContaining<StaticPatchIssue>({
144
+ path,
145
+ message: expect.anything(),
146
+ })
147
+ )
148
+ )
149
+ )
150
+ );
151
+ });
152
+ });
153
+
154
+ const JSONPointerTestCases: { str: string; arr: string[] }[] = [
155
+ {
156
+ str: "/",
157
+ arr: [],
158
+ },
159
+ {
160
+ str: "/foo",
161
+ arr: ["foo"],
162
+ },
163
+ {
164
+ str: "/foo/",
165
+ arr: ["foo", ""],
166
+ },
167
+ {
168
+ str: "/~1",
169
+ arr: ["/"],
170
+ },
171
+ {
172
+ str: "/~1/~1",
173
+ arr: ["/", "/"],
174
+ },
175
+ {
176
+ str: "/~0",
177
+ arr: ["~"],
178
+ },
179
+ {
180
+ str: "/~0/~0",
181
+ arr: ["~", "~"],
182
+ },
183
+ ];
184
+
185
+ describe("parseJSONPointer", () => {
186
+ test.each(JSONPointerTestCases)("valid: $str", ({ str, arr }) => {
187
+ expect(parseJSONPointer(str)).toEqual(result.ok(arr));
188
+ });
189
+
190
+ test.each(["", "foo", "foo/bar", "/~2", "/~"])(
191
+ "invalid: %s",
192
+ (path: string) => {
193
+ expect(parseJSONPointer(path)).toEqual(result.err(expect.any(String)));
194
+ }
195
+ );
196
+ });
197
+
198
+ describe("formatJSONPointer", () => {
199
+ test.each(JSONPointerTestCases)("$str", ({ str, arr }) => {
200
+ expect(formatJSONPointer(arr)).toEqual(str);
201
+ });
202
+ });