d2l-json-patch-gen
Version:
JavaScript library that generates JSON patches (RFC 6902)
188 lines (151 loc) • 5.72 kB
JavaScript
/*global module*/
const _ = require( 'lodash' );
(function() {
'use strict';
var checkIsJsonValue = function( value ) {
if (typeof value === 'string') {
return value;
}
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'number') {
return value;
}
if (typeof value === 'object') {
return value;
}
throw new Error(value.toString() + ' is not a valid JSON value');
};
var buildPath = function( segments ) {
if (segments.length === 0) {
return '';
}
return '/' + segments.map(function( seg ) {
if (typeof seg === 'string') {
return seg.replace(/~/g, '~0').replace(/\//g, '~1');
}
return seg;
}).join('/');
};
// TODO: look at Hirschberg's algorithm to do better than O(n^2) space
var arrayDiff = function( arr1, arr2, path ) {
var len = Math.max(arr1.length, arr2.length)+1;
var init = function ( len ) {
var table = new Array(len);
for (var i = 0; i < len; ++i) {
table[i] = new Array(len);
table[0][i] = { cost: i, op: i > 0 ? 'i' : undefined };
table[i][0] = { cost: i, op: i > 0 ? 'd' : undefined };
}
return table;
};
var diff = function ( table ) {
var arr1val, arr2val, match, ins, del, cost, eql;
for (var i = 1; i <= arr1.length; ++i) {
for (var j = 1; j <= arr2.length; ++j) {
arr1val = arr1[i-1];
arr2val = arr2[j-1];
eql = _.isEqual( arr1val, arr2val );
match = table[i-1][j-1].cost + (eql ? 0 : 1);
ins = table[i][j-1].cost + 1;
del = table[i-1][j].cost + 1;
cost = Math.min(match, ins, del);
table[i][j] = {};
table[i][j].cost = cost;
table[i][j].op = cost === match ? (eql ? 'm' : 'r') : (cost === ins ? 'i' : 'd');
}
}
return table;
};
var constructPath = function ( table ) {
var i = arr1.length,
j = arr2.length,
entry = table[i][j],
acc = [];
while (entry.op !== undefined) {
if (entry.op === 'i') {
acc.push({op: 'add', path: buildPath(path.concat(i)), value: arr2[--j]});
} else if (entry.op === 'd') {
acc.push({op: 'remove', path: buildPath(path.concat(--i))});
} else if (entry.op === 'r') {
acc.push({op: 'replace', path: path.concat(--i), value: arr2[--j]});
} else if (entry.op === 'm') {
i--; j--;
} else {
throw new Error('Unknown op: ' + entry.op);
}
entry = table[i][j];
}
return acc;
};
var deepDiff = function( patches ) {
var idx, patch, patchIdx;
for (idx in patches) {
patch = patches[idx];
if (patch.op === 'replace') {
patchIdx = patch.path[patch.path.length - 1];
patches[idx] = valueDiff(arr1[patchIdx], patch.value, patch.path);
}
}
return patches;
};
var flatten = function( arr ) {
return arr.map(function( x ) {
return Array.isArray(x) ? x : [x];
}).reduce(function(a, b) {
return a.concat(b);
}, []);
};
return flatten(deepDiff(constructPath(diff(init(len)))));
};
var valueDiff = function( val1, val2, path ) {
var acc = [],
x;
if (Array.isArray(val1) && Array.isArray(val2)) {
return arrayDiff(val1, val2, path);
}
if (Array.isArray(val1) !== Array.isArray(val2)) {
return [{op: 'replace', path: buildPath(path), value: val2}];
}
if (typeof val1 !== 'object' || typeof val2 !== 'object' || val1 === null || val2 === null) {
if (val1 === val2) {
return [];
}
return [{op: 'replace', path: buildPath(path), value: val2}];
}
for (x in val2) {
if (! val2.hasOwnProperty(x)) {
throw new Error(val2.toString() + ' has a prototype');
}
if (x in val1) {
acc = acc.concat(valueDiff(val1[x], val2[x], path.concat(x)));
} else {
acc.push({op: 'add', path: buildPath(path.concat(x)), value: checkIsJsonValue(val2[x])});
}
}
for (x in val1) {
if (! val1.hasOwnProperty(x)) {
throw new Error(val1.toString() + ' has a prototype');
}
if (!(x in val2)) {
acc.push({op: 'remove', path: buildPath(path.concat(x))});
}
}
return acc;
};
var diff = function( obj1, obj2 ) {
if (typeof obj1 !== 'object') {
throw new TypeError('obj1 is not an object: ' + obj1.toString());
}
if (typeof obj2 !== 'object') {
throw new TypeError('obj2 is not an object: ' + obj2.toString());
}
return valueDiff(obj1, obj2, []);
};
if( typeof module !== 'undefined' && module.exports ) {
module.exports = diff;
} else {
this.diff = diff;
}
}).call(this);