-
Notifications
You must be signed in to change notification settings - Fork 41
/
jiff.js
127 lines (101 loc) · 3.55 KB
/
jiff.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/** @license MIT License (c) copyright 2010-2014 original author or authors */
/** @author Brian Cavalier */
/** @author John Hann */
var lcs = require('./lib/lcs');
var patch = require('./lib/jsonPatch');
var inverse = require('./lib/inverse');
var jsonPointer = require('./lib/jsonPointer');
var encodeSegment = jsonPointer.encodeSegment;
exports.diff = diff;
exports.patch = patch.apply;
exports.patchInPlace = patch.applyInPlace;
exports.inverse = inverse;
exports.clone = patch.clone;
// Errors
exports.InvalidPatchOperationError = require('./lib/InvalidPatchOperationError');
exports.TestFailedError = require('./lib/TestFailedError');
exports.PatchNotInvertibleError = require('./lib/PatchNotInvertibleError');
/**
* Compute a JSON Patch representing the differences between a and b.
* @param {object|array|string|number} a
* @param {object|array|string|number} b
* @param {function} hasher hashing function that will be used to
* recognize identical objects
* @returns {array} JSON Patch such that patch(diff(a, b), a) ~ b
*/
function diff(a, b, hasher) {
var hash = typeof hasher === 'function' ? hasher : defaultHash;
var state = { patch: [], hash: hash };
return appendChanges(a, b, '', state).patch.reverse();
}
function appendChanges(a, b, path, state) {
if(Array.isArray(a) && Array.isArray(b)) {
return appendListChanges(a, b, path, state);
}
if(isValidObject(a) && isValidObject(b)) {
return appendObjectChanges(a, b, path, state);
}
return appendValueChanges(a, b, path, state);
}
function appendObjectChanges(o1, o2, path, state) {
state = Object.keys(o2).reduceRight(function(state, key) {
var keyPath = path + '/' + encodeSegment(key);
if(key in o1) {
appendChanges(o1[key], o2[key], keyPath, state);
} else {
state.patch.push({ op: 'add', path: keyPath, value: o2[key] });
}
return state;
}, state);
return Object.keys(o1).reduceRight(function(state, key) {
if(!(key in o2)) {
var p = path + '/' + encodeSegment(key);
// remove.value is for monomorphism, not strictly necessary
state.patch.push({ op: 'remove', path: p, value: void 0 });
state.patch.push({ op: 'test', path: p, value: o1[key] });
}
return state;
}, state);
}
function appendListChanges(a1, a2, path, state) {
var a1hash = a1.map(state.hash);
var a2hash = a2.map(state.hash);
var lcsMatrix = lcs.compare(a1hash, a2hash);
return lcsToJsonPatch(a1, a2, path, state, lcsMatrix);
}
function lcsToJsonPatch(a1, a2, path, state, lcsMatrix) {
return lcs.reduce(function(state, op, i, j) {
var last, p;
if (op == lcs.REMOVE) {
p = path + '/' + j;
// Coalesce adjacent remove + add into replace
last = state.patch[state.patch.length-1];
if(last !== void 0 && last.op === 'add' && last.path === p) {
last.op = 'replace';
} else {
state.patch.push({ op: 'remove', path: p, value: void 0 });
}
state.patch.push({ op: 'test', path: p, value: a1[j] });
} else if (op == lcs.ADD) {
// See https://tools.ietf.org/html/rfc6902#section-4.1
// May use either index===length *or* '-' to indicate appending to array
state.patch.push({ op: 'add', path: path + '/' + j, value: a2[i] });
} else {
appendChanges(a1[j], a2[i], path + '/' + j, state);
}
return state;
}, state, lcsMatrix);
}
function appendValueChanges(a, b, path, state) {
if(a !== b) {
state.patch.push({ op: 'replace', path: path, value: b });
state.patch.push({ op: 'test', path: path, value: a });
}
return state;
}
function defaultHash(x) {
return JSON.stringify(x);
}
function isValidObject (x) {
return x !== null && typeof x === 'object';
}