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