jiff
Version:
JSON diff and patch based on rfc6902
382 lines (310 loc) • 9.22 kB
JavaScript
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';
}