jiff
Version:
JSON diff and patch based on rfc6902
241 lines (208 loc) • 6.6 kB
JavaScript
/** @license MIT License (c) copyright 2010-2014 original author or authors */
/** @author Brian Cavalier */
/** @author John Hann */
var lcs = require('./lib/lcs');
var array = require('./lib/array');
var patch = require('./lib/jsonPatch');
var inverse = require('./lib/inverse');
var jsonPointer = require('./lib/jsonPointer');
var encodeSegment = jsonPointer.encodeSegment;
exports.diff = diff;
exports.patch = patch.apply;
exports.patchInPlace = patch.applyInPlace;
exports.inverse = inverse;
exports.clone = patch.clone;
// Errors
exports.InvalidPatchOperationError = require('./lib/InvalidPatchOperationError');
exports.TestFailedError = require('./lib/TestFailedError');
exports.PatchNotInvertibleError = require('./lib/PatchNotInvertibleError');
var isValidObject = patch.isValidObject;
var defaultHash = patch.defaultHash;
/**
* Compute a JSON Patch representing the differences between a and b.
* @param {object|array|string|number|null} a
* @param {object|array|string|number|null} b
* @param {?function|?object} options if a function, see options.hash
* @param {?function(x:*):String|Number} options.hash used to hash array items
* in order to recognize identical objects, defaults to JSON.stringify
* @param {?function(index:Number, array:Array):object} options.makeContext
* used to generate patch context. If not provided, context will not be generated
* @returns {array} JSON Patch such that patch(diff(a, b), a) ~ b
*/
function diff(a, b, options) {
return appendChanges(a, b, '', initState(options, [])).patch;
}
/**
* Create initial diff state from the provided options
* @param {?function|?object} options @see diff options above
* @param {array} patch an empty or existing JSON Patch array into which
* the diff should generate new patch operations
* @returns {object} initialized diff state
*/
function initState(options, patch) {
if(typeof options === 'object') {
return {
patch: patch,
hash: orElse(isFunction, options.hash, defaultHash),
makeContext: orElse(isFunction, options.makeContext, defaultContext),
invertible: !(options.invertible === false)
};
} else {
return {
patch: patch,
hash: orElse(isFunction, options, defaultHash),
makeContext: defaultContext,
invertible: true
};
}
}
/**
* Given two JSON values (object, array, number, string, etc.), find their
* differences and append them to the diff state
* @param {object|array|string|number|null} a
* @param {object|array|string|number|null} b
* @param {string} path
* @param {object} state
* @returns {Object} updated diff state
*/
function appendChanges(a, b, path, state) {
if(Array.isArray(a) && Array.isArray(b)) {
return appendArrayChanges(a, b, path, state);
}
if(isValidObject(a) && isValidObject(b)) {
return appendObjectChanges(a, b, path, state);
}
return appendValueChanges(a, b, path, state);
}
/**
* Given two objects, find their differences and append them to the diff state
* @param {object} o1
* @param {object} o2
* @param {string} path
* @param {object} state
* @returns {Object} updated diff state
*/
function appendObjectChanges(o1, o2, path, state) {
var keys = Object.keys(o2);
var patch = state.patch;
var i, key;
for(i=keys.length-1; i>=0; --i) {
key = keys[i];
var keyPath = path + '/' + encodeSegment(key);
if(o1[key] !== void 0) {
appendChanges(o1[key], o2[key], keyPath, state);
} else {
patch.push({ op: 'add', path: keyPath, value: o2[key] });
}
}
keys = Object.keys(o1);
for(i=keys.length-1; i>=0; --i) {
key = keys[i];
if(o2[key] === void 0) {
var p = path + '/' + encodeSegment(key);
if(state.invertible) {
patch.push({ op: 'test', path: p, value: o1[key] });
}
patch.push({ op: 'remove', path: p });
}
}
return state;
}
/**
* Given two arrays, find their differences and append them to the diff state
* @param {array} a1
* @param {array} a2
* @param {string} path
* @param {object} state
* @returns {Object} updated diff state
*/
function appendArrayChanges(a1, a2, path, state) {
var a1hash = array.map(state.hash, a1);
var a2hash = array.map(state.hash, a2);
var lcsMatrix = lcs.compare(a1hash, a2hash);
return lcsToJsonPatch(a1, a2, path, state, lcsMatrix);
}
/**
* Transform an lcsMatrix into JSON Patch operations and append
* them to state.patch, recursing into array elements as necessary
* @param {array} a1
* @param {array} a2
* @param {string} path
* @param {object} state
* @param {object} lcsMatrix
* @returns {object} new state with JSON Patch operations added based
* on the provided lcsMatrix
*/
function lcsToJsonPatch(a1, a2, path, state, lcsMatrix) {
var offset = 0;
return lcs.reduce(function(state, op, i, j) {
var last, context;
var patch = state.patch;
var p = path + '/' + (j + offset);
if (op === lcs.REMOVE) {
// Coalesce adjacent remove + add into replace
last = patch[patch.length-1];
context = state.makeContext(j, a1);
if(state.invertible) {
patch.push({ op: 'test', path: p, value: a1[j], context: context });
}
if(last !== void 0 && last.op === 'add' && last.path === p) {
last.op = 'replace';
last.context = context;
} else {
patch.push({ op: 'remove', path: p, context: context });
}
offset -= 1;
} else if (op === lcs.ADD) {
// See https://tools.ietf.org/html/rfc6902#section-4.1
// May use either index===length *or* '-' to indicate appending to array
patch.push({ op: 'add', path: p, value: a2[i],
context: state.makeContext(j, a1)
});
offset += 1;
} else {
appendChanges(a1[j], a2[i], p, state);
}
return state;
}, state, lcsMatrix);
}
/**
* Given two number|string|null values, if they differ, append to diff state
* @param {string|number|null} a
* @param {string|number|null} b
* @param {string} path
* @param {object} state
* @returns {object} updated diff state
*/
function appendValueChanges(a, b, path, state) {
if(a !== b) {
if(state.invertible) {
state.patch.push({ op: 'test', path: path, value: a });
}
state.patch.push({ op: 'replace', path: path, value: b });
}
return state;
}
/**
* @param {function} predicate
* @param {*} x
* @param {*} y
* @returns {*} x if predicate(x) is truthy, otherwise y
*/
function orElse(predicate, x, y) {
return predicate(x) ? x : y;
}
/**
* Default patch context generator
* @returns {undefined} undefined context
*/
function defaultContext() {
return void 0;
}
/**
* @param {*} x
* @returns {boolean} true if x is a function, false otherwise
*/
function isFunction(x) {
return typeof x === 'function';
}