UNPKG

jiff

Version:

JSON diff and patch based on rfc6902

241 lines (208 loc) 6.6 kB
/** @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'; }