public
Last active

JavaScript implementation for constructing objects diffs compatible with JSON PATCH syntax.

  • Download Gist
jsondiff.js
JavaScript
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 128 129 130 131 132 133 134 135
var jsondiff = (function() {
 
// Patch helper functions
function getParent(paths, path) {
return paths[path.substr(0, path.match(/\//g).length)];
}
 
// Checks if `obj` is an array or object
function isContainer(obj) {
return _.isArray(obj) || _.isObject(obj);
}
 
// Checks if the two objects are of the same container type
function isSameContainer(obj1, obj2) {
return (_.isArray(obj1) && _.isArray(obj2)) || (_.isObject(obj1) && _.isObject(obj2));
}
 
// Flattens an object to a hash of paths and values.
function flattenObject(obj, prefix, paths) {
prefix || (prefix = '/');
paths || (paths = {});
 
// Do not bother logging the root path
paths[prefix] = {
path: prefix,
value: obj
};
 
prefix !== '/' && (prefix = prefix + '/')
 
// Recurse for container types
if (_.isArray(obj)) {
for (var i = 0, l = obj.length; i < l; i++) {
flattenObject(obj[i], prefix + i, paths);
}
} else if (_.isObject(obj)) {
for (var key in obj) {
flattenObject(obj[key], prefix + key, paths);
}
}
 
return paths;
}
 
// Constructs a patch that when applied to `obj2`, it will be equivalent
// to `obj1`. The patch format conforms to IETF JSON Patch proposal
// http://tools.ietf.org/html/draft-ietf-appsawg-json-patch-01
function constructPatch(obj1, obj2) {
// Patches are only applicable to two of the same container types.
if (!isSameContainer(obj1, obj2)) {
throw new Error('Patches can only be derived from objects or arrays');
}
 
var paths1 = flattenObject(obj1),
paths2 = flattenObject(obj2),
key1,
key2,
doc1,
doc2,
patch = [],
add = {},
remove = {},
replace = {},
move = {};
 
// Iterate over the first object's paths and compare them to the second
// set of paths.
for (key1 in paths1) {
doc1 = paths1[key1], doc2 = paths2[key1];
// If the parent of `doc2` doesn't exist, skip it since neither a
// remove or replace can occur.
if (!getParent(paths2, key1)) {
continue;
}
// If there is a miss in the second object, the key will be marked for
// removal.
if (!doc2) {
remove[key1] = doc1;
// If both members have existing values, make sure they are not the
// same container and they are not equal. If they are the same
// container type, values will be replaced downstream.
} else if (!isSameContainer(doc1.value, doc2.value) && !_.isEqual(doc1.value, doc2.value)) {
replace[key1] = doc2;
}
}
 
// Iterate over the second object's paths and compare them to the first
// set of paths.
for (key2 in paths2) {
doc1 = paths1[key2], doc2 = paths2[key2];
// Missing in first object, thus we mark it to be added.
// If the parent path is not present in the first obj, then this
// means the whole array/object is new.
if (!doc1 && isSameContainer(getParent(paths1, key2), getParent(paths2, key2))) {
add[key2] = doc2;
}
}
 
// Attempt to promote add/remove operations to a move operation.
// The first occurence of the same value, we can promote to a move.
for (key1 in remove) {
doc1 = remove[key1];
for (key2 in add) {
doc2 = add[key2];
if (_.isEqual(doc2.value, doc1.value)) {
// Remove them from previous hashes
delete remove[key1];
delete add[key2];
move[key1] = doc2
break;
}
}
}
 
var key;
// Populate the patch
for (key in add) {
patch.push({add: key, value: add[key].value});
}
for (key in remove) {
patch.push({remove: key});
}
for (key in replace) {
patch.push({replace: key, value: replace[key].value});
}
for (key in move) {
patch.push({move: key, to: move[key].path, value: move[key].value});
}
 
return patch;
}
 
return constructPatch;
 
})();

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.