@crewhaus/tool-data 0.7.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/README.md +193 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +988 -0
- package/dist/lib/csv.d.ts +80 -0
- package/dist/lib/csv.js +240 -0
- package/dist/lib/diff.d.ts +44 -0
- package/dist/lib/diff.js +166 -0
- package/dist/lib/json.d.ts +65 -0
- package/dist/lib/json.js +133 -0
- package/dist/lib/jsonl.d.ts +39 -0
- package/dist/lib/jsonl.js +63 -0
- package/dist/lib/jsonpath.d.ts +91 -0
- package/dist/lib/jsonpath.js +430 -0
- package/dist/lib/patch.d.ts +72 -0
- package/dist/lib/patch.js +272 -0
- package/dist/lib/table.d.ts +156 -0
- package/dist/lib/table.js +571 -0
- package/dist/lib/toml.d.ts +59 -0
- package/dist/lib/toml.js +530 -0
- package/dist/lib/xml.d.ts +72 -0
- package/dist/lib/xml.js +344 -0
- package/dist/lib/yaml.d.ts +61 -0
- package/dist/lib/yaml.js +598 -0
- package/package.json +45 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small JSONPath-like query language, implemented here rather than taken
|
|
3
|
+
* from a dependency so its grammar is exactly what is documented and nothing
|
|
4
|
+
* more.
|
|
5
|
+
*
|
|
6
|
+
* ## Supported grammar
|
|
7
|
+
*
|
|
8
|
+
* ```
|
|
9
|
+
* path := "$"? step*
|
|
10
|
+
* step := "." name
|
|
11
|
+
* | ".." name recursive descent to every `name` at any depth
|
|
12
|
+
* | ".." "*" every node at any depth
|
|
13
|
+
* | "." "*" | "[*]" every element of an array / every value of an object
|
|
14
|
+
* | "[" index "]" integer, negative counts from the end
|
|
15
|
+
* | "[" slice "]" start:end or start:end:step (Python semantics)
|
|
16
|
+
* | "[" quoted "]" 'key' or "key" — for keys with dots or spaces
|
|
17
|
+
* | "[?(" filter ")]" keep array elements / object values matching filter
|
|
18
|
+
* filter := "@" ("." name)* (op literal)?
|
|
19
|
+
* op := == | != | < | <= | > | >= | =~
|
|
20
|
+
* literal := number | 'string' | "string" | true | false | null
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* A filter with no operator is an existence test: `[?(@.email)]` keeps
|
|
24
|
+
* elements whose `email` is defined and neither null nor false. `=~` matches
|
|
25
|
+
* the string form of the field against a JavaScript regular expression
|
|
26
|
+
* source (no flags).
|
|
27
|
+
*
|
|
28
|
+
* ## Deliberately not supported
|
|
29
|
+
*
|
|
30
|
+
* Unions (`['a','b']`), script expressions beyond the filter grammar above,
|
|
31
|
+
* `length()` and other functions, parent axes, and boolean combinators
|
|
32
|
+
* (`&&`, `||`) inside a filter. Chain two filter steps instead of using
|
|
33
|
+
* `&&`. Anything unsupported raises a parse error naming the offending
|
|
34
|
+
* position, never a silent empty result.
|
|
35
|
+
*/
|
|
36
|
+
import { isPlainObject } from "./json";
|
|
37
|
+
export class PathError extends Error {
|
|
38
|
+
}
|
|
39
|
+
const IDENT = /^[A-Za-z_$][A-Za-z0-9_$-]*/;
|
|
40
|
+
/** Match a leading identifier, or null when the text does not start with one. */
|
|
41
|
+
function leadingIdent(text) {
|
|
42
|
+
const m = IDENT.test(text) ? text.match(IDENT) : null;
|
|
43
|
+
return m === null ? null : m[0];
|
|
44
|
+
}
|
|
45
|
+
/** Parse a path expression into steps, or throw a `PathError` saying where it broke. */
|
|
46
|
+
export function parsePath(expr) {
|
|
47
|
+
const steps = [];
|
|
48
|
+
let i = 0;
|
|
49
|
+
if (expr.startsWith("$"))
|
|
50
|
+
i = 1;
|
|
51
|
+
const fail = (msg) => {
|
|
52
|
+
throw new PathError(`${msg} at position ${i} of ${JSON.stringify(expr)}`);
|
|
53
|
+
};
|
|
54
|
+
while (i < expr.length) {
|
|
55
|
+
const ch = expr[i];
|
|
56
|
+
if (ch === ".") {
|
|
57
|
+
if (expr[i + 1] === ".") {
|
|
58
|
+
i += 2;
|
|
59
|
+
if (expr[i] === "*") {
|
|
60
|
+
i += 1;
|
|
61
|
+
steps.push({ kind: "descend", name: null });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const name = leadingIdent(expr.slice(i));
|
|
65
|
+
if (name === null)
|
|
66
|
+
fail("expected a name after '..'");
|
|
67
|
+
i += name.length;
|
|
68
|
+
steps.push({ kind: "descend", name: name });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
i += 1;
|
|
72
|
+
if (expr[i] === "*") {
|
|
73
|
+
i += 1;
|
|
74
|
+
steps.push({ kind: "wildcard" });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const name = leadingIdent(expr.slice(i));
|
|
78
|
+
if (name === null)
|
|
79
|
+
fail("expected a name after '.'");
|
|
80
|
+
i += name.length;
|
|
81
|
+
steps.push({ kind: "child", name: name });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (ch === "[") {
|
|
85
|
+
i += 1;
|
|
86
|
+
const close = findBracketEnd(expr, i);
|
|
87
|
+
if (close < 0)
|
|
88
|
+
fail("unclosed '['");
|
|
89
|
+
const inner = expr.slice(i, close);
|
|
90
|
+
steps.push(parseBracket(inner, expr, i));
|
|
91
|
+
i = close + 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const bare = steps.length === 0 ? leadingIdent(expr.slice(i)) : null;
|
|
95
|
+
if (bare !== null) {
|
|
96
|
+
// Allow a leading bare name: `users[0].name` means `$.users[0].name`.
|
|
97
|
+
i += bare.length;
|
|
98
|
+
steps.push({ kind: "child", name: bare });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
fail(`unexpected character ${JSON.stringify(ch)}`);
|
|
102
|
+
}
|
|
103
|
+
return steps;
|
|
104
|
+
}
|
|
105
|
+
/** Find the `]` that closes the bracket opened before `from`, skipping quoted text. */
|
|
106
|
+
function findBracketEnd(expr, from) {
|
|
107
|
+
let depth = 1;
|
|
108
|
+
let quote = null;
|
|
109
|
+
for (let i = from; i < expr.length; i++) {
|
|
110
|
+
const c = expr[i];
|
|
111
|
+
if (quote !== null) {
|
|
112
|
+
if (c === "\\")
|
|
113
|
+
i += 1;
|
|
114
|
+
else if (c === quote)
|
|
115
|
+
quote = null;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (c === "'" || c === '"') {
|
|
119
|
+
quote = c;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (c === "[")
|
|
123
|
+
depth += 1;
|
|
124
|
+
else if (c === "]") {
|
|
125
|
+
depth -= 1;
|
|
126
|
+
if (depth === 0)
|
|
127
|
+
return i;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return -1;
|
|
131
|
+
}
|
|
132
|
+
function parseBracket(inner, whole, at) {
|
|
133
|
+
const body = inner.trim();
|
|
134
|
+
const fail = (msg) => {
|
|
135
|
+
throw new PathError(`${msg} at position ${at} of ${JSON.stringify(whole)}`);
|
|
136
|
+
};
|
|
137
|
+
if (body === "*")
|
|
138
|
+
return { kind: "wildcard" };
|
|
139
|
+
if (body.startsWith("?(") && body.endsWith(")")) {
|
|
140
|
+
return parseFilter(body.slice(2, -1).trim(), whole, at);
|
|
141
|
+
}
|
|
142
|
+
if (body.startsWith("?"))
|
|
143
|
+
return parseFilter(body.slice(1).trim(), whole, at);
|
|
144
|
+
if ((body.startsWith("'") && body.endsWith("'")) ||
|
|
145
|
+
(body.startsWith('"') && body.endsWith('"'))) {
|
|
146
|
+
if (body.length < 2)
|
|
147
|
+
fail("empty quoted key");
|
|
148
|
+
return { kind: "child", name: unquote(body) };
|
|
149
|
+
}
|
|
150
|
+
if (body.includes(":")) {
|
|
151
|
+
const parts = body.split(":");
|
|
152
|
+
if (parts.length > 3)
|
|
153
|
+
fail("a slice takes at most start:end:step");
|
|
154
|
+
const num = (s, label) => {
|
|
155
|
+
if (s === undefined || s.trim() === "")
|
|
156
|
+
return null;
|
|
157
|
+
const n = Number(s.trim());
|
|
158
|
+
if (!Number.isInteger(n))
|
|
159
|
+
fail(`slice ${label} must be an integer`);
|
|
160
|
+
return n;
|
|
161
|
+
};
|
|
162
|
+
const step = num(parts[2], "step") ?? 1;
|
|
163
|
+
if (step === 0)
|
|
164
|
+
fail("slice step cannot be 0");
|
|
165
|
+
return { kind: "slice", start: num(parts[0], "start"), end: num(parts[1], "end"), step };
|
|
166
|
+
}
|
|
167
|
+
const n = Number(body);
|
|
168
|
+
if (body !== "" && Number.isInteger(n))
|
|
169
|
+
return { kind: "index", index: n };
|
|
170
|
+
if (body === "")
|
|
171
|
+
fail("empty '[]'");
|
|
172
|
+
fail(`cannot read ${JSON.stringify(body)} as an index, slice, quoted key or filter`);
|
|
173
|
+
return { kind: "wildcard" };
|
|
174
|
+
}
|
|
175
|
+
function unquote(s) {
|
|
176
|
+
return s.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
177
|
+
}
|
|
178
|
+
function parseFilter(body, whole, at) {
|
|
179
|
+
const fail = (msg) => {
|
|
180
|
+
throw new PathError(`${msg} at position ${at} of ${JSON.stringify(whole)}`);
|
|
181
|
+
};
|
|
182
|
+
if (!body.startsWith("@"))
|
|
183
|
+
fail("a filter must start with '@'");
|
|
184
|
+
let rest = body.slice(1);
|
|
185
|
+
const field = [];
|
|
186
|
+
while (rest.startsWith(".") || rest.startsWith("[")) {
|
|
187
|
+
if (rest.startsWith(".")) {
|
|
188
|
+
const name = leadingIdent(rest.slice(1));
|
|
189
|
+
if (name === null)
|
|
190
|
+
break;
|
|
191
|
+
field.push(name);
|
|
192
|
+
rest = rest.slice(1 + name.length);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
const end = findBracketEnd(rest, 1);
|
|
196
|
+
if (end < 0)
|
|
197
|
+
fail("unclosed '[' in a filter");
|
|
198
|
+
field.push(unquoteOrRaw(rest.slice(1, end).trim()));
|
|
199
|
+
rest = rest.slice(end + 1);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
rest = rest.trim();
|
|
203
|
+
if (rest === "")
|
|
204
|
+
return { kind: "filter", field, op: null, literal: undefined };
|
|
205
|
+
const ops = ["==", "!=", "<=", ">=", "=~", "<", ">"];
|
|
206
|
+
const found = ops.find((o) => rest.startsWith(o));
|
|
207
|
+
if (found === undefined)
|
|
208
|
+
fail(`expected a comparison operator, got ${JSON.stringify(rest)}`);
|
|
209
|
+
const op = found;
|
|
210
|
+
const literalText = rest.slice(op.length).trim();
|
|
211
|
+
return { kind: "filter", field, op, literal: parseLiteral(literalText, whole, at) };
|
|
212
|
+
}
|
|
213
|
+
function unquoteOrRaw(s) {
|
|
214
|
+
if ((s.startsWith("'") && s.endsWith("'")) || (s.startsWith('"') && s.endsWith('"'))) {
|
|
215
|
+
return unquote(s);
|
|
216
|
+
}
|
|
217
|
+
return s;
|
|
218
|
+
}
|
|
219
|
+
function parseLiteral(text, whole, at) {
|
|
220
|
+
if (text === "true")
|
|
221
|
+
return true;
|
|
222
|
+
if (text === "false")
|
|
223
|
+
return false;
|
|
224
|
+
if (text === "null")
|
|
225
|
+
return null;
|
|
226
|
+
if ((text.startsWith("'") && text.endsWith("'")) ||
|
|
227
|
+
(text.startsWith('"') && text.endsWith('"'))) {
|
|
228
|
+
if (text.length >= 2)
|
|
229
|
+
return unquote(text);
|
|
230
|
+
}
|
|
231
|
+
const n = Number(text);
|
|
232
|
+
if (text !== "" && Number.isFinite(n))
|
|
233
|
+
return n;
|
|
234
|
+
throw new PathError(`cannot read ${JSON.stringify(text)} as a literal at position ${at} of ${JSON.stringify(whole)}`);
|
|
235
|
+
}
|
|
236
|
+
/** Run a parsed path over a value, returning matches in document order. */
|
|
237
|
+
export function queryPath(root, steps, limit) {
|
|
238
|
+
let nodes = [{ path: [], value: root }];
|
|
239
|
+
for (const step of steps) {
|
|
240
|
+
const next = [];
|
|
241
|
+
for (const node of nodes)
|
|
242
|
+
applyStep(step, node, next);
|
|
243
|
+
nodes = next;
|
|
244
|
+
// Bound intermediate sets too: `..*` over a deep tree can blow up.
|
|
245
|
+
const ceiling = Math.max(limit, 1) * 20;
|
|
246
|
+
if (nodes.length > ceiling)
|
|
247
|
+
nodes = nodes.slice(0, ceiling);
|
|
248
|
+
}
|
|
249
|
+
const truncated = nodes.length > limit;
|
|
250
|
+
return {
|
|
251
|
+
matches: nodes.slice(0, limit).map((n) => ({ path: renderPath(n.path), value: n.value })),
|
|
252
|
+
truncated,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function applyStep(step, node, out) {
|
|
256
|
+
const v = node.value;
|
|
257
|
+
switch (step.kind) {
|
|
258
|
+
case "child": {
|
|
259
|
+
if (isPlainObject(v) && Object.hasOwn(v, step.name)) {
|
|
260
|
+
out.push({ path: [...node.path, step.name], value: v[step.name] });
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
case "wildcard": {
|
|
265
|
+
if (Array.isArray(v)) {
|
|
266
|
+
v.forEach((el, i) => out.push({ path: [...node.path, i], value: el }));
|
|
267
|
+
}
|
|
268
|
+
else if (isPlainObject(v)) {
|
|
269
|
+
for (const k of Object.keys(v))
|
|
270
|
+
out.push({ path: [...node.path, k], value: v[k] });
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
case "descend": {
|
|
275
|
+
const name = step.name;
|
|
276
|
+
walk(node, (n) => {
|
|
277
|
+
if (name === null) {
|
|
278
|
+
if (n.path.length !== node.path.length)
|
|
279
|
+
out.push(n);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (isPlainObject(n.value) && Object.hasOwn(n.value, name)) {
|
|
283
|
+
out.push({ path: [...n.path, name], value: n.value[name] });
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
case "index": {
|
|
289
|
+
if (!Array.isArray(v))
|
|
290
|
+
return;
|
|
291
|
+
const i = step.index < 0 ? v.length + step.index : step.index;
|
|
292
|
+
if (i >= 0 && i < v.length)
|
|
293
|
+
out.push({ path: [...node.path, i], value: v[i] });
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
case "slice": {
|
|
297
|
+
if (!Array.isArray(v))
|
|
298
|
+
return;
|
|
299
|
+
for (const i of sliceIndices(v.length, step.start, step.end, step.step)) {
|
|
300
|
+
out.push({ path: [...node.path, i], value: v[i] });
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
case "filter": {
|
|
305
|
+
if (Array.isArray(v)) {
|
|
306
|
+
v.forEach((el, i) => {
|
|
307
|
+
if (matchesFilter(el, step))
|
|
308
|
+
out.push({ path: [...node.path, i], value: el });
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
else if (isPlainObject(v)) {
|
|
312
|
+
for (const k of Object.keys(v)) {
|
|
313
|
+
if (matchesFilter(v[k], step))
|
|
314
|
+
out.push({ path: [...node.path, k], value: v[k] });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
default:
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/** Depth-first pre-order walk, used by recursive descent. */
|
|
324
|
+
function walk(node, visit) {
|
|
325
|
+
visit(node);
|
|
326
|
+
const v = node.value;
|
|
327
|
+
if (Array.isArray(v)) {
|
|
328
|
+
v.forEach((el, i) => walk({ path: [...node.path, i], value: el }, visit));
|
|
329
|
+
}
|
|
330
|
+
else if (isPlainObject(v)) {
|
|
331
|
+
for (const k of Object.keys(v))
|
|
332
|
+
walk({ path: [...node.path, k], value: v[k] }, visit);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/** Python slice semantics, including negative bounds and a negative step. */
|
|
336
|
+
export function sliceIndices(length, start, end, step) {
|
|
337
|
+
const out = [];
|
|
338
|
+
const clamp = (n, lo, hi) => Math.min(Math.max(n, lo), hi);
|
|
339
|
+
if (step > 0) {
|
|
340
|
+
const s = clamp(start === null ? 0 : start < 0 ? length + start : start, 0, length);
|
|
341
|
+
const e = clamp(end === null ? length : end < 0 ? length + end : end, 0, length);
|
|
342
|
+
for (let i = s; i < e; i += step)
|
|
343
|
+
out.push(i);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
const s = clamp(start === null ? length - 1 : start < 0 ? length + start : start, -1, length - 1);
|
|
347
|
+
const e = clamp(end === null ? -1 : end < 0 ? length + end : end, -1, length - 1);
|
|
348
|
+
for (let i = s; i > e; i += step)
|
|
349
|
+
out.push(i);
|
|
350
|
+
}
|
|
351
|
+
return out;
|
|
352
|
+
}
|
|
353
|
+
function matchesFilter(value, step) {
|
|
354
|
+
let cur = value;
|
|
355
|
+
for (const seg of step.field) {
|
|
356
|
+
if (isPlainObject(cur))
|
|
357
|
+
cur = cur[seg];
|
|
358
|
+
else if (Array.isArray(cur) && /^-?\d+$/.test(seg)) {
|
|
359
|
+
const i = Number(seg);
|
|
360
|
+
cur = cur[i < 0 ? cur.length + i : i];
|
|
361
|
+
}
|
|
362
|
+
else
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
if (step.op === null)
|
|
366
|
+
return cur !== undefined && cur !== null && cur !== false;
|
|
367
|
+
const lit = step.literal;
|
|
368
|
+
if (step.op === "==")
|
|
369
|
+
return looseEqual(cur, lit);
|
|
370
|
+
if (step.op === "!=")
|
|
371
|
+
return !looseEqual(cur, lit);
|
|
372
|
+
if (step.op === "=~") {
|
|
373
|
+
if (typeof lit !== "string")
|
|
374
|
+
return false;
|
|
375
|
+
try {
|
|
376
|
+
return new RegExp(lit).test(String(cur));
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (typeof cur === "number" && typeof lit === "number") {
|
|
383
|
+
return compare(cur < lit, cur > lit, step.op);
|
|
384
|
+
}
|
|
385
|
+
if (typeof cur === "string" && typeof lit === "string") {
|
|
386
|
+
return compare(cur < lit, cur > lit, step.op);
|
|
387
|
+
}
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
function compare(lt, gt, op) {
|
|
391
|
+
if (op === "<")
|
|
392
|
+
return lt;
|
|
393
|
+
if (op === "<=")
|
|
394
|
+
return lt || !gt;
|
|
395
|
+
if (op === ">")
|
|
396
|
+
return gt;
|
|
397
|
+
if (op === ">=")
|
|
398
|
+
return gt || !lt;
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
/** `==` compares scalars by value; objects and arrays never equal a literal. */
|
|
402
|
+
function looseEqual(a, b) {
|
|
403
|
+
if (a === null || b === null)
|
|
404
|
+
return a === b;
|
|
405
|
+
if (typeof a === "object" || typeof b === "object")
|
|
406
|
+
return false;
|
|
407
|
+
return a === b;
|
|
408
|
+
}
|
|
409
|
+
const SAFE_KEY = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
410
|
+
/**
|
|
411
|
+
* Render a path as `$.a[0]["odd key"]` — readable, and re-parseable by
|
|
412
|
+
* `parsePath` into a path that reaches the same node in the same document.
|
|
413
|
+
*
|
|
414
|
+
* A numeric *segment* is only written as `[0]` when it really is an array
|
|
415
|
+
* index. An object key that happens to look numeric is written `["0"]`,
|
|
416
|
+
* because `[0]` only ever matches an array and the round trip would
|
|
417
|
+
* otherwise silently return nothing.
|
|
418
|
+
*/
|
|
419
|
+
export function renderPath(segments) {
|
|
420
|
+
let out = "$";
|
|
421
|
+
for (const seg of segments) {
|
|
422
|
+
if (typeof seg === "number")
|
|
423
|
+
out += `[${seg}]`;
|
|
424
|
+
else if (SAFE_KEY.test(seg))
|
|
425
|
+
out += `.${seg}`;
|
|
426
|
+
else
|
|
427
|
+
out += `[${JSON.stringify(seg)}]`;
|
|
428
|
+
}
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Pointer (RFC 6901), JSON Patch (RFC 6902) and JSON Merge Patch
|
|
3
|
+
* (RFC 7386).
|
|
4
|
+
*
|
|
5
|
+
* All three are implemented in full. Patching is transactional: the document
|
|
6
|
+
* is cloned, every operation is applied in order, and a failure anywhere
|
|
7
|
+
* leaves the caller's input untouched and reports which operation index
|
|
8
|
+
* failed — which is the behaviour RFC 6902 §5 requires.
|
|
9
|
+
*/
|
|
10
|
+
export declare class PatchError extends Error {
|
|
11
|
+
readonly opIndex: number;
|
|
12
|
+
constructor(message: string, opIndex: number);
|
|
13
|
+
}
|
|
14
|
+
/** Decode one JSON Pointer reference token: `~1` is `/`, `~0` is `~`. */
|
|
15
|
+
export declare function unescapeToken(token: string): string;
|
|
16
|
+
/** Encode one reference token for embedding in a pointer. */
|
|
17
|
+
export declare function escapeToken(token: string): string;
|
|
18
|
+
/** Split a JSON Pointer into its decoded tokens. `""` is the whole document. */
|
|
19
|
+
export declare function parsePointer(pointer: string): string[];
|
|
20
|
+
/** Build a pointer from already-decoded tokens. */
|
|
21
|
+
export declare function formatPointer(tokens: ReadonlyArray<string>): string;
|
|
22
|
+
/** Resolve a pointer, returning `{ found: false }` rather than throwing when it misses. */
|
|
23
|
+
export declare function resolvePointer(doc: unknown, pointer: string): {
|
|
24
|
+
found: true;
|
|
25
|
+
value: unknown;
|
|
26
|
+
} | {
|
|
27
|
+
found: false;
|
|
28
|
+
reason: string;
|
|
29
|
+
};
|
|
30
|
+
export type PatchOp = {
|
|
31
|
+
op: "add";
|
|
32
|
+
path: string;
|
|
33
|
+
value: unknown;
|
|
34
|
+
} | {
|
|
35
|
+
op: "remove";
|
|
36
|
+
path: string;
|
|
37
|
+
} | {
|
|
38
|
+
op: "replace";
|
|
39
|
+
path: string;
|
|
40
|
+
value: unknown;
|
|
41
|
+
} | {
|
|
42
|
+
op: "move";
|
|
43
|
+
from: string;
|
|
44
|
+
path: string;
|
|
45
|
+
} | {
|
|
46
|
+
op: "copy";
|
|
47
|
+
from: string;
|
|
48
|
+
path: string;
|
|
49
|
+
} | {
|
|
50
|
+
op: "test";
|
|
51
|
+
path: string;
|
|
52
|
+
value: unknown;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Apply an RFC 6902 patch. Returns the new document; the input is never
|
|
56
|
+
* mutated. Throws `PatchError` (carrying the failing operation's index) if
|
|
57
|
+
* any operation cannot be applied, leaving nothing half-done.
|
|
58
|
+
*/
|
|
59
|
+
export declare function applyJsonPatch(doc: unknown, ops: ReadonlyArray<PatchOp>): unknown;
|
|
60
|
+
/**
|
|
61
|
+
* Apply an RFC 7386 merge patch: an object merges key by key, `null` deletes
|
|
62
|
+
* a key, and any non-object patch replaces the target outright. Arrays are
|
|
63
|
+
* replaced whole — that is the RFC's behaviour, not a shortcut here.
|
|
64
|
+
*/
|
|
65
|
+
export declare function applyMergePatch(target: unknown, patch: unknown): unknown;
|
|
66
|
+
/**
|
|
67
|
+
* Derive the RFC 7386 merge patch that turns `from` into `to`. Returns the
|
|
68
|
+
* smallest object that does so; `null` entries mark deletions. Where either
|
|
69
|
+
* side is not an object the result is `to` itself, since a merge patch
|
|
70
|
+
* cannot express a partial array or scalar change.
|
|
71
|
+
*/
|
|
72
|
+
export declare function diffMergePatch(from: unknown, to: unknown): unknown;
|