UNPKG

json-diff-rfc6902

Version:

This framework is to compare two JSON data and generate the Patch

458 lines (379 loc) 14 kB
var applyPatches = require('./lib/applyPatches'); var unchangedArea = require('./lib/unchangedArea.js'); var patchArea = require('./lib/patchArea.js'); var hashObject = require('./lib/hashObject.js'); exports.diff = diff; exports.apply = apply; // browserify -s jdr -e JSON-Diff.js -o json-diff-rfc6902.js var OBJ_COM = true; var ARR_COM = true; var HASH_ID = null; function apply(app_old, jpn_patch) { applyPatches.apply(app_old, jpn_patch); } function diff(oldJson, newJson, options) { // Initial if(typeof options === 'object') { if(options.OBJ_COM !== void 0) {OBJ_COM = options.OBJ_COM;} if(options.ARR_COM !== void 0) {ARR_COM = options.ARR_COM;} if(options.HASH_ID !== void 0) {HASH_ID = options.HASH_ID;} } // Get the unchanged area var unchanged = []; if (OBJ_COM === true) { unchangedArea.generateUnchanged(oldJson, newJson, unchanged, ''); } // Generate the diff var patches = []; generateDiff(oldJson, newJson, unchanged, patches, ''); patchArea.handlePatch(patches); return patches; } function generateDiff(oldJson, newJson, unchanged, patches, path) { // var a = null object Array.isArray: false // var a = 5 number // var a = [1,2] object Array.isArray: true // var a undefined Array.isArray: false if (Array.isArray(oldJson) && Array.isArray(newJson)) { generateArrayDiff(oldJson, newJson, unchanged, patches, path); return; } if (typeof oldJson === "object" && oldJson !== null && typeof newJson === "object" && newJson !== null) { generateObjectDiff(oldJson, newJson, unchanged, patches, path); return; } return generateValueDiff(oldJson, newJson, unchanged, patches, path); } function generateValueDiff(oldJson, newJson, unchanged, patches, path) { // the endpoint if (newJson !== oldJson) { patches.push({ op: "replace", path: path, value: newJson}); } } function generateArrayDiff(oldJson, newJson, unchanged, patches, path) { // console.log("--------This is Array-------------"); // x, y is the hash of json // var x = hashObject.map(hashObject.hash, oldJson); // var y = hashObject.map(hashObject.hash, newJson); if (oldJson.length === 0 && newJson.length ===0 ) {return;} // Use LCS var tmpPatches = []; var tmpPatchHashes = []; if (oldJson.length === 0) { patches.push({ op: "add", path: path, value: newJson}); } else { // Use sortBack tmpPatches = transformArray(oldJson, newJson, unchanged, tmpPatches, tmpPatchHashes, path); for (var l = 0; l < tmpPatches.length; l++) { patches.push(tmpPatches[l]); } } } function generateObjectDiff(oldJson, newJson, unchanged, patches, path) { var oldKeys = Object.keys(oldJson); var newKeys = Object.keys(newJson); var removed = false; var oldKey, oldValue; // Loop from the old; from lengths -1 to 0 for (var i = oldKeys.length -1; i >= 0; i--) { oldKey = oldKeys[i]; oldValue = oldJson[oldKey]; if (newJson.hasOwnProperty(oldKey)) { // go deeper generateDiff(oldJson[oldKey], newJson[oldKey], unchanged, patches, path + "/" + oldKey); } else { // Remove removed = true; patches.push({ op: "remove", path: path + "/" + patchPointString(oldKey), value: oldValue }); } } // If doesn't remove and the length is the same, return // Return: only the length is equal and doesn't remove if (!removed && newKeys.length === oldKeys.length) { return; } // Loop from the new // length is not the same var newKey; var newVal; for (var j = 0; j < newKeys.length; j ++) { newKey = newKeys[j]; newVal = newJson[newKey]; if (!oldJson.hasOwnProperty(newKey)) { //Try to find the value in the unchanged area // change JSON.stringify() var pointer = unchangedArea.findValueInUnchanged(JSON.stringify(newVal), unchanged); if (pointer) { //COPY patches.push({ op: "copy", path: path + "/" + patchPointString(newKey), from: pointer}); } else { // no json.stringnify var previousIndex = -1; if (OBJ_COM === true) { previousIndex = patchArea.findValueInPatch(newVal, patches); } if (previousIndex !== -1 && patches[previousIndex].op === 'remove') { // MOVE var oldPath = patches[previousIndex].path; patches.splice(previousIndex, 1); patches.push({ op: "move", from: oldPath, path: path + "/" + patchPointString(newKey)}); } else { //ADD patches.push({ op: "add", path: path + "/" + patchPointString(newKey), value: newVal}); } } } } } function patchPointString(str) { // According to RFC 6901 // '~' needs to be encoded as '~0' // '/' needs to be encoded as '~1' if (str.indexOf('/') === -1 && str.indexOf('~') === -1) { return str; } return str.replace(/~/g, '~0').replace(/\//g, '~1'); } function transformIndex(element, m, array) { var finalIndex; switch(element.op) { case 'add': case 'replace': case 'copy': // When add, replace and copy, add directly return element.index; case 'remove': finalIndex = element.index; break; case 'move': finalIndex = element.from; break; } for (var i = 0; i < m; i++) { switch (array[i].op) { case 'remove': if(finalIndex > array[i].index) { // when equal, don't -- finalIndex --; } break; case 'add': case 'copy': if(finalIndex >= array[i].index) { // when equal, do ++ finalIndex ++; } break; case 'replace': // when equal, don't change break; case 'move': if (array[i].from !== array[i].index) { var min = Math.min(array[i].from, array[i].index); var max = Math.max(array[i].from, array[i].index); if (finalIndex >= min && finalIndex <= max) { if (array[i].from > array[i].index) { finalIndex ++; } else { finalIndex --; } } } break; } } return finalIndex; } function operationValue (op) { switch (op) { case "move" : return 0; case "remove" : return 1; case "add" : return 2; case "replace": return 3; case "copy" : return 4; } } function compare(a, b) { if (a.index === b.index) { // Order: move < remove < add var a_value = operationValue(a.op); var b_value = operationValue(b.op); if (a_value > b_value) { return 1; } else { return -1; } } else { return a.index - b.index; } } function findCopyInArray(element, m, array, arrUnchanged) { var copyIndex = -1; for (var i = 0; i < m; i++) { switch (element.op) { case 'remove': case 'copy': break; default: // Move Replace Add if (element.hash === array[i].hash) { return array[i].index; } break; } } //Find value in arrUnchanged for (var j= 0; j< arrUnchanged.length; j++) { if (element.hash === arrUnchanged[j].hash) { return arrUnchanged[j].index; } } return copyIndex; } function transformArray(oldJson, newJson, unchanged, patches, patchHashes, path, jsondiff) { //When is the Array, stop to find leaf node // (hash, value, index) var x = hashObject.mapArray(hashObject.hash, oldJson, HASH_ID); var y = hashObject.mapArray(hashObject.hash, newJson, HASH_ID); // Reserve the origin index // COPY ARRAY var x_sorted = x.slice(); var y_sorted = y.slice(); x_sorted.sort(function(a, b) {return a.hash - b.hash;}); y_sorted.sort(function(a, b) {return a.hash - b.hash;}); //Diff var arrPatch = [], arrUnchanged = [], arrtmp = []; var i= 0, j = 0; while (i < x_sorted.length) { while( j < y_sorted.length) { if(x_sorted[i] !== void 0) { if (x_sorted[i].hash > y_sorted[j].hash) { arrPatch.push({op: "add", value: y_sorted[j].value, index: y_sorted[j].index, hash: y_sorted[j].hash }); j++; } else if (x_sorted[i].hash === y_sorted[j].hash) { // Unchanged push unchanged.push( path + '/' + y_sorted[j].index + "=" + JSON.stringify(x_sorted[i].hash)); arrPatch.push({op: "move", value: y_sorted[j].value, valueOld: x_sorted[i].value, from: x_sorted[i].index , index: y_sorted[j].index, hash: y_sorted[j].hash }); i++; j++; } else { arrPatch.push({op: "remove", index: x_sorted[i].index, value: x_sorted[i].value}); i++; } } else { arrPatch.push({op: "add", value: y_sorted[j].value, index: y_sorted[j].index, hash: y_sorted[j].hash }); j++; } } if (i < x_sorted.length) { // Remove the rest elements of the x_sorted arrPatch.push({op: "remove", index: x_sorted[i].index, value: x_sorted[i].value }); i++; } } //Get the patch to make all the elements are the same, but index is random arrPatch = arrPatch.sort(compare); var m = 0; while(arrPatch[m] !== void 0) { // f_index = transformIndex(arrPatch[m], arrPatch); switch(arrPatch[m].op) { case 'add': arrPatch[m].index = transformIndex(arrPatch[m], m, arrPatch); // replace if (arrPatch[m-1] !== void 0 ) { if (arrPatch[m-1].op === 'remove' && arrPatch[m-1].index === arrPatch[m].index) { // if replace a object, go deeper // Set thresholds length == 30 // if (JSON.stringify(arrPatch[m-1].value).length > 20 && typeof arrPatch[m-1].value === "object" && arrPatch[m-1].value !== null && typeof arrPatch[m].value === "object" && arrPatch[m].value !== null) { if (typeof arrPatch[m-1].value === "object" && arrPatch[m-1].value !== null && typeof arrPatch[m].value === "object" && arrPatch[m].value !== null) { if (ARR_COM === true) { var tmPatch = []; //1. generateDiff(arrPatch[m-1].value, arrPatch[m].value, unchanged, tmPatch, path + "/" + arrPatch[m-1].index); //2. // tmPatch = fjp.compare(arrPatch[m-1].value, arrPatch[m].value); //Need to be fixed. arrPatch[m].op = 'replace'; arrPatch.splice(m-1,1); arrtmp.pop(); arrtmp = arrtmp.concat(tmPatch); continue; } else { arrPatch[m].op = 'replace'; arrPatch.splice(m-1,1); arrtmp.pop(); arrtmp.push({op: "replace", value: arrPatch[m-1].value, path: path + '/' + arrPatch[m-1].index}); continue; } } else { arrPatch[m].op = 'replace'; arrPatch.splice(m-1,1); arrtmp.pop(); arrtmp.push({op: "replace", value: arrPatch[m-1].value, path: path + '/' + arrPatch[m-1].index}); continue; } } } // COPY var copyIndex = findCopyInArray(arrPatch[m], m, arrPatch, arrUnchanged); if (copyIndex !== -1) { arrPatch[m].op = 'copy'; arrPatch[m].from = copyIndex; if (arrPatch[m].index === arrPatch[m].from) { arrPatch.splice(m, 1); continue; } arrtmp.push({op: "copy", from: path + '/' + arrPatch[m].from, path: path + '/' + arrPatch[m].index}); } else { arrtmp.push({op: "add", value: arrPatch[m].value , path: path + '/' + arrPatch[m].index}); } break; case 'remove': arrPatch[m].index = transformIndex(arrPatch[m], m, arrPatch); if (arrPatch[m-1] !== void 0) { // change move 2->1 and remove 2 to remove 1 if (arrPatch[m-1].op === 'move' && arrPatch[m-1].from === arrPatch[m].index && arrPatch[m-1].from === (arrPatch[m-1].index + 1) ) { arrPatch[m].index = arrPatch[m-1].index; arrPatch.splice(m-1,1); arrtmp.pop(); arrtmp.push({op: "remove", path: path + '/' + arrPatch[m-1].index}); continue; } else { arrtmp.push({op: "remove", path: path + '/' + arrPatch[m].index}); } } else { arrtmp.push({op: "remove", path: path + '/' + arrPatch[m].index}); } break; case 'move': arrPatch[m].from = transformIndex(arrPatch[m], m, arrPatch); if (arrPatch[m].index === arrPatch[m].from) { if (JSON.stringify(arrPatch[m].valueOld) === JSON.stringify(arrPatch[m].value)) { arrUnchanged.push(arrPatch[m]); arrPatch.splice(m, 1); continue; } else { //If index is the same, go to the internal node var tmMove = []; generateDiff(arrPatch[m].valueOld, arrPatch[m].value, unchanged, tmMove, path + "/" + arrPatch[m].index); // Remove current move in the patch. arrPatch.splice(m,1); arrtmp = arrtmp.concat(tmMove); continue; } } arrtmp.push({op: "move", from: path + '/' + arrPatch[m].from, path: path + '/' + arrPatch[m].index}); break; } m++; } arrPatch = arrPatch.map(function(obj) { obj.path = path + '/' + obj.index; delete obj.hash; delete obj.index; if (obj.op === 'move' || obj.op === 'copy') { obj.from = path + '/' + obj.from; delete obj.value; } return obj; }); return arrtmp; }