1 | (function (global, factory) {
|
2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
3 | typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
4 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.rfc6902 = {}));
|
5 | })(this, (function (exports) { 'use strict';
|
6 |
|
7 | /**
|
8 | Unescape token part of a JSON Pointer string
|
9 |
|
10 | `token` should *not* contain any '/' characters.
|
11 |
|
12 | > Evaluation of each reference token begins by decoding any escaped
|
13 | > character sequence. This is performed by first transforming any
|
14 | > occurrence of the sequence '~1' to '/', and then transforming any
|
15 | > occurrence of the sequence '~0' to '~'. By performing the
|
16 | > substitutions in this order, an implementation avoids the error of
|
17 | > turning '~01' first into '~1' and then into '/', which would be
|
18 | > incorrect (the string '~01' correctly becomes '~1' after
|
19 | > transformation).
|
20 |
|
21 | Here's my take:
|
22 |
|
23 | ~1 is unescaped with higher priority than ~0 because it is a lower-order escape character.
|
24 | I say "lower order" because '/' needs escaping due to the JSON Pointer serialization technique.
|
25 | Whereas, '~' is escaped because escaping '/' uses the '~' character.
|
26 | */
|
27 | function unescape(token) {
|
28 | return token.replace(/~1/g, '/').replace(/~0/g, '~');
|
29 | }
|
30 | /** Escape token part of a JSON Pointer string
|
31 |
|
32 | > '~' needs to be encoded as '~0' and '/'
|
33 | > needs to be encoded as '~1' when these characters appear in a
|
34 | > reference token.
|
35 |
|
36 | This is the exact inverse of `unescape()`, so the reverse replacements must take place in reverse order.
|
37 | */
|
38 | function escape(token) {
|
39 | return token.replace(/~/g, '~0').replace(/\//g, '~1');
|
40 | }
|
41 | /**
|
42 | JSON Pointer representation
|
43 | */
|
44 | class Pointer {
|
45 | constructor(tokens = ['']) {
|
46 | this.tokens = tokens;
|
47 | }
|
48 | /**
|
49 | `path` *must* be a properly escaped string.
|
50 | */
|
51 | static fromJSON(path) {
|
52 | const tokens = path.split('/').map(unescape);
|
53 | if (tokens[0] !== '')
|
54 | throw new Error(`Invalid JSON Pointer: ${path}`);
|
55 | return new Pointer(tokens);
|
56 | }
|
57 | toString() {
|
58 | return this.tokens.map(escape).join('/');
|
59 | }
|
60 | /**
|
61 | Returns an object with 'parent', 'key', and 'value' properties.
|
62 | In the special case that this Pointer's path == "",
|
63 | this object will be {parent: null, key: '', value: object}.
|
64 | Otherwise, parent and key will have the property such that parent[key] == value.
|
65 | */
|
66 | evaluate(object) {
|
67 | let parent = null;
|
68 | let key = '';
|
69 | let value = object;
|
70 | for (let i = 1, l = this.tokens.length; i < l; i++) {
|
71 | parent = value;
|
72 | key = this.tokens[i];
|
73 | if (key == '__proto__' || key == 'constructor' || key == 'prototype') {
|
74 | continue;
|
75 | }
|
76 | // not sure if this the best way to handle non-existant paths...
|
77 | value = (parent || {})[key];
|
78 | }
|
79 | return { parent, key, value };
|
80 | }
|
81 | get(object) {
|
82 | return this.evaluate(object).value;
|
83 | }
|
84 | set(object, value) {
|
85 | const endpoint = this.evaluate(object);
|
86 | if (endpoint.parent) {
|
87 | endpoint.parent[endpoint.key] = value;
|
88 | }
|
89 | }
|
90 | push(token) {
|
91 | // mutable
|
92 | this.tokens.push(token);
|
93 | }
|
94 | /**
|
95 | `token` should be a String. It'll be coerced to one anyway.
|
96 |
|
97 | immutable (shallowly)
|
98 | */
|
99 | add(token) {
|
100 | const tokens = this.tokens.concat(String(token));
|
101 | return new Pointer(tokens);
|
102 | }
|
103 | }
|
104 |
|
105 | const hasOwnProperty = Object.prototype.hasOwnProperty;
|
106 | function objectType(object) {
|
107 | if (object === undefined) {
|
108 | return 'undefined';
|
109 | }
|
110 | if (object === null) {
|
111 | return 'null';
|
112 | }
|
113 | if (Array.isArray(object)) {
|
114 | return 'array';
|
115 | }
|
116 | return typeof object;
|
117 | }
|
118 | function isNonPrimitive(value) {
|
119 | // loose-equality checking for null is faster than strict checking for each of null/undefined/true/false
|
120 | // checking null first, then calling typeof, is faster than vice-versa
|
121 | return value != null && typeof value == 'object';
|
122 | }
|
123 | /**
|
124 | Recursively copy a value.
|
125 |
|
126 | @param source - should be a JavaScript primitive, Array, Date, or (plain old) Object.
|
127 | @returns copy of source where every Array and Object have been recursively
|
128 | reconstructed from their constituent elements
|
129 | */
|
130 | function clone(source) {
|
131 | if (!isNonPrimitive(source)) {
|
132 | // short-circuiting is faster than a single return
|
133 | return source;
|
134 | }
|
135 | // x.constructor == Array is the fastest way to check if x is an Array
|
136 | if (source.constructor == Array) {
|
137 | // construction via imperative for-loop is faster than source.map(arrayVsObject)
|
138 | const length = source.length;
|
139 | // setting the Array length during construction is faster than just `[]` or `new Array()`
|
140 | const arrayTarget = new Array(length);
|
141 | for (let i = 0; i < length; i++) {
|
142 | arrayTarget[i] = clone(source[i]);
|
143 | }
|
144 | return arrayTarget;
|
145 | }
|
146 | // Date
|
147 | if (source.constructor == Date) {
|
148 | const dateTarget = new Date(+source);
|
149 | return dateTarget;
|
150 | }
|
151 | // Object
|
152 | const objectTarget = {};
|
153 | // declaring the variable (with const) inside the loop is faster
|
154 | for (const key in source) {
|
155 | // hasOwnProperty costs a bit of performance, but it's semantically necessary
|
156 | // using a global helper is MUCH faster than calling source.hasOwnProperty(key)
|
157 | if (hasOwnProperty.call(source, key)) {
|
158 | objectTarget[key] = clone(source[key]);
|
159 | }
|
160 | }
|
161 | return objectTarget;
|
162 | }
|
163 |
|
164 | function isDestructive({ op }) {
|
165 | return op === 'remove' || op === 'replace' || op === 'copy' || op === 'move';
|
166 | }
|
167 | /**
|
168 | List the keys in `minuend` that are not in `subtrahend`.
|
169 |
|
170 | A key is only considered if it is both 1) an own-property (o.hasOwnProperty(k))
|
171 | of the object, and 2) has a value that is not undefined. This is to match JSON
|
172 | semantics, where JSON object serialization drops keys with undefined values.
|
173 |
|
174 | @param minuend Object of interest
|
175 | @param subtrahend Object of comparison
|
176 | @returns Array of keys that are in `minuend` but not in `subtrahend`.
|
177 | */
|
178 | function subtract(minuend, subtrahend) {
|
179 | // initialize empty object; we only care about the keys, the values can be anything
|
180 | const obj = {};
|
181 | // build up obj with all the properties of minuend
|
182 | for (const add_key in minuend) {
|
183 | if (hasOwnProperty.call(minuend, add_key) && minuend[add_key] !== undefined) {
|
184 | obj[add_key] = 1;
|
185 | }
|
186 | }
|
187 | // now delete all the properties of subtrahend from obj
|
188 | // (deleting a missing key has no effect)
|
189 | for (const del_key in subtrahend) {
|
190 | if (hasOwnProperty.call(subtrahend, del_key) && subtrahend[del_key] !== undefined) {
|
191 | delete obj[del_key];
|
192 | }
|
193 | }
|
194 | // finally, extract whatever keys remain in obj
|
195 | return Object.keys(obj);
|
196 | }
|
197 | /**
|
198 | List the keys that shared by all `objects`.
|
199 |
|
200 | The semantics of what constitutes a "key" is described in {@link subtract}.
|
201 |
|
202 | @param objects Array of objects to compare
|
203 | @returns Array of keys that are in ("own-properties" of) every object in `objects`.
|
204 | */
|
205 | function intersection(objects) {
|
206 | const length = objects.length;
|
207 | // prepare empty counter to keep track of how many objects each key occurred in
|
208 | const counter = {};
|
209 | // go through each object and increment the counter for each key in that object
|
210 | for (let i = 0; i < length; i++) {
|
211 | const object = objects[i];
|
212 | for (const key in object) {
|
213 | if (hasOwnProperty.call(object, key) && object[key] !== undefined) {
|
214 | counter[key] = (counter[key] || 0) + 1;
|
215 | }
|
216 | }
|
217 | }
|
218 | // now delete all keys from the counter that were not seen in every object
|
219 | for (const key in counter) {
|
220 | if (counter[key] < length) {
|
221 | delete counter[key];
|
222 | }
|
223 | }
|
224 | // finally, extract whatever keys remain in the counter
|
225 | return Object.keys(counter);
|
226 | }
|
227 | function isArrayAdd(array_operation) {
|
228 | return array_operation.op === 'add';
|
229 | }
|
230 | function isArrayRemove(array_operation) {
|
231 | return array_operation.op === 'remove';
|
232 | }
|
233 | function appendArrayOperation(base, operation) {
|
234 | return {
|
235 | // the new operation must be pushed on the end
|
236 | operations: base.operations.concat(operation),
|
237 | cost: base.cost + 1,
|
238 | };
|
239 | }
|
240 | /**
|
241 | Calculate the shortest sequence of operations to get from `input` to `output`,
|
242 | using a dynamic programming implementation of the Levenshtein distance algorithm.
|
243 |
|
244 | To get from the input ABC to the output AZ we could just delete all the input
|
245 | and say "insert A, insert Z" and be done with it. That's what we do if the
|
246 | input is empty. But we can be smarter.
|
247 |
|
248 | output
|
249 | A Z
|
250 | - -
|
251 | [0] 1 2
|
252 | input A | 1 [0] 1
|
253 | B | 2 [1] 1
|
254 | C | 3 2 [2]
|
255 |
|
256 | 1) start at 0,0 (+0)
|
257 | 2) keep A (+0)
|
258 | 3) remove B (+1)
|
259 | 4) replace C with Z (+1)
|
260 |
|
261 | If the `input` (source) is empty, they'll all be in the top row, resulting in an
|
262 | array of 'add' operations.
|
263 | If the `output` (target) is empty, everything will be in the left column,
|
264 | resulting in an array of 'remove' operations.
|
265 |
|
266 | @returns A list of add/remove/replace operations.
|
267 | */
|
268 | function diffArrays(input, output, ptr, diff = diffAny) {
|
269 | // set up cost matrix (very simple initialization: just a map)
|
270 | const memo = {
|
271 | '0,0': { operations: [], cost: 0 },
|
272 | };
|
273 | /**
|
274 | Calculate the cheapest sequence of operations required to get from
|
275 | input.slice(0, i) to output.slice(0, j).
|
276 | There may be other valid sequences with the same cost, but none cheaper.
|
277 |
|
278 | @param i The row in the layout above
|
279 | @param j The column in the layout above
|
280 | @returns An object containing a list of operations, along with the total cost
|
281 | of applying them (+1 for each add/remove/replace operation)
|
282 | */
|
283 | function dist(i, j) {
|
284 | // memoized
|
285 | const memo_key = `${i},${j}`;
|
286 | let memoized = memo[memo_key];
|
287 | if (memoized === undefined) {
|
288 | // TODO: this !diff(...).length usage could/should be lazy
|
289 | if (i > 0 && j > 0 && !diff(input[i - 1], output[j - 1], ptr.add(String(i - 1))).length) {
|
290 | // equal (no operations => no cost)
|
291 | memoized = dist(i - 1, j - 1);
|
292 | }
|
293 | else {
|
294 | const alternatives = [];
|
295 | if (i > 0) {
|
296 | // NOT topmost row
|
297 | const remove_base = dist(i - 1, j);
|
298 | const remove_operation = {
|
299 | op: 'remove',
|
300 | index: i - 1,
|
301 | };
|
302 | alternatives.push(appendArrayOperation(remove_base, remove_operation));
|
303 | }
|
304 | if (j > 0) {
|
305 | // NOT leftmost column
|
306 | const add_base = dist(i, j - 1);
|
307 | const add_operation = {
|
308 | op: 'add',
|
309 | index: i - 1,
|
310 | value: output[j - 1],
|
311 | };
|
312 | alternatives.push(appendArrayOperation(add_base, add_operation));
|
313 | }
|
314 | if (i > 0 && j > 0) {
|
315 | // TABLE MIDDLE
|
316 | // supposing we replaced it, compute the rest of the costs:
|
317 | const replace_base = dist(i - 1, j - 1);
|
318 | // okay, the general plan is to replace it, but we can be smarter,
|
319 | // recursing into the structure and replacing only part of it if
|
320 | // possible, but to do so we'll need the original value
|
321 | const replace_operation = {
|
322 | op: 'replace',
|
323 | index: i - 1,
|
324 | original: input[i - 1],
|
325 | value: output[j - 1],
|
326 | };
|
327 | alternatives.push(appendArrayOperation(replace_base, replace_operation));
|
328 | }
|
329 | // the only other case, i === 0 && j === 0, has already been memoized
|
330 | // the meat of the algorithm:
|
331 | // sort by cost to find the lowest one (might be several ties for lowest)
|
332 | // [4, 6, 7, 1, 2].sort((a, b) => a - b) -> [ 1, 2, 4, 6, 7 ]
|
333 | const best = alternatives.sort((a, b) => a.cost - b.cost)[0];
|
334 | memoized = best;
|
335 | }
|
336 | memo[memo_key] = memoized;
|
337 | }
|
338 | return memoized;
|
339 | }
|
340 | // handle weird objects masquerading as Arrays that don't have proper length
|
341 | // properties by using 0 for everything but positive numbers
|
342 | const input_length = (isNaN(input.length) || input.length <= 0) ? 0 : input.length;
|
343 | const output_length = (isNaN(output.length) || output.length <= 0) ? 0 : output.length;
|
344 | const array_operations = dist(input_length, output_length).operations;
|
345 | const [padded_operations] = array_operations.reduce(([operations, padding], array_operation) => {
|
346 | if (isArrayAdd(array_operation)) {
|
347 | const padded_index = array_operation.index + 1 + padding;
|
348 | const index_token = padded_index < (input_length + padding) ? String(padded_index) : '-';
|
349 | const operation = {
|
350 | op: array_operation.op,
|
351 | path: ptr.add(index_token).toString(),
|
352 | value: array_operation.value,
|
353 | };
|
354 | // padding++ // maybe only if array_operation.index > -1 ?
|
355 | return [operations.concat(operation), padding + 1];
|
356 | }
|
357 | else if (isArrayRemove(array_operation)) {
|
358 | const operation = {
|
359 | op: array_operation.op,
|
360 | path: ptr.add(String(array_operation.index + padding)).toString(),
|
361 | };
|
362 | // padding--
|
363 | return [operations.concat(operation), padding - 1];
|
364 | }
|
365 | else { // replace
|
366 | const replace_ptr = ptr.add(String(array_operation.index + padding));
|
367 | const replace_operations = diff(array_operation.original, array_operation.value, replace_ptr);
|
368 | return [operations.concat(...replace_operations), padding];
|
369 | }
|
370 | }, [[], 0]);
|
371 | return padded_operations;
|
372 | }
|
373 | function diffObjects(input, output, ptr, diff = diffAny) {
|
374 | // if a key is in input but not output -> remove it
|
375 | const operations = [];
|
376 | subtract(input, output).forEach(key => {
|
377 | operations.push({ op: 'remove', path: ptr.add(key).toString() });
|
378 | });
|
379 | // if a key is in output but not input -> add it
|
380 | subtract(output, input).forEach(key => {
|
381 | operations.push({ op: 'add', path: ptr.add(key).toString(), value: output[key] });
|
382 | });
|
383 | // if a key is in both, diff it recursively
|
384 | intersection([input, output]).forEach(key => {
|
385 | operations.push(...diff(input[key], output[key], ptr.add(key)));
|
386 | });
|
387 | return operations;
|
388 | }
|
389 | /**
|
390 | `diffAny()` returns an empty array if `input` and `output` are materially equal
|
391 | (i.e., would produce equivalent JSON); otherwise it produces an array of patches
|
392 | that would transform `input` into `output`.
|
393 |
|
394 | > Here, "equal" means that the value at the target location and the
|
395 | > value conveyed by "value" are of the same JSON type, and that they
|
396 | > are considered equal by the following rules for that type:
|
397 | > o strings: are considered equal if they contain the same number of
|
398 | > Unicode characters and their code points are byte-by-byte equal.
|
399 | > o numbers: are considered equal if their values are numerically
|
400 | > equal.
|
401 | > o arrays: are considered equal if they contain the same number of
|
402 | > values, and if each value can be considered equal to the value at
|
403 | > the corresponding position in the other array, using this list of
|
404 | > type-specific rules.
|
405 | > o objects: are considered equal if they contain the same number of
|
406 | > members, and if each member can be considered equal to a member in
|
407 | > the other object, by comparing their keys (as strings) and their
|
408 | > values (using this list of type-specific rules).
|
409 | > o literals (false, true, and null): are considered equal if they are
|
410 | > the same.
|
411 | */
|
412 | function diffAny(input, output, ptr, diff = diffAny) {
|
413 | // strict equality handles literals, numbers, and strings (a sufficient but not necessary cause)
|
414 | if (input === output) {
|
415 | return [];
|
416 | }
|
417 | const input_type = objectType(input);
|
418 | const output_type = objectType(output);
|
419 | if (input_type == 'array' && output_type == 'array') {
|
420 | return diffArrays(input, output, ptr, diff);
|
421 | }
|
422 | if (input_type == 'object' && output_type == 'object') {
|
423 | return diffObjects(input, output, ptr, diff);
|
424 | }
|
425 | // at this point we know that input and output are materially different;
|
426 | // could be array -> object, object -> array, boolean -> undefined,
|
427 | // number -> string, or some other combination, but nothing that can be split
|
428 | // up into multiple patches: so `output` must replace `input` wholesale.
|
429 | return [{ op: 'replace', path: ptr.toString(), value: output }];
|
430 | }
|
431 |
|
432 | class MissingError extends Error {
|
433 | constructor(path) {
|
434 | super(`Value required at path: ${path}`);
|
435 | this.path = path;
|
436 | this.name = 'MissingError';
|
437 | }
|
438 | }
|
439 | class TestError extends Error {
|
440 | constructor(actual, expected) {
|
441 | super(`Test failed: ${actual} != ${expected}`);
|
442 | this.actual = actual;
|
443 | this.expected = expected;
|
444 | this.name = 'TestError';
|
445 | }
|
446 | }
|
447 | function _add(object, key, value) {
|
448 | if (Array.isArray(object)) {
|
449 | // `key` must be an index
|
450 | if (key == '-') {
|
451 | object.push(value);
|
452 | }
|
453 | else {
|
454 | const index = parseInt(key, 10);
|
455 | object.splice(index, 0, value);
|
456 | }
|
457 | }
|
458 | else {
|
459 | object[key] = value;
|
460 | }
|
461 | }
|
462 | function _remove(object, key) {
|
463 | if (Array.isArray(object)) {
|
464 | // '-' syntax doesn't make sense when removing
|
465 | const index = parseInt(key, 10);
|
466 | object.splice(index, 1);
|
467 | }
|
468 | else {
|
469 | // not sure what the proper behavior is when path = ''
|
470 | delete object[key];
|
471 | }
|
472 | }
|
473 | /**
|
474 | > o If the target location specifies an array index, a new value is
|
475 | > inserted into the array at the specified index.
|
476 | > o If the target location specifies an object member that does not
|
477 | > already exist, a new member is added to the object.
|
478 | > o If the target location specifies an object member that does exist,
|
479 | > that member's value is replaced.
|
480 | */
|
481 | function add(object, operation) {
|
482 | const endpoint = Pointer.fromJSON(operation.path).evaluate(object);
|
483 | // it's not exactly a "MissingError" in the same way that `remove` is -- more like a MissingParent, or something
|
484 | if (endpoint.parent === undefined) {
|
485 | return new MissingError(operation.path);
|
486 | }
|
487 | _add(endpoint.parent, endpoint.key, clone(operation.value));
|
488 | return null;
|
489 | }
|
490 | /**
|
491 | > The "remove" operation removes the value at the target location.
|
492 | > The target location MUST exist for the operation to be successful.
|
493 | */
|
494 | function remove(object, operation) {
|
495 | // endpoint has parent, key, and value properties
|
496 | const endpoint = Pointer.fromJSON(operation.path).evaluate(object);
|
497 | if (endpoint.value === undefined) {
|
498 | return new MissingError(operation.path);
|
499 | }
|
500 | // not sure what the proper behavior is when path = ''
|
501 | _remove(endpoint.parent, endpoint.key);
|
502 | return null;
|
503 | }
|
504 | /**
|
505 | > The "replace" operation replaces the value at the target location
|
506 | > with a new value. The operation object MUST contain a "value" member
|
507 | > whose content specifies the replacement value.
|
508 | > The target location MUST exist for the operation to be successful.
|
509 |
|
510 | > This operation is functionally identical to a "remove" operation for
|
511 | > a value, followed immediately by an "add" operation at the same
|
512 | > location with the replacement value.
|
513 |
|
514 | Even more simply, it's like the add operation with an existence check.
|
515 | */
|
516 | function replace(object, operation) {
|
517 | const endpoint = Pointer.fromJSON(operation.path).evaluate(object);
|
518 | if (endpoint.parent === null) {
|
519 | return new MissingError(operation.path);
|
520 | }
|
521 | // this existence check treats arrays as a special case
|
522 | if (Array.isArray(endpoint.parent)) {
|
523 | if (parseInt(endpoint.key, 10) >= endpoint.parent.length) {
|
524 | return new MissingError(operation.path);
|
525 | }
|
526 | }
|
527 | else if (endpoint.value === undefined) {
|
528 | return new MissingError(operation.path);
|
529 | }
|
530 | endpoint.parent[endpoint.key] = operation.value;
|
531 | return null;
|
532 | }
|
533 | /**
|
534 | > The "move" operation removes the value at a specified location and
|
535 | > adds it to the target location.
|
536 | > The operation object MUST contain a "from" member, which is a string
|
537 | > containing a JSON Pointer value that references the location in the
|
538 | > target document to move the value from.
|
539 | > This operation is functionally identical to a "remove" operation on
|
540 | > the "from" location, followed immediately by an "add" operation at
|
541 | > the target location with the value that was just removed.
|
542 |
|
543 | > The "from" location MUST NOT be a proper prefix of the "path"
|
544 | > location; i.e., a location cannot be moved into one of its children.
|
545 |
|
546 | TODO: throw if the check described in the previous paragraph fails.
|
547 | */
|
548 | function move(object, operation) {
|
549 | const from_endpoint = Pointer.fromJSON(operation.from).evaluate(object);
|
550 | if (from_endpoint.value === undefined) {
|
551 | return new MissingError(operation.from);
|
552 | }
|
553 | const endpoint = Pointer.fromJSON(operation.path).evaluate(object);
|
554 | if (endpoint.parent === undefined) {
|
555 | return new MissingError(operation.path);
|
556 | }
|
557 | _remove(from_endpoint.parent, from_endpoint.key);
|
558 | _add(endpoint.parent, endpoint.key, from_endpoint.value);
|
559 | return null;
|
560 | }
|
561 | /**
|
562 | > The "copy" operation copies the value at a specified location to the
|
563 | > target location.
|
564 | > The operation object MUST contain a "from" member, which is a string
|
565 | > containing a JSON Pointer value that references the location in the
|
566 | > target document to copy the value from.
|
567 | > The "from" location MUST exist for the operation to be successful.
|
568 |
|
569 | > This operation is functionally identical to an "add" operation at the
|
570 | > target location using the value specified in the "from" member.
|
571 |
|
572 | Alternatively, it's like 'move' without the 'remove'.
|
573 | */
|
574 | function copy(object, operation) {
|
575 | const from_endpoint = Pointer.fromJSON(operation.from).evaluate(object);
|
576 | if (from_endpoint.value === undefined) {
|
577 | return new MissingError(operation.from);
|
578 | }
|
579 | const endpoint = Pointer.fromJSON(operation.path).evaluate(object);
|
580 | if (endpoint.parent === undefined) {
|
581 | return new MissingError(operation.path);
|
582 | }
|
583 | _add(endpoint.parent, endpoint.key, clone(from_endpoint.value));
|
584 | return null;
|
585 | }
|
586 | /**
|
587 | > The "test" operation tests that a value at the target location is
|
588 | > equal to a specified value.
|
589 | > The operation object MUST contain a "value" member that conveys the
|
590 | > value to be compared to the target location's value.
|
591 | > The target location MUST be equal to the "value" value for the
|
592 | > operation to be considered successful.
|
593 | */
|
594 | function test(object, operation) {
|
595 | const endpoint = Pointer.fromJSON(operation.path).evaluate(object);
|
596 | // TODO: this diffAny(...).length usage could/should be lazy
|
597 | if (diffAny(endpoint.value, operation.value, new Pointer()).length) {
|
598 | return new TestError(endpoint.value, operation.value);
|
599 | }
|
600 | return null;
|
601 | }
|
602 | class InvalidOperationError extends Error {
|
603 | constructor(operation) {
|
604 | super(`Invalid operation: ${operation.op}`);
|
605 | this.operation = operation;
|
606 | this.name = 'InvalidOperationError';
|
607 | }
|
608 | }
|
609 | /**
|
610 | Switch on `operation.op`, applying the corresponding patch function for each
|
611 | case to `object`.
|
612 | */
|
613 | function apply(object, operation) {
|
614 | // not sure why TypeScript can't infer typesafety of:
|
615 | // {add, remove, replace, move, copy, test}[operation.op](object, operation)
|
616 | // (seems like a bug)
|
617 | switch (operation.op) {
|
618 | case 'add': return add(object, operation);
|
619 | case 'remove': return remove(object, operation);
|
620 | case 'replace': return replace(object, operation);
|
621 | case 'move': return move(object, operation);
|
622 | case 'copy': return copy(object, operation);
|
623 | case 'test': return test(object, operation);
|
624 | }
|
625 | return new InvalidOperationError(operation);
|
626 | }
|
627 |
|
628 | /**
|
629 | Apply a 'application/json-patch+json'-type patch to an object.
|
630 |
|
631 | `patch` *must* be an array of operations.
|
632 |
|
633 | > Operation objects MUST have exactly one "op" member, whose value
|
634 | > indicates the operation to perform. Its value MUST be one of "add",
|
635 | > "remove", "replace", "move", "copy", or "test"; other values are
|
636 | > errors.
|
637 |
|
638 | This method mutates the target object in-place.
|
639 |
|
640 | @returns list of results, one for each operation: `null` indicated success,
|
641 | otherwise, the result will be an instance of one of the Error classes:
|
642 | MissingError, InvalidOperationError, or TestError.
|
643 | */
|
644 | function applyPatch(object, patch) {
|
645 | return patch.map(operation => apply(object, operation));
|
646 | }
|
647 | function wrapVoidableDiff(diff) {
|
648 | function wrappedDiff(input, output, ptr) {
|
649 | const custom_patch = diff(input, output, ptr);
|
650 | // ensure an array is always returned
|
651 | return Array.isArray(custom_patch) ? custom_patch : diffAny(input, output, ptr, wrappedDiff);
|
652 | }
|
653 | return wrappedDiff;
|
654 | }
|
655 | /**
|
656 | Produce a 'application/json-patch+json'-type patch to get from one object to
|
657 | another.
|
658 |
|
659 | This does not alter `input` or `output` unless they have a property getter with
|
660 | side-effects (which is not a good idea anyway).
|
661 |
|
662 | `diff` is called on each pair of comparable non-primitive nodes in the
|
663 | `input`/`output` object trees, producing nested patches. Return `undefined`
|
664 | to fall back to default behaviour.
|
665 |
|
666 | Returns list of operations to perform on `input` to produce `output`.
|
667 | */
|
668 | function createPatch(input, output, diff) {
|
669 | const ptr = new Pointer();
|
670 | // a new Pointer gets a default path of [''] if not specified
|
671 | return (diff ? wrapVoidableDiff(diff) : diffAny)(input, output, ptr);
|
672 | }
|
673 | /**
|
674 | Create a test operation based on `input`'s current evaluation of the JSON
|
675 | Pointer `path`; if such a pointer cannot be resolved, returns undefined.
|
676 | */
|
677 | function createTest(input, path) {
|
678 | const endpoint = Pointer.fromJSON(path).evaluate(input);
|
679 | if (endpoint !== undefined) {
|
680 | return { op: 'test', path, value: endpoint.value };
|
681 | }
|
682 | }
|
683 | /**
|
684 | Produce an 'application/json-patch+json'-type list of tests, to verify that
|
685 | existing values in an object are identical to the those captured at some
|
686 | checkpoint (whenever this function is called).
|
687 |
|
688 | This does not alter `input` or `output` unless they have a property getter with
|
689 | side-effects (which is not a good idea anyway).
|
690 |
|
691 | Returns list of test operations.
|
692 | */
|
693 | function createTests(input, patch) {
|
694 | const tests = new Array();
|
695 | patch.filter(isDestructive).forEach(operation => {
|
696 | const pathTest = createTest(input, operation.path);
|
697 | if (pathTest)
|
698 | tests.push(pathTest);
|
699 | if ('from' in operation) {
|
700 | const fromTest = createTest(input, operation.from);
|
701 | if (fromTest)
|
702 | tests.push(fromTest);
|
703 | }
|
704 | });
|
705 | return tests;
|
706 | }
|
707 |
|
708 | exports.Pointer = Pointer;
|
709 | exports.applyPatch = applyPatch;
|
710 | exports.createPatch = createPatch;
|
711 | exports.createTests = createTests;
|
712 |
|
713 | Object.defineProperty(exports, '__esModule', { value: true });
|
714 |
|
715 | }));
|