UNPKG

jiff

Version:

JSON diff and patch based on rfc6902

382 lines (310 loc) 9.22 kB
var jsonPointer = require('./jsonPointer'); var clone = require('./clone'); var deepEquals = require('./deepEquals'); var commutePaths = require('./commutePaths'); var array = require('./array'); var TestFailedError = require('./TestFailedError'); var InvalidPatchOperationError = require('./InvalidPatchOperationError'); var PatchNotInvertibleError = require('./PatchNotInvertibleError'); var find = jsonPointer.find; var parseArrayIndex = jsonPointer.parseArrayIndex; exports.test = { apply: applyTest, inverse: invertTest, commute: commuteTest }; exports.add = { apply: applyAdd, inverse: invertAdd, commute: commuteAddOrCopy }; exports.remove = { apply: applyRemove, inverse: invertRemove, commute: commuteRemove }; exports.replace = { apply: applyReplace, inverse: invertReplace, commute: commuteReplace }; exports.move = { apply: applyMove, inverse: invertMove, commute: commuteMove }; exports.copy = { apply: applyCopy, inverse: notInvertible, commute: commuteAddOrCopy }; /** * Apply a test operation to x * @param {object|array} x * @param {object} test test operation * @throws {TestFailedError} if the test operation fails */ function applyTest(x, test, options) { var pointer = find(x, test.path, options.findContext, test.context); var target = pointer.target; var index, value; if(Array.isArray(target)) { index = parseArrayIndex(pointer.key); //index = findIndex(options.findContext, index, target, test.context); value = target[index]; } else { value = pointer.key === void 0 ? pointer.target : pointer.target[pointer.key]; } if(!deepEquals(value, test.value)) { throw new TestFailedError('test failed ' + JSON.stringify(test)); } return x; } /** * Invert the provided test and add it to the inverted patch sequence * @param pr * @param test * @returns {number} */ function invertTest(pr, test) { pr.push(test); return 1; } function commuteTest(test, b) { if(test.path === b.path && b.op === 'remove') { throw new TypeError('Can\'t commute test,remove -> remove,test for same path'); } if(b.op === 'test' || b.op === 'replace') { return [b, test]; } return commutePaths(test, b); } /** * Apply an add operation to x * @param {object|array} x * @param {object} change add operation */ function applyAdd(x, change, options) { var pointer = find(x, change.path, options.findContext, change.context); if(notFound(pointer)) { throw new InvalidPatchOperationError('path does not exist ' + change.path); } if(change.value === void 0) { throw new InvalidPatchOperationError('missing value'); } var val = clone(change.value); // If pointer refers to whole document, replace whole document if(pointer.key === void 0) { return val; } _add(pointer, val); return x; } function _add(pointer, value) { var target = pointer.target; if(Array.isArray(target)) { // '-' indicates 'append' to array if(pointer.key === '-') { target.push(value); } else if (pointer.key > target.length) { throw new InvalidPatchOperationError('target of add outside of array bounds') } else { target.splice(pointer.key, 0, value); } } else if(isValidObject(target)) { target[pointer.key] = value; } else { throw new InvalidPatchOperationError('target of add must be an object or array ' + pointer.key); } } function invertAdd(pr, add) { var context = add.context; if(context !== void 0) { context = { before: context.before, after: array.cons(add.value, context.after) } } pr.push({ op: 'test', path: add.path, value: add.value, context: context }); pr.push({ op: 'remove', path: add.path, context: context }); return 1; } function commuteAddOrCopy(add, b) { if(add.path === b.path && b.op === 'remove') { throw new TypeError('Can\'t commute add,remove -> remove,add for same path'); } return commutePaths(add, b); } /** * Apply a replace operation to x * @param {object|array} x * @param {object} change replace operation */ function applyReplace(x, change, options) { var pointer = find(x, change.path, options.findContext, change.context); if(notFound(pointer) || missingValue(pointer)) { throw new InvalidPatchOperationError('path does not exist ' + change.path); } if(change.value === void 0) { throw new InvalidPatchOperationError('missing value'); } var value = clone(change.value); // If pointer refers to whole document, replace whole document if(pointer.key === void 0) { return value; } var target = pointer.target; if(Array.isArray(target)) { target[parseArrayIndex(pointer.key)] = value; } else { target[pointer.key] = value; } return x; } function invertReplace(pr, c, i, patch) { var prev = patch[i-1]; if(prev === void 0 || prev.op !== 'test' || prev.path !== c.path) { throw new PatchNotInvertibleError('cannot invert replace w/o test'); } var context = prev.context; if(context !== void 0) { context = { before: context.before, after: array.cons(prev.value, array.tail(context.after)) } } pr.push({ op: 'test', path: prev.path, value: c.value }); pr.push({ op: 'replace', path: prev.path, value: prev.value }); return 2; } function commuteReplace(replace, b) { if(replace.path === b.path && b.op === 'remove') { throw new TypeError('Can\'t commute replace,remove -> remove,replace for same path'); } if(b.op === 'test' || b.op === 'replace') { return [b, replace]; } return commutePaths(replace, b); } /** * Apply a remove operation to x * @param {object|array} x * @param {object} change remove operation */ function applyRemove(x, change, options) { var pointer = find(x, change.path, options.findContext, change.context); // key must exist for remove if(notFound(pointer) || pointer.target[pointer.key] === void 0) { throw new InvalidPatchOperationError('path does not exist ' + change.path); } _remove(pointer); return x; } function _remove (pointer) { var target = pointer.target; var removed; if (Array.isArray(target)) { removed = target.splice(parseArrayIndex(pointer.key), 1); return removed[0]; } else if (isValidObject(target)) { removed = target[pointer.key]; delete target[pointer.key]; return removed; } else { throw new InvalidPatchOperationError('target of remove must be an object or array'); } } function invertRemove(pr, c, i, patch) { var prev = patch[i-1]; if(prev === void 0 || prev.op !== 'test' || prev.path !== c.path) { throw new PatchNotInvertibleError('cannot invert remove w/o test'); } var context = prev.context; if(context !== void 0) { context = { before: context.before, after: array.tail(context.after) } } pr.push({ op: 'add', path: prev.path, value: prev.value, context: context }); return 2; } function commuteRemove(remove, b) { if(remove.path === b.path && b.op === 'remove') { return [b, remove]; } return commutePaths(remove, b); } /** * Apply a move operation to x * @param {object|array} x * @param {object} change move operation */ function applyMove(x, change, options) { if(jsonPointer.contains(change.path, change.from)) { throw new InvalidPatchOperationError('move.from cannot be ancestor of move.path'); } var pto = find(x, change.path, options.findContext, change.context); var pfrom = find(x, change.from, options.findContext, change.fromContext); _add(pto, _remove(pfrom)); return x; } function invertMove(pr, c) { pr.push({ op: 'move', path: c.from, context: c.fromContext, from: c.path, fromContext: c.context }); return 1; } function commuteMove(move, b) { if(move.path === b.path && b.op === 'remove') { throw new TypeError('Can\'t commute move,remove -> move,replace for same path'); } return commutePaths(move, b); } /** * Apply a copy operation to x * @param {object|array} x * @param {object} change copy operation */ function applyCopy(x, change, options) { var pto = find(x, change.path, options.findContext, change.context); var pfrom = find(x, change.from, options.findContext, change.fromContext); if(notFound(pfrom) || missingValue(pfrom)) { throw new InvalidPatchOperationError('copy.from must exist'); } var target = pfrom.target; var value; if(Array.isArray(target)) { value = target[parseArrayIndex(pfrom.key)]; } else { value = target[pfrom.key]; } _add(pto, clone(value)); return x; } // NOTE: Copy is not invertible // See https://github.com/cujojs/jiff/issues/9 // This needs more thought. We may have to extend/amend JSON Patch. // At first glance, this seems like it should just be a remove. // However, that's not correct. It violates the involution: // invert(invert(p)) ~= p. For example: // invert(copy) -> remove // invert(remove) -> add // thus: invert(invert(copy)) -> add (DOH! this should be copy!) function notInvertible(_, c) { throw new PatchNotInvertibleError('cannot invert ' + c.op); } function notFound (pointer) { return pointer === void 0 || (pointer.target == null && pointer.key !== void 0); } function missingValue(pointer) { return pointer.key !== void 0 && pointer.target[pointer.key] === void 0; } /** * Return true if x is a non-null object * @param {*} x * @returns {boolean} */ function isValidObject (x) { return x !== null && typeof x === 'object'; }