1module.exports = { 2 diff: diff, 3 jsonPatchPathConverter: jsonPatchPathConverter, 4}; 5 6/* 7 const obj1 = {a: 4, b: 5}; 8 const obj2 = {a: 3, b: 5}; 9 const obj3 = {a: 4, c: 5}; 10 11 diff(obj1, obj2); 12 [ 13 { "op": "replace", "path": ['a'], "value": 3 } 14 ] 15 16 diff(obj2, obj3); 17 [ 18 { "op": "remove", "path": ['b'] }, 19 { "op": "replace", "path": ['a'], "value": 4 } 20 { "op": "add", "path": ['c'], "value": 5 } 21 ] 22 23 // using converter to generate jsPatch standard paths 24 // see http://jsonpatch.com 25 import {diff, jsonPatchPathConverter} from 'just-diff' 26 diff(obj1, obj2, jsonPatchPathConverter); 27 [ 28 { "op": "replace", "path": '/a', "value": 3 } 29 ] 30 31 diff(obj2, obj3, jsonPatchPathConverter); 32 [ 33 { "op": "remove", "path": '/b' }, 34 { "op": "replace", "path": '/a', "value": 4 } 35 { "op": "add", "path": '/c', "value": 5 } 36 ] 37 38 // arrays 39 const obj4 = {a: 4, b: [1, 2, 3]}; 40 const obj5 = {a: 3, b: [1, 2, 4]}; 41 const obj6 = {a: 3, b: [1, 2, 4, 5]}; 42 43 diff(obj4, obj5); 44 [ 45 { "op": "replace", "path": ['a'], "value": 3 } 46 { "op": "replace", "path": ['b', 2], "value": 4 } 47 ] 48 49 diff(obj5, obj6); 50 [ 51 { "op": "add", "path": ['b', 3], "value": 5 } 52 ] 53 54 // nested paths 55 const obj7 = {a: 4, b: {c: 3}}; 56 const obj8 = {a: 4, b: {c: 4}}; 57 const obj9 = {a: 5, b: {d: 4}}; 58 59 diff(obj7, obj8); 60 [ 61 { "op": "replace", "path": ['b', 'c'], "value": 4 } 62 ] 63 64 diff(obj8, obj9); 65 [ 66 { "op": "replace", "path": ['a'], "value": 5 } 67 { "op": "remove", "path": ['b', 'c']} 68 { "op": "add", "path": ['b', 'd'], "value": 4 } 69 ] 70*/ 71 72function diff(obj1, obj2, pathConverter) { 73 if (!obj1 || typeof obj1 != 'object' || !obj2 || typeof obj2 != 'object') { 74 throw new Error('both arguments must be objects or arrays'); 75 } 76 77 pathConverter || 78 (pathConverter = function(arr) { 79 return arr; 80 }); 81 82 function getDiff({obj1, obj2, basePath, basePathForRemoves, diffs}) { 83 var obj1Keys = Object.keys(obj1); 84 var obj1KeysLength = obj1Keys.length; 85 var obj2Keys = Object.keys(obj2); 86 var obj2KeysLength = obj2Keys.length; 87 var path; 88 89 var lengthDelta = obj1.length - obj2.length; 90 91 if (trimFromRight(obj1, obj2)) { 92 for (var i = 0; i < obj1KeysLength; i++) { 93 var key = Array.isArray(obj1) ? Number(obj1Keys[i]) : obj1Keys[i]; 94 if (!(key in obj2)) { 95 path = basePathForRemoves.concat(key); 96 diffs.remove.push({ 97 op: 'remove', 98 path: pathConverter(path), 99 }); 100 } 101 } 102 103 for (var i = 0; i < obj2KeysLength; i++) { 104 var key = Array.isArray(obj2) ? Number(obj2Keys[i]) : obj2Keys[i]; 105 pushReplaces({ 106 key, 107 obj1, 108 obj2, 109 path: basePath.concat(key), 110 pathForRemoves: basePath.concat(key), 111 diffs, 112 }); 113 } 114 } else { 115 // trim from left, objects are both arrays 116 for (var i = 0; i < lengthDelta; i++) { 117 path = basePathForRemoves.concat(i); 118 diffs.remove.push({ 119 op: 'remove', 120 path: pathConverter(path), 121 }); 122 } 123 124 // now make a copy of obj1 with excess elements left trimmed and see if there any replaces 125 var obj1Trimmed = obj1.slice(lengthDelta);; 126 for (var i = 0; i < obj2KeysLength; i++) { 127 pushReplaces({ 128 key: i, 129 obj1: obj1Trimmed, 130 obj2, 131 path: basePath.concat(i), 132 // since list of removes are reversed before presenting result, 133 // we need to ignore existing parent removes when doing nested removes 134 pathForRemoves: basePath.concat(i + lengthDelta), 135 diffs, 136 }); 137 } 138 } 139 } 140 141 var diffs = {remove: [], replace: [], add: []}; 142 getDiff({ 143 obj1, 144 obj2, 145 basePath: [], 146 basePathForRemoves: [], 147 diffs, 148 }); 149 150 // reverse removes since we want to maintain indexes 151 return diffs.remove 152 .reverse() 153 .concat(diffs.replace) 154 .concat(diffs.add); 155 156 function pushReplaces({key, obj1, obj2, path, pathForRemoves, diffs}) { 157 var obj1AtKey = obj1[key]; 158 var obj2AtKey = obj2[key]; 159 160 if(!(key in obj1) && (key in obj2)) { 161 var obj2Value = obj2AtKey; 162 diffs.add.push({ 163 op: 'add', 164 path: pathConverter(path), 165 value: obj2Value, 166 }); 167 } else if(obj1AtKey !== obj2AtKey) { 168 if(Object(obj1AtKey) !== obj1AtKey || 169 Object(obj2AtKey) !== obj2AtKey || differentTypes(obj1AtKey, obj2AtKey) 170 ) { 171 pushReplace(path, diffs, obj2AtKey); 172 } else { 173 if(!Object.keys(obj1AtKey).length && 174 !Object.keys(obj2AtKey).length && 175 String(obj1AtKey) != String(obj2AtKey)) { 176 pushReplace(path, diffs, obj2AtKey); 177 } else { 178 getDiff({ 179 obj1: obj1[key], 180 obj2: obj2[key], 181 basePath: path, 182 basePathForRemoves: pathForRemoves, 183 diffs}); 184 } 185 } 186 } 187 } 188 189 function pushReplace(path, diffs, newValue) { 190 diffs.replace.push({ 191 op: 'replace', 192 path: pathConverter(path), 193 value: newValue, 194 }); 195 } 196} 197 198function jsonPatchPathConverter(arrayPath) { 199 return [''].concat(arrayPath).join('/'); 200} 201 202function differentTypes(a, b) { 203 return Object.prototype.toString.call(a) != Object.prototype.toString.call(b); 204} 205 206function trimFromRight(obj1, obj2) { 207 var lengthDelta = obj1.length - obj2.length; 208 if (Array.isArray(obj1) && Array.isArray(obj2) && lengthDelta > 0) { 209 var leftMatches = 0; 210 var rightMatches = 0; 211 for (var i = 0; i < obj2.length; i++) { 212 if (String(obj1[i]) === String(obj2[i])) { 213 leftMatches++; 214 } else { 215 break; 216 } 217 } 218 for (var j = obj2.length; j > 0; j--) { 219 if (String(obj1[j + lengthDelta]) === String(obj2[j])) { 220 rightMatches++; 221 } else { 222 break; 223 } 224 } 225 226 // bias to trim right becase it requires less index shifting 227 return leftMatches >= rightMatches; 228 } 229 return true; 230} 231