diff --git a/README.md b/README.md index efba4db..1229b68 100644 --- a/README.md +++ b/README.md @@ -40,17 +40,20 @@ console.log(JSON.stringify(patched)); ### patch ```js -var b = jiff.patch(patch, a); +var b = jiff.patch(patch, a [, options]); ``` Given an rfc6902 JSON Patch, apply it to `a` and return a new patched JSON object/array/value. Patching is atomic, and is performed on a clone of `a`. Thus, if patching fails mid-patch, `a` will still be in a consistent state. +* `options` + * `options.findContext : function(index, array, context) -> number`: **Experimental** function to be called before each change to an array. It is passed the array and index of the change, *and* a patch context (see [`options.makeContext` below](#diff)). It should return an adjusted index at which the change will actually be applied. This allows for smart patching of arrays that may have changed since the patch was created. + Throws [InvalidPatchOperationError](#invalidpatchoperationerror) and [TestFailedError](#testfailederror). ### patchInPlace ```js -a = jiff.patchInPlace(patch, a); +a = jiff.patchInPlace(patch, a [, options]); ``` Given an rfc6902 JSON Patch, apply it directly to `a`, *mutating `a`*. @@ -64,12 +67,18 @@ Throws [InvalidPatchOperationError](#invalidpatchoperationerror) and [TestFailed ### diff ```js -var patch = jiff.diff(a, b [, hashFunction]); +var patch = jiff.diff(a, b [, hashFunction | options]); ``` -Computes and returns a JSON Patch from `a` to `b`: `a` and `b` must be valid JSON objects/arrays/values of the same type. If `patch` is applied to `a`, it will yield `b`. +Computes and returns a JSON Patch from `a` to `b`: `a` and `b` must be valid JSON objects/arrays/values. If `patch` is applied to `a`, it will yield `b`. + +The optional third parameter can be *either* an `options` object (preferably) or a function (deprecated: allowed backward compatibility). -If provided, the optional `hashFunction` will be used to recognize when two objects are the same. If not provided, `JSON.stringify` will be used. +* `options`: + * `options.hash : function(x) -> string|number`: used to recognize when two objects are the same. If not provided, `JSON.stringify` will be used for objects and arrays, and simply returns `x` for all other primitive values. + * `options.makeContext : function(index, array) -> *`: **Experimental** function that will be called for each item added or removed from an array. It can return *any* legal JSON value or undefined, which if not `null` or undefined, will be fed directly to the `findContext` function provided to [`jiff.patch`](#patch). + * `options.invertible : boolean`: by default, jiff generates patches containing extra `test` operations to ensure they are invertible via [`jiff.inverse`](#inverse). When `options.invertible === false` will omit the extra `test` operations. This will result in smaller patches, but they will not be invertible. +* `hashFunction(x) -> string|number`: same as `options.hash` above While jiff's patch algorithm handles all the JSON Patch operations required by rfc6902, the diff algorithm currently does not generate `move`, or `copy` operations, only `add`, `remove`, and `replace`. @@ -84,7 +93,7 @@ Compute an inverse patch. Applying the inverse of a patch will undo the effect Due to the current JSON Patch format defined in rfc6902, not all patches can be inverted. To be invertible, a patch must have the following characteristics: 1. Each `remove` and `replace` operation must be preceded by a `test` operation that verifies the `value` at the `path` being removed/replaced. -2. The patch must *not* contain any `copy` operations. Read [this discussion](https://github.com/cujojs/jiff/issues/9) to understand why `copy` operations are not (yet) invertible. You can achieve the same effect by using `add` instead of `copy`, albeit potentially at the cost of data size. +2. The patch must *not* contain any `copy` operations. Read [this discussion](https://github.com/cujojs/jiff/issues/9) to understand why `copy` operations are not (yet) invertible. You can achieve the same effect by using `add` instead of `copy`, albeit potentially at the cost of increased patch size. ### clone @@ -94,8 +103,51 @@ var b = jiff.clone(a); Creates a deep copy of `a`, which must be a valid JSON object/array/value. +**NOTE:** In jiff <= 0.6.x, `jiff.clone` incorrectly caused some ISO Date-formatted strings (eg `"2014-12-03T11:40:16.816Z"`) to be turned into `Date` objects. Thus, a clone *might not end up as an exact copy*. + +As of 0.7.0 `jiff.clone` creates exact copies. + +If you have code that depended on that hidden deserialization, *it will break*. Date deserialization is now the responsibility of the party who parsed the JSON string from which the original object/array/etc. (ie, the one passed to `jiff.clone`) was created. + +### Patch context + +As of v0.2, `jiff.diff` and `jiff.patch` support [patch contexts](http://en.wikipedia.org/wiki/Diff#Context_format), an extra bit of information carried with each patch operation. Patch contexts allow smarter patching, especially in the case of arrays, where items may have moved and thus their indices changed. + +Using patch contexts can greatly improve patch accuracy for arrays, at the cost of increasing the size of patches. + +Patch contexts are entirely opt-in. To use them, you must provide a pair of closely related functions: `makeContext` and `findContext`. An API for creating default `makeContext` and `findContext` functions is provided in [`jiff/lib/context`](#jifflibcontext), or you can implement your own. + +When you supply the optional `makeContext` function to `jiff.diff`, it will be used to generated a context for each change to an array. + +Likewise, when you supply the optional `findContext` function to `jiff.patch` (or `jiff.patchInPlace`), it will be used to find adjusted array indices where patches should actually be applied. + +The context is opaque, and jiff itself will not attempt to inspect or interpret it: `jiff.diff` will simply add whatever is returned by `makeContext` to patch operations, and `jiff.patch` will simply hand it to `findContext` when it sees a context in a patch operation. + + ## Experimental APIs +These APIs are still considered experimental, signatures may change. + +### jiff/lib/context + +```js +var context = require('jiff/lib/context'); + +// Create a makeContext function that can be passed to jiff.diff +var makeContext = context.makeContext(size); + +// Create a findContext function that can be passed to jiff.patch +var findContext = context.makeContextFinder(equals); +``` + +Provides simple, but effective default implementations of `makeContext` and `findContext` functions that can be passed to `jiff.diff` and `jiff.patch` to take advantage of smarter array patching. + +`context.makeContext(size)` *returns* a function that can be passed as `options.makeContext` to `jiff.diff`. + * `size: number` is the number of array items before and after each change to include in the patch. + +`context.makeContextFinder(equals)` *returns* a function that can be passed as `options.findContext` to `jiff.patch`. + * `equals: function(a, b) -> boolean` a function to compare two array items, must return truthy when `a` and `b` are equal, falsy otherwise. + ### jiff/lib/rebase ```js @@ -117,9 +169,9 @@ var [p2c, p1c] = commute(p1, p2); ``` Given two patches `p1` and `p2`, which are intended to be applied in the order `p1` then `p2`, transform them so that they can be safely applied in the order `p2c` and then `p1c`. - + Commutation is currently *highly experimental*. It works for patch operations whose path refers to a common array ancestor by transforming array indices. Operations that share a common object ancestor are simply swapped for now, which is likely not the right thing in most cases! - + Commutation does attempt to detect operations that cannot be commuted, and in such cases, will throw a `TypeError`. ## Errors diff --git a/bower.json b/bower.json index 4e3aaba..c680673 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,7 @@ { "name": "jiff", "main": "jiff.js", - "version": "0.5.5", + "version": "0.7.2", "authors": [ "Brian Cavalier " ], diff --git a/jiff.js b/jiff.js index 4a1da3a..65669e7 100644 --- a/jiff.js +++ b/jiff.js @@ -3,6 +3,7 @@ /** @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'); @@ -19,24 +20,61 @@ 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} a - * @param {object|array|string|number} b - * @param {function} hasher hashing function that will be used to - * recognize identical objects + * @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, hasher) { - var hash = typeof hasher === 'function' ? hasher : defaultHash; - var state = { patch: [], hash: hash }; +function diff(a, b, options) { + return appendChanges(a, b, '', initState(options, [])).patch; +} - return appendChanges(a, b, '', state).patch.reverse(); +/** + * 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 appendListChanges(a, b, path, state); + return appendArrayChanges(a, b, path, state); } if(isValidObject(a) && isValidObject(b)) { @@ -46,62 +84,108 @@ function appendChanges(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) { - state = Object.keys(o2).reduceRight(function(state, key) { + 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(key in o1) { + if(o1[key] !== void 0) { appendChanges(o1[key], o2[key], keyPath, state); } else { - state.patch.push({ op: 'add', path: keyPath, value: o2[key] }); + patch.push({ op: 'add', path: keyPath, value: o2[key] }); } + } - return state; - }, state); - - return Object.keys(o1).reduceRight(function(state, key) { - if(!(key in o2)) { + 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); - // remove.value is for monomorphism, not strictly necessary - state.patch.push({ op: 'remove', path: p, value: void 0 }); - state.patch.push({ op: 'test', path: p, value: o1[key] }); - + if(state.invertible) { + patch.push({ op: 'test', path: p, value: o1[key] }); + } + patch.push({ op: 'remove', path: p }); } + } - return state; - }, state); + return state; } -function appendListChanges(a1, a2, path, state) { - var a1hash = a1.map(state.hash); - var a2hash = a2.map(state.hash); +/** + * 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, p; - if (op == lcs.REMOVE) { - p = path + '/' + j; + var last, context; + var patch = state.patch; + var p = path + '/' + (j + offset); + if (op === lcs.REMOVE) { // Coalesce adjacent remove + add into replace - last = state.patch[state.patch.length-1]; + 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 { - state.patch.push({ op: 'remove', path: p, value: void 0 }); + patch.push({ op: 'remove', path: p, context: context }); } - state.patch.push({ op: 'test', path: p, value: a1[j] }); - } else if (op == lcs.ADD) { + 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 - state.patch.push({ op: 'add', path: path + '/' + j, value: a2[i] }); + patch.push({ op: 'add', path: p, value: a2[i], + context: state.makeContext(j, a1) + }); + + offset += 1; } else { - appendChanges(a1[j], a2[i], path + '/' + j, state); + appendChanges(a1[j], a2[i], p, state); } return state; @@ -109,19 +193,48 @@ function lcsToJsonPatch(a1, a2, path, state, lcsMatrix) { }, 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 }); - state.patch.push({ op: 'test', path: path, value: a }); } return state; } -function defaultHash(x) { - return JSON.stringify(x); +/** + * @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; } -function isValidObject (x) { - return x !== null && typeof x === 'object'; +/** + * 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'; } diff --git a/lib/array.js b/lib/array.js new file mode 100644 index 0000000..0898b84 --- /dev/null +++ b/lib/array.js @@ -0,0 +1,54 @@ +/** @license MIT License (c) copyright 2010-2014 original author or authors */ +/** @author Brian Cavalier */ +/** @author John Hann */ + +exports.cons = cons; +exports.tail = tail; +exports.map = map; + +/** + * Prepend x to a, without mutating a. Faster than a.unshift(x) + * @param {*} x + * @param {Array} a array-like + * @returns {Array} new Array with x prepended + */ +function cons(x, a) { + var l = a.length; + var b = new Array(l+1); + b[0] = x; + for(var i=0; i 0 || bmax < blen) { + index = findPositionWith(equals, array, start, + before.slice(bmax), + after.slice(0, amax)); + + if(index >= 0) { + return index; + } + + bmax = Math.min(blen, bmax+1); + amax = Math.max(0, amax-1); + } + + return start; +} + +function findPositionWith(equals, array, start, before, after, patch) { + var blen = before.length; + var b = start-blen; + + var found = false; + var i = b; + + while(i >= 0 && !found) { + found = matchContext(equals, array, i, i+blen+1, before, after); + if(found) { + return i + blen; + } + + --i; + } + + i = start; + while(i < array.length && !found) { + found = matchContext(equals, array, i-blen, i+1, before, after); + if(found) { + return i; + } + + ++i; + } + + return -1; +} + +function matchContext(equals, array, b, a, before, after) { + var i, l; + for(i=0, l=before.length; i= 0. Does not check for decimal numbers * @param {string} s numeric string * @returns {number} number >= 0 */ function parseArrayIndex (s) { - if(!arrayIndexRx.test(s)) { - throw new SyntaxError('invalid array index ' + s); + if(isValidArrayIndex(s)) { + return +s; } - return +s; + + throw new SyntaxError('invalid array index ' + s); } + +function findIndex (findContext, start, array, context) { + var index = start; + + if(index < 0) { + throw new Error('array index out of bounds ' + index); + } + + if(context !== void 0 && typeof findContext === 'function') { + index = findContext(start, array, context); + if(index < 0) { + throw new Error('could not find patch context ' + context); + } + } + + return index; +} \ No newline at end of file diff --git a/lib/lcs.js b/lib/lcs.js index 838c740..2ee1f0a 100644 --- a/lib/lcs.js +++ b/lib/lcs.js @@ -11,8 +11,6 @@ exports.REMOVE = REMOVE = RIGHT = -1; exports.ADD = ADD = DOWN = 1; exports.EQUAL = SKIP = 0; -var skip = { value: 1, type: SKIP }; - /** * Create an lcs comparison matrix describing the differences * between two array-like sequences @@ -52,7 +50,7 @@ function compare(a, b) { * @param {function(result:*, type:number, i:number, j:number)} f * reducer function, where: * - result is the current reduce value, - * - type is the type of change: ADD, REMOVE, or EQUAL + * - type is the type of change: ADD, REMOVE, or SKIP * - i is the index of the change location in b * - j is the index of the change location in a * @param {*} r initial value diff --git a/lib/patches.js b/lib/patches.js index e7e07f9..64dd161 100644 --- a/lib/patches.js +++ b/lib/patches.js @@ -3,6 +3,8 @@ 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'); @@ -53,11 +55,20 @@ exports.copy = { * @throws {TestFailedError} if the test operation fails */ -function applyTest(x, test) { - var pointer = find(x, test.path); - var target = pointer.key === void 0 ? pointer.target : pointer.target[pointer.key]; +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(target, test.value)) { + if(!deepEquals(value, test.value)) { throw new TestFailedError('test failed ' + JSON.stringify(test)); } @@ -92,15 +103,18 @@ function commuteTest(test, b) { * @param {object|array} x * @param {object} change add operation */ -function applyAdd(x, change) { - var pointer = find(x, change.path); +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); } - var val = typeof change.value === 'object' - ? clone(change.value) : change.value; + 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) { @@ -113,19 +127,15 @@ function applyAdd(x, change) { function _add(pointer, value) { var target = pointer.target; - var index; 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 { - index = parseArrayIndex(pointer.key); - if(index < 0 || index > target.length) { - throw new InvalidPatchOperationError('array index out of bounds ' + index); - } - - target.splice(index, 0, value); + target.splice(pointer.key, 0, value); } } else if(isValidObject(target)) { target[pointer.key] = value; @@ -135,17 +145,15 @@ function _add(pointer, value) { } function invertAdd(pr, add) { - pr.push({ - op: 'test', - path: add.path, - value: add.value - }); - - pr.push({ - op: 'remove', - path: add.path - }); - + 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; } @@ -162,15 +170,18 @@ function commuteAddOrCopy(add, b) { * @param {object|array} x * @param {object} change replace operation */ -function applyReplace(x, change) { - var pointer = find(x, change.path); +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); } - var value = typeof change.value === 'object' - ? clone(change.value) : change.value; + 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) { @@ -188,24 +199,22 @@ function applyReplace(x, change) { return x; } -function invertReplace(pr, c, i, context) { - var prev = context[i-1]; +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'); } - pr.push({ - op: 'test', - path: prev.path, - value: c.value - }); - - pr.push({ - op: 'replace', - path: prev.path, - value: prev.value - }); + 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; } @@ -226,8 +235,8 @@ function commuteReplace(replace, b) { * @param {object|array} x * @param {object} change remove operation */ -function applyRemove(x, change) { - var pointer = find(x, change.path); +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) { @@ -256,18 +265,21 @@ function _remove (pointer) { } } -function invertRemove(pr, c, i, context) { - var prev = context[i-1]; +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'); } - pr.push({ - op: 'add', - path: prev.path, - value: prev.value - }); + 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; } @@ -284,25 +296,22 @@ function commuteRemove(remove, b) { * @param {object|array} x * @param {object} change move operation */ -function applyMove(x, change) { +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); - var pfrom = find(x, change.from); + 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, - from: c.path - }); - + pr.push({ op: 'move', + path: c.from, context: c.fromContext, + from: c.path, fromContext: c.context }); return 1; } @@ -319,11 +328,24 @@ function commuteMove(move, b) { * @param {object|array} x * @param {object} change copy operation */ -function applyCopy(x, change) { - var pto = find(x, change.path, true); - var pfrom = find(x, change.from); +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; - _add(pto, clone(pfrom.target[pfrom.key])); + if(Array.isArray(target)) { + value = target[parseArrayIndex(pfrom.key)]; + } else { + value = target[pfrom.key]; + } + + _add(pto, clone(value)); return x; } @@ -334,7 +356,8 @@ function applyCopy(x, change) { // However, that's not correct. It violates the involution: // invert(invert(p)) ~= p. For example: // invert(copy) -> remove -// invert(remove) -> add (DOH! this should be copy!) +// invert(remove) -> add +// thus: invert(invert(copy)) -> add (DOH! this should be copy!) function notInvertible(_, c) { throw new PatchNotInvertibleError('cannot invert ' + c.op); @@ -348,7 +371,6 @@ 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 diff --git a/package.json b/package.json index 4b61d2e..ffeaabf 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,9 @@ { "name": "jiff", - "version": "0.5.5", + "version": "0.7.3", "description": "JSON diff and patch based on rfc6902", "main": "jiff", + "jsdelivr": "./jiff.js", "scripts": { "test": "buster-test -e node" }, @@ -17,9 +18,10 @@ "author": "brian@hovercraftstudios.com", "license": "MIT", "devDependencies": { - "jshint": "~2", "buster": "~0.7", - "gent": "0.6.2" + "gent": "0.6.2", + "jshint": "~2", + "json-patch-test-suite": "^1.0.1" }, "repository": { "type": "git", diff --git a/perf/diff.js b/perf/diff.js new file mode 100644 index 0000000..a41becf --- /dev/null +++ b/perf/diff.js @@ -0,0 +1,51 @@ +var jiff = require('../jiff'); +var json = require('gent/generator/json'); + + +var n = 4; +var o = json.object(8); +var a = makeArray(o, 1, 1000); + +test(a); + +var start = Date.now(); +for(var i=0; i