@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.
- package/jest.config.js +4 -0
- package/package.json +1 -1
- package/src/Json.ts +4 -0
- package/src/expr/README.md +193 -0
- package/src/expr/eval.test.ts +202 -0
- package/src/expr/eval.ts +248 -0
- package/src/expr/expr.ts +91 -0
- package/src/expr/index.ts +3 -0
- package/src/expr/parser.test.ts +158 -0
- package/src/expr/parser.ts +229 -0
- package/src/expr/repl.ts +93 -0
- package/src/expr/tokenizer.test.ts +539 -0
- package/src/expr/tokenizer.ts +117 -0
- package/src/fetchVal.test.ts +164 -0
- package/src/fetchVal.ts +211 -0
- package/src/fp/array.ts +30 -0
- package/src/fp/index.ts +3 -0
- package/src/fp/result.ts +214 -0
- package/src/fp/util.ts +52 -0
- package/src/index.ts +55 -0
- package/src/initSchema.ts +45 -0
- package/src/initVal.ts +96 -0
- package/src/module.test.ts +170 -0
- package/src/module.ts +333 -0
- package/src/patch/deref.test.ts +300 -0
- package/src/patch/deref.ts +128 -0
- package/src/patch/index.ts +11 -0
- package/src/patch/json.test.ts +583 -0
- package/src/patch/json.ts +304 -0
- package/src/patch/operation.ts +74 -0
- package/src/patch/ops.ts +83 -0
- package/src/patch/parse.test.ts +202 -0
- package/src/patch/parse.ts +187 -0
- package/src/patch/patch.ts +46 -0
- package/src/patch/util.ts +67 -0
- package/src/schema/array.ts +52 -0
- package/src/schema/boolean.ts +38 -0
- package/src/schema/i18n.ts +65 -0
- package/src/schema/image.ts +70 -0
- package/src/schema/index.ts +46 -0
- package/src/schema/literal.ts +42 -0
- package/src/schema/number.ts +45 -0
- package/src/schema/object.ts +67 -0
- package/src/schema/oneOf.ts +60 -0
- package/src/schema/richtext.ts +417 -0
- package/src/schema/string.ts +49 -0
- package/src/schema/union.ts +62 -0
- package/src/selector/ExprProxy.test.ts +203 -0
- package/src/selector/ExprProxy.ts +209 -0
- package/src/selector/SelectorProxy.test.ts +172 -0
- package/src/selector/SelectorProxy.ts +237 -0
- package/src/selector/array.ts +37 -0
- package/src/selector/boolean.ts +4 -0
- package/src/selector/file.ts +14 -0
- package/src/selector/i18n.ts +13 -0
- package/src/selector/index.ts +159 -0
- package/src/selector/number.ts +4 -0
- package/src/selector/object.ts +22 -0
- package/src/selector/primitive.ts +17 -0
- package/src/selector/remote.ts +9 -0
- package/src/selector/selector.test.ts +453 -0
- package/src/selector/selectorOf.ts +7 -0
- package/src/selector/string.ts +4 -0
- package/src/source/file.ts +45 -0
- package/src/source/i18n.ts +60 -0
- package/src/source/index.ts +50 -0
- package/src/source/remote.ts +54 -0
- package/src/val/array.ts +10 -0
- package/src/val/index.ts +90 -0
- package/src/val/object.ts +13 -0
- 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
|
+
};
|
package/src/patch/ops.ts
ADDED
@@ -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
|
+
});
|