├── .gitignore ├── .npmignore ├── .editorconfig ├── .travis.yml ├── test ├── buster.js ├── rebase-test.js ├── json-patch-tests-test.js ├── jsonPointerParse-test.js ├── inverse-test.js ├── commute-test.js ├── commutePaths-test.js ├── jiff-test.js └── jsonPatch-test.js ├── lib ├── TestFailedError.js ├── inverse.js ├── PatchNotInvertibleError.js ├── InvalidPatchOperationError.js ├── rebase.js ├── clone.js ├── array.js ├── deepEquals.js ├── jsonPointerParse.js ├── commute.js ├── jsonPatch.js ├── context.js ├── lcs.js ├── jsonPointer.js ├── commutePaths.js └── patches.js ├── .jshintrc ├── bower.json ├── package.json ├── perf ├── jsonPointer-perf.js └── diff.js ├── LICENSE.txt ├── jiff.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /node_modules/ 3 | /experiments/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /test/ 3 | /experiments/ 4 | .* 5 | bower.json -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = LF 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | branches: 5 | only: 6 | - master -------------------------------------------------------------------------------- /test/buster.js: -------------------------------------------------------------------------------- 1 | require('gent/test-adapter/buster'); 2 | 3 | exports.all = { 4 | environment: 'node', 5 | tests: ['*-test.js'] 6 | }; -------------------------------------------------------------------------------- /lib/TestFailedError.js: -------------------------------------------------------------------------------- 1 | module.exports = TestFailedError; 2 | 3 | function TestFailedError(message) { 4 | Error.call(this); 5 | this.name = this.constructor.name; 6 | this.message = message; 7 | if(typeof Error.captureStackTrace === 'function') { 8 | Error.captureStackTrace(this, this.constructor); 9 | } 10 | } 11 | 12 | TestFailedError.prototype = Object.create(Error.prototype); 13 | TestFailedError.prototype.constructor = TestFailedError; -------------------------------------------------------------------------------- /lib/inverse.js: -------------------------------------------------------------------------------- 1 | var patches = require('./patches'); 2 | 3 | module.exports = function inverse(p) { 4 | var pr = []; 5 | var i, skip; 6 | for(i = p.length-1; i>= 0; i -= skip) { 7 | skip = invertOp(pr, p[i], i, p); 8 | } 9 | 10 | return pr; 11 | }; 12 | 13 | function invertOp(patch, c, i, context) { 14 | var op = patches[c.op]; 15 | return op !== void 0 && typeof op.inverse === 'function' 16 | ? op.inverse(patch, c, i, context) 17 | : 1; 18 | } 19 | -------------------------------------------------------------------------------- /lib/PatchNotInvertibleError.js: -------------------------------------------------------------------------------- 1 | module.exports = PatchNotInvertibleError; 2 | 3 | function PatchNotInvertibleError(message) { 4 | Error.call(this); 5 | this.name = this.constructor.name; 6 | this.message = message; 7 | if(typeof Error.captureStackTrace === 'function') { 8 | Error.captureStackTrace(this, this.constructor); 9 | } 10 | } 11 | 12 | PatchNotInvertibleError.prototype = Object.create(Error.prototype); 13 | PatchNotInvertibleError.prototype.constructor = PatchNotInvertibleError; -------------------------------------------------------------------------------- /lib/InvalidPatchOperationError.js: -------------------------------------------------------------------------------- 1 | module.exports = InvalidPatchOperationError; 2 | 3 | function InvalidPatchOperationError(message) { 4 | Error.call(this); 5 | this.name = this.constructor.name; 6 | this.message = message; 7 | if(typeof Error.captureStackTrace === 'function') { 8 | Error.captureStackTrace(this, this.constructor); 9 | } 10 | } 11 | 12 | InvalidPatchOperationError.prototype = Object.create(Error.prototype); 13 | InvalidPatchOperationError.prototype.constructor = InvalidPatchOperationError; -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "node": true, 4 | 5 | "predef": [ 6 | "define", 7 | "module", 8 | "system" 9 | ], 10 | 11 | "boss": true, 12 | "curly": true, 13 | "eqnull": true, 14 | "expr": true, 15 | "globalstrict": false, 16 | "laxbreak": true, 17 | "newcap": true, 18 | "noarg": true, 19 | "noempty": true, 20 | "nonew": true, 21 | "quotmark": "single", 22 | "smarttabs": true, 23 | "strict": false, 24 | "sub": true, 25 | "trailing": true, 26 | "undef": true, 27 | "unused": true, 28 | 29 | "maxdepth": 3, 30 | "maxcomplexity": 5 31 | } -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jiff", 3 | "main": "jiff.js", 4 | "version": "0.7.2", 5 | "authors": [ 6 | "Brian Cavalier " 7 | ], 8 | "description": "JSON diff and patch based on rfc6902", 9 | "moduleType": [ 10 | "node" 11 | ], 12 | "keywords": [ 13 | "json", 14 | "json patch", 15 | "rfc6902", 16 | "diff", 17 | "patch", 18 | "json pointer" 19 | ], 20 | "license": "MIT", 21 | "homepage": "http://cujojs.com", 22 | "ignore": [ 23 | "**/.*", 24 | "node_modules", 25 | "bower_components", 26 | "test", 27 | "experiments" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /lib/rebase.js: -------------------------------------------------------------------------------- 1 | var commuteRtL = require('./commute').rtl; 2 | var inverse = require('./inverse'); 3 | 4 | /** 5 | * Given a patch history (array of patches) and a single patch, rooted 6 | * at the same starting document context d1, rebase patch onto history 7 | * so that d1 + history -> d2, d2 + patch -> d3 8 | * @param {array} history array of JSON Patch 9 | * @param {array} patch JSON Patch 10 | * @returns {array} rebased patch which can be applied after history 11 | */ 12 | module.exports = function rebase(history, patch) { 13 | return history.reduce(function(commuted, patchFromHistory) { 14 | return commuteRtL(inverse(patchFromHistory), commuted); 15 | }, patch); 16 | }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jiff", 3 | "version": "0.7.3", 4 | "description": "JSON diff and patch based on rfc6902", 5 | "main": "jiff", 6 | "jsdelivr": "./jiff.js", 7 | "scripts": { 8 | "test": "buster-test -e node" 9 | }, 10 | "keywords": [ 11 | "json", 12 | "json patch", 13 | "rfc6902", 14 | "diff", 15 | "patch", 16 | "json pointer" 17 | ], 18 | "author": "brian@hovercraftstudios.com", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "buster": "~0.7", 22 | "gent": "0.6.2", 23 | "jshint": "~2", 24 | "json-patch-test-suite": "^1.0.1" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git://github.com/cujojs/jiff.git" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/rebase-test.js: -------------------------------------------------------------------------------- 1 | var buster = require('buster'); 2 | var assert = buster.referee.assert; 3 | 4 | var rebase = require('../lib/rebase'); 5 | var jiff = require('../jiff'); 6 | var deepEquals = require('../lib/deepEquals'); 7 | 8 | buster.testCase('rebase', { 9 | 'should allow parallel patches': function() { 10 | var d1 = [1,2,3,4,5]; 11 | var d2a = [1,2,4,5]; 12 | var d2b = [1,2,3,6,4,5]; 13 | var d3 = [1,2,6,4,5]; 14 | 15 | // Two parallel patches created from d1 16 | var d1pd2a = jiff.diff(d1, d2a); 17 | var d1pd2b = jiff.diff(d1, d2b); 18 | 19 | // Rebase d1pd2b onto d1pd2a 20 | var d2apd2b = rebase([d1pd2a], d1pd2b); 21 | 22 | var d3a = jiff.patch(d2apd2b, jiff.patch(d1pd2a, d1)); 23 | assert(deepEquals(d3, d3a)); 24 | } 25 | 26 | }); -------------------------------------------------------------------------------- /perf/jsonPointer-perf.js: -------------------------------------------------------------------------------- 1 | /** @license MIT License (c) copyright 2010-2014 original author or authors */ 2 | /** @author Brian Cavalier */ 3 | /** @author John Hann */ 4 | 5 | var jsonPointer = require('../lib/jsonPointer'); 6 | var json = require('gent/generator/json'); 7 | 8 | var docDepth = 20; 9 | var numTests = 10000; 10 | var tests = []; 11 | 12 | var i, start, b; 13 | var o; 14 | var string = json.key(); 15 | 16 | function addKey (n, o) { 17 | if (n === 0) { 18 | return ''; 19 | } 20 | 21 | var child = {}; 22 | var k = string.next().value; 23 | o[k] = child; 24 | return '/' + k + addKey(n - 1, child); 25 | } 26 | 27 | for (i = 0; i < numTests; ++i) { 28 | o = {}; 29 | tests.push({ 30 | path: addKey(docDepth, o), 31 | data: o 32 | }); 33 | } 34 | 35 | // warm up JIT 36 | for (i = 0; i < numTests; ++i) { 37 | b = jsonPointer.find(tests[i].data, tests[i].path); 38 | } 39 | 40 | start = Date.now(); 41 | 42 | // run tests 43 | for (i = 0; i < numTests; ++i) { 44 | b = jsonPointer.find(tests[i].data, tests[i].path); 45 | } 46 | 47 | console.log('find', Date.now() - start); 48 | -------------------------------------------------------------------------------- /perf/diff.js: -------------------------------------------------------------------------------- 1 | var jiff = require('../jiff'); 2 | var json = require('gent/generator/json'); 3 | 4 | 5 | var n = 4; 6 | var o = json.object(8); 7 | var a = makeArray(o, 1, 1000); 8 | 9 | test(a); 10 | 11 | var start = Date.now(); 12 | for(var i=0; i} pair [commutedRight, commutedLeft] 13 | */ 14 | function commute(p1, p2) { 15 | return p1.reduceRight(function(a, p1) { 16 | var commuted = commuteOneFully(p1, a[1]); 17 | a[0].unshift(commuted[0]); 18 | var b = [a[0], commuted[1]]; 19 | return b; 20 | }, [[], p2]); 21 | } 22 | 23 | function commuteOneFully(op, p) { 24 | return p.reduce(function(a, p) { 25 | var commuted = commuteOne(a[0], p); 26 | a[0] = commuted[1]; 27 | a[1].push(commuted[0]); 28 | return a; 29 | }, [op, []]) 30 | } 31 | /** 32 | * Commute left and right and return only the newly commuted left, throwing 33 | * away the newly commuted right. 34 | * @param p1 35 | * @param p2 36 | * @returns {*} 37 | */ 38 | function commuteRtL(p1, p2) { 39 | return p2.reduce(function(accum, p2) { 40 | accum.push(p1.reduceRight(function(p2c, p1) { 41 | return commuteOne(p1, p2c)[0]; 42 | }, p2)); 43 | return accum; 44 | }, []); 45 | } 46 | 47 | function commuteOne (p1, p2) { 48 | var patch = patches[p2.op]; 49 | if (patch === void 0 || typeof patch.commute !== 'function') { 50 | throw new TypeError('patches cannot be commuted'); 51 | } 52 | 53 | return patch.commute(p1, p2); 54 | } 55 | -------------------------------------------------------------------------------- /test/jsonPointerParse-test.js: -------------------------------------------------------------------------------- 1 | var buster = require('buster'); 2 | var assert = buster.referee.assert; 3 | 4 | var parse = require('../lib/jsonPointerParse'); 5 | 6 | buster.testCase('jsonPointerParse', { 7 | 8 | 'should call onSegment callback once if passed a blank string': function () { 9 | var onSegment = this.spy(); 10 | parse('', onSegment); 11 | assert.calledOnceWith(onSegment, ''); 12 | }, 13 | 'should call onSegment callback once if passed a single slash': function () { 14 | var onSegment = this.spy(); 15 | parse('/', onSegment); 16 | assert.calledOnceWith(onSegment, ''); 17 | }, 18 | 'should call onSegment once a single-segment path': function () { 19 | var onSegment = this.spy(); 20 | parse('/foo', onSegment); 21 | assert.calledOnceWith(onSegment, 'foo'); 22 | }, 23 | 'should call onSegment once a single-segment path with encoding': function () { 24 | var onSegment; 25 | onSegment = this.spy(); 26 | parse('/m~0n', onSegment); 27 | assert.calledOnceWith(onSegment, 'm~n'); 28 | onSegment = this.spy(); 29 | parse('/a~1b', onSegment); 30 | assert.calledOnceWith(onSegment, 'a/b'); 31 | }, 32 | 'should call onSegment for each segment in a multi-segment path': function () { 33 | var onSegment = this.spy(); 34 | parse('/foo/bar/0', onSegment); 35 | assert.calledThrice(onSegment); 36 | assert.calledWithExactly(onSegment, 'foo'); 37 | assert.calledWithExactly(onSegment, 'bar'); 38 | assert.calledWithExactly(onSegment, '0'); 39 | }, 40 | 'should call onSegment for each segment in a multi-segment path with encoding': function () { 41 | var onSegment = this.spy(); 42 | parse('/m~0n/a~1b/~01', onSegment); 43 | assert.calledThrice(onSegment); 44 | assert.calledWithExactly(onSegment, 'm~n'); 45 | assert.calledWithExactly(onSegment, 'a/b'); 46 | assert.calledWithExactly(onSegment, '~1'); 47 | }, 48 | 'should bail early if onSegment returns false': function () { 49 | var onSegment = this.spy(function () { return false; }); 50 | parse('/foo/bar/0', onSegment); 51 | assert.calledOnce(onSegment); 52 | } 53 | 54 | }); 55 | -------------------------------------------------------------------------------- /test/inverse-test.js: -------------------------------------------------------------------------------- 1 | var buster = require('buster'); 2 | var json = require('gent/generator/json'); 3 | var assert = buster.referee.assert; 4 | var deepEquals = require('../lib/deepEquals'); 5 | 6 | var inverse = require('../lib/inverse'); 7 | var jiff = require('../jiff'); 8 | 9 | buster.testCase('inverse', { 10 | 'for objects': { 11 | 'should have inverse effect': function() { 12 | assert.claim(function(a, b) { 13 | var p = jiff.diff(a, b); 14 | var a2 = jiff.patch(inverse(p), b); 15 | return deepEquals(a, a2); 16 | }, json.object(), json.object()); 17 | }, 18 | 19 | 'should be an involution': function() { 20 | assert.claim(function(a, b) { 21 | var p = jiff.diff(a, b); 22 | var b2 = jiff.patch(inverse(inverse(p)), a); 23 | return deepEquals(b, b2); 24 | }, json.object(), json.object()); 25 | } 26 | }, 27 | 28 | 'for arrays': { 29 | 'should have inverse effect': function() { 30 | assert.claim(function(a, b) { 31 | var p = jiff.diff(a, b); 32 | var a2 = jiff.patch(inverse(p), b); 33 | return deepEquals(a, a2); 34 | }, json.array(), json.array()); 35 | }, 36 | 37 | 'should be an involution': function() { 38 | assert.claim(function(a, b) { 39 | var p = jiff.diff(a, b); 40 | var b2 = jiff.patch(inverse(inverse(p)), a); 41 | return deepEquals(b, b2); 42 | }, json.array(), json.array()); 43 | } 44 | }, 45 | 46 | 'should fail when patch is not invertible': { 47 | 'because it contains a remove not preceded by test': function() { 48 | assert.exception(function() { 49 | inverse([{ op: 'remove', path: '/a' }]); 50 | }, 'PatchNotInvertibleError'); 51 | }, 52 | 53 | 'because it contains a replace not preceded by test': function() { 54 | assert.exception(function() { 55 | inverse([{ op: 'replace', path: '/a', value: 'b' }]); 56 | }, 'PatchNotInvertibleError'); 57 | }, 58 | 59 | 'because it contains a copy operation': function() { 60 | assert.exception(function() { 61 | inverse([{ op: 'copy', path: '/a', from: '/b' }]); 62 | }, 'PatchNotInvertibleError'); 63 | } 64 | } 65 | }); -------------------------------------------------------------------------------- /lib/jsonPatch.js: -------------------------------------------------------------------------------- 1 | /** @license MIT License (c) copyright 2010-2014 original author or authors */ 2 | /** @author Brian Cavalier */ 3 | /** @author John Hann */ 4 | 5 | var patches = require('./patches'); 6 | var clone = require('./clone'); 7 | var InvalidPatchOperationError = require('./InvalidPatchOperationError'); 8 | 9 | exports.apply = patch; 10 | exports.applyInPlace = patchInPlace; 11 | exports.clone = clone; 12 | exports.isValidObject = isValidObject; 13 | exports.defaultHash = defaultHash; 14 | 15 | var defaultOptions = {}; 16 | 17 | /** 18 | * Apply the supplied JSON Patch to x 19 | * @param {array} changes JSON Patch 20 | * @param {object|array|string|number} x object/array/value to patch 21 | * @param {object} options 22 | * @param {function(index:Number, array:Array, context:object):Number} options.findContext 23 | * function used adjust array indexes for smarty/fuzzy patching, for 24 | * patches containing context 25 | * @returns {object|array|string|number} patched version of x. If x is 26 | * an array or object, it will be mutated and returned. Otherwise, if 27 | * x is a value, the new value will be returned. 28 | */ 29 | function patch(changes, x, options) { 30 | return patchInPlace(changes, clone(x), options); 31 | } 32 | 33 | function patchInPlace(changes, x, options) { 34 | if(!options) { 35 | options = defaultOptions; 36 | } 37 | 38 | // TODO: Consider throwing if changes is not an array 39 | if(!Array.isArray(changes)) { 40 | return x; 41 | } 42 | 43 | var patch, p; 44 | for(var i=0; i 0 || bmax < blen) { 48 | index = findPositionWith(equals, array, start, 49 | before.slice(bmax), 50 | after.slice(0, amax)); 51 | 52 | if(index >= 0) { 53 | return index; 54 | } 55 | 56 | bmax = Math.min(blen, bmax+1); 57 | amax = Math.max(0, amax-1); 58 | } 59 | 60 | return start; 61 | } 62 | 63 | function findPositionWith(equals, array, start, before, after, patch) { 64 | var blen = before.length; 65 | var b = start-blen; 66 | 67 | var found = false; 68 | var i = b; 69 | 70 | while(i >= 0 && !found) { 71 | found = matchContext(equals, array, i, i+blen+1, before, after); 72 | if(found) { 73 | return i + blen; 74 | } 75 | 76 | --i; 77 | } 78 | 79 | i = start; 80 | while(i < array.length && !found) { 81 | found = matchContext(equals, array, i-blen, i+1, before, after); 82 | if(found) { 83 | return i; 84 | } 85 | 86 | ++i; 87 | } 88 | 89 | return -1; 90 | } 91 | 92 | function matchContext(equals, array, b, a, before, after) { 93 | var i, l; 94 | for(i=0, l=before.length; i remove,add': { 10 | 'paths same length, add lower index': function() { 11 | var l = { op: 'add', path: '/foo/0' }; 12 | var r = { op: 'remove', path: '/foo/1' }; 13 | 14 | var rl = commutePaths(l, r); 15 | assert.equals(rl[0].path, '/foo/0'); 16 | assert.equals(rl[1].path, '/foo/0'); 17 | }, 18 | 19 | 'paths same length, add higher index': function() { 20 | var l = { op: 'add', path: '/foo/1' }; 21 | var r = { op: 'remove', path: '/foo/0' }; 22 | 23 | var rl = commutePaths(l, r); 24 | assert.equals(rl[0].path, '/foo/0'); 25 | assert.equals(rl[1].path, '/foo/0'); 26 | }, 27 | 28 | 'add longer path, add lower index': function() { 29 | var l = { op: 'add', path: '/foo/0/x' }; 30 | var r = { op: 'remove', path: '/foo/1' }; 31 | 32 | var rl = commutePaths(l, r); 33 | assert.equals(rl[0].path, '/foo/1'); 34 | assert.equals(rl[1].path, '/foo/0/x'); 35 | }, 36 | 37 | 'add longer path, add higher index': function() { 38 | var l = { op: 'add', path: '/foo/1/x' }; 39 | var r = { op: 'remove', path: '/foo/0' }; 40 | 41 | var rl = commutePaths(l, r); 42 | assert.equals(rl[0].path, '/foo/0'); 43 | assert.equals(rl[1].path, '/foo/0/x'); 44 | }, 45 | 46 | 'identical path': function() { 47 | var l = { op: 'add', path: '/foo/1' }; 48 | var r = { op: 'remove', path: '/foo/1' }; 49 | 50 | var rl = commutePaths(l, r); 51 | assert.equals(rl[0].path, '/foo/1'); 52 | assert.equals(rl[1].path, '/foo/1'); 53 | } 54 | }, 55 | 56 | 'remove,add -> add,remove': { 57 | 'paths same length, remove lower index': function() { 58 | var l = { op: 'remove', path: '/foo/0' }; 59 | var r = { op: 'add', path: '/foo/1' }; 60 | 61 | var rl = commutePaths(l, r); 62 | assert.equals(rl[0].path, '/foo/2'); 63 | assert.equals(rl[1].path, '/foo/0'); 64 | }, 65 | 66 | 'paths same length, remove higher index': function() { 67 | var l = { op: 'remove', path: '/foo/1' }; 68 | var r = { op: 'add', path: '/foo/0' }; 69 | 70 | var rl = commutePaths(l, r); 71 | assert.equals(rl[0].path, '/foo/0'); 72 | assert.equals(rl[1].path, '/foo/2'); 73 | }, 74 | 75 | 'remove longer path, remove lower index': function() { 76 | var l = { op: 'remove', path: '/foo/0/x' }; 77 | var r = { op: 'add', path: '/foo/1', value: 1 }; 78 | 79 | var rl = commutePaths(l, r); 80 | assert.equals(rl[0].path, '/foo/1'); 81 | assert.equals(rl[1].path, '/foo/0/x'); 82 | }, 83 | 84 | 'remove longer path, remove higher index': function() { 85 | var l = { op: 'remove', path: '/foo/1/x' }; 86 | var r = { op: 'add', path: '/foo/0', value: 1 }; 87 | 88 | var rl = commutePaths(l, r); 89 | assert.equals(rl[0].path, '/foo/0'); 90 | assert.equals(rl[1].path, '/foo/2/x'); 91 | } 92 | }, 93 | 94 | 'add,add, both 0 index': function() { 95 | var l = { op: 'add', path: '/foo/0', value: 1 }; 96 | var r = { op: 'add', path: '/foo/0', value: 0 }; 97 | 98 | var rl = commutePaths(l, r); 99 | assert.equals(rl[0].path, '/foo/0'); 100 | assert.equals(rl[0].value, 0); 101 | assert.equals(rl[1].path, '/foo/1'); 102 | assert.equals(rl[1].value, 1); 103 | } 104 | 105 | }); -------------------------------------------------------------------------------- /lib/lcs.js: -------------------------------------------------------------------------------- 1 | /** @license MIT License (c) copyright 2010-2014 original author or authors */ 2 | /** @author Brian Cavalier */ 3 | /** @author John Hann */ 4 | 5 | exports.compare = compare; 6 | exports.reduce = reduce; 7 | 8 | var REMOVE, RIGHT, ADD, DOWN, SKIP; 9 | 10 | exports.REMOVE = REMOVE = RIGHT = -1; 11 | exports.ADD = ADD = DOWN = 1; 12 | exports.EQUAL = SKIP = 0; 13 | 14 | /** 15 | * Create an lcs comparison matrix describing the differences 16 | * between two array-like sequences 17 | * @param {array} a array-like 18 | * @param {array} b array-like 19 | * @returns {object} lcs descriptor, suitable for passing to reduce() 20 | */ 21 | function compare(a, b) { 22 | var cols = a.length; 23 | var rows = b.length; 24 | 25 | var prefix = findPrefix(a, b); 26 | var suffix = prefix < cols && prefix < rows 27 | ? findSuffix(a, b, prefix) 28 | : 0; 29 | 30 | var remove = suffix + prefix - 1; 31 | cols -= remove; 32 | rows -= remove; 33 | var matrix = createMatrix(cols, rows); 34 | 35 | for (var j = cols - 1; j >= 0; --j) { 36 | for (var i = rows - 1; i >= 0; --i) { 37 | matrix[i][j] = backtrack(matrix, a, b, prefix, j, i); 38 | } 39 | } 40 | 41 | return { 42 | prefix: prefix, 43 | matrix: matrix, 44 | suffix: suffix 45 | }; 46 | } 47 | 48 | /** 49 | * Reduce a set of lcs changes previously created using compare 50 | * @param {function(result:*, type:number, i:number, j:number)} f 51 | * reducer function, where: 52 | * - result is the current reduce value, 53 | * - type is the type of change: ADD, REMOVE, or SKIP 54 | * - i is the index of the change location in b 55 | * - j is the index of the change location in a 56 | * @param {*} r initial value 57 | * @param {object} lcs results returned by compare() 58 | * @returns {*} the final reduced value 59 | */ 60 | function reduce(f, r, lcs) { 61 | var i, j, k, op; 62 | 63 | var m = lcs.matrix; 64 | 65 | // Reduce shared prefix 66 | var l = lcs.prefix; 67 | for(i = 0;i < l; ++i) { 68 | r = f(r, SKIP, i, i); 69 | } 70 | 71 | // Reduce longest change span 72 | k = i; 73 | l = m.length; 74 | i = 0; 75 | j = 0; 76 | while(i < l) { 77 | op = m[i][j].type; 78 | r = f(r, op, i+k, j+k); 79 | 80 | switch(op) { 81 | case SKIP: ++i; ++j; break; 82 | case RIGHT: ++j; break; 83 | case DOWN: ++i; break; 84 | } 85 | } 86 | 87 | // Reduce shared suffix 88 | i += k; 89 | j += k; 90 | l = lcs.suffix; 91 | for(k = 0;k < l; ++k) { 92 | r = f(r, SKIP, i+k, j+k); 93 | } 94 | 95 | return r; 96 | } 97 | 98 | function findPrefix(a, b) { 99 | var i = 0; 100 | var l = Math.min(a.length, b.length); 101 | while(i < l && a[i] === b[i]) { 102 | ++i; 103 | } 104 | return i; 105 | } 106 | 107 | function findSuffix(a, b) { 108 | var al = a.length - 1; 109 | var bl = b.length - 1; 110 | var l = Math.min(al, bl); 111 | var i = 0; 112 | while(i < l && a[al-i] === b[bl-i]) { 113 | ++i; 114 | } 115 | return i; 116 | } 117 | 118 | function backtrack(matrix, a, b, start, j, i) { 119 | if (a[j+start] === b[i+start]) { 120 | return { value: matrix[i + 1][j + 1].value, type: SKIP }; 121 | } 122 | if (matrix[i][j + 1].value < matrix[i + 1][j].value) { 123 | return { value: matrix[i][j + 1].value + 1, type: RIGHT }; 124 | } 125 | 126 | return { value: matrix[i + 1][j].value + 1, type: DOWN }; 127 | } 128 | 129 | function createMatrix (cols, rows) { 130 | var m = [], i, j, lastrow; 131 | 132 | // Fill the last row 133 | lastrow = m[rows] = []; 134 | for (j = 0; j= 0. Does not check for decimal numbers 136 | * @param {string} s numeric string 137 | * @returns {number} number >= 0 138 | */ 139 | function parseArrayIndex (s) { 140 | if(isValidArrayIndex(s)) { 141 | return +s; 142 | } 143 | 144 | throw new SyntaxError('invalid array index ' + s); 145 | } 146 | 147 | function findIndex (findContext, start, array, context) { 148 | var index = start; 149 | 150 | if(index < 0) { 151 | throw new Error('array index out of bounds ' + index); 152 | } 153 | 154 | if(context !== void 0 && typeof findContext === 'function') { 155 | index = findContext(start, array, context); 156 | if(index < 0) { 157 | throw new Error('could not find patch context ' + context); 158 | } 159 | } 160 | 161 | return index; 162 | } -------------------------------------------------------------------------------- /test/jiff-test.js: -------------------------------------------------------------------------------- 1 | var buster = require('buster'); 2 | require('gent/test-adapter/buster'); 3 | var assert = buster.referee.assert; 4 | var refute = buster.referee.refute; 5 | var gent = require('gent'); 6 | var json = require('gent/generator/json'); 7 | var deepEquals = require('../lib/deepEquals'); 8 | 9 | var jiff = require('../jiff'); 10 | 11 | buster.testCase('jiff', { 12 | 'diff and patch should be inverses': { 13 | 'for objects': function() { 14 | assert.claim(deepEqualAfterDiffPatch(), json.object(), json.object()); 15 | }, 16 | 17 | 'for arrays': function() { 18 | assert.claim(deepEqualAfterDiffPatch(), 19 | json.array(10, gent.string(gent.integer(2, 64))), 20 | json.array(10, gent.string(gent.integer(2, 64)))); 21 | }, 22 | 23 | 'for arrays of objects': function() { 24 | assert(deepEqualAfterDiffPatch()( 25 | [{name:'a'},{name:'b'},{name:'c'}], 26 | [{name:'b'}] 27 | )); 28 | 29 | assert(deepEqualAfterDiffPatch()( 30 | [{name:'a'}], 31 | [{name:'b'}] 32 | )); 33 | 34 | assert(deepEqualAfterDiffPatch()( 35 | [{name:'a'},{name:'b'},{name:'c'}], 36 | [{name:'d'}] 37 | )); 38 | 39 | assert(deepEqualAfterDiffPatch()( 40 | [{name:'b'}], 41 | [{name:'a'},{name:'b'},{name:'c'}] 42 | )); 43 | 44 | assert(deepEqualAfterDiffPatch()( 45 | [{name:'d'}], 46 | [{name:'a'},{name:'b'},{name:'c'}] 47 | )); 48 | 49 | assert.claim(deepEqualAfterDiffPatch(), 50 | json.array(1, json.object()), 51 | json.array(1, json.object()) 52 | ); 53 | }, 54 | 55 | 'for arrays of arrays': function() { 56 | assert(deepEqualAfterDiffPatch()( 57 | [['a'],['b'],['c']], 58 | [['b']] 59 | )); 60 | 61 | assert(deepEqualAfterDiffPatch()( 62 | [['a']], 63 | [['b']] 64 | )); 65 | 66 | assert(deepEqualAfterDiffPatch()( 67 | [['a'],['b'],['c']], 68 | [['d']] 69 | )); 70 | 71 | assert(deepEqualAfterDiffPatch()( 72 | [['b']], 73 | [['a'],['b'],['c']] 74 | )); 75 | 76 | assert(deepEqualAfterDiffPatch()( 77 | [['d']], 78 | [['a'],['b'],['c']] 79 | )); 80 | 81 | assert.claim(deepEqualAfterDiffPatch(), 82 | json.array(1, json.array()), 83 | json.array(1, json.array()) 84 | ); 85 | } 86 | }, 87 | 88 | 'diff': { 89 | 'on arrays': { 90 | 'should generate - or length for path suffix when appending': function() { 91 | var patch = jiff.diff([], [1]); 92 | assert.equals(patch[0].op, 'add'); 93 | assert(patch[0].path === '/-' || patch[0].path === '/0'); 94 | assert.same(patch[0].value, 1); 95 | }, 96 | 'with default hash function': { 97 | 'primitives as elements': { 98 | 'should generate an empty patch when elements are equal': function() { 99 | var patch = jiff.diff([1,'a',true], [1,'a',true]); 100 | assert.equals(patch.length, 0); 101 | } 102 | }, 103 | 'arrays as elements': { 104 | 'should generate an empty patch when elements are equal': function() { 105 | var patch = jiff.diff([['a'],['b'],['c']], [['a'],['b'],['c']]); 106 | assert.equals(patch.length, 0); 107 | } 108 | }, 109 | 'objects as elements': { 110 | 'object keys with consistent order': { 111 | 'should generate an empty patch when elements are equal': function() { 112 | var patch = jiff.diff([{a:'a',b:'b',c:'c'}], [{a:'a',b:'b',c:'c'}]); 113 | assert.equals(patch.length, 0); 114 | } 115 | }, 116 | 'object keys with inconsistent order': { 117 | 'should generate a non empty patch even though elements are equal': function() { 118 | var patch = jiff.diff([{b:'b',c:'c',a:'a'}], [{a:'a',b:'b',c:'c'}]); 119 | refute.equals(patch.length, 0); 120 | } 121 | } 122 | } 123 | } 124 | }, 125 | 126 | 'invertible': { 127 | 'when false': { 128 | 'should not generate extra test ops for array remove': function() { 129 | var patch = jiff.diff([1,2,3], [1,3], { invertible: false }); 130 | assert.equals(patch.length, 1); 131 | assert.equals(patch[0].op, 'remove'); 132 | }, 133 | 134 | 'should not generate extra test ops for object remove': function() { 135 | var patch = jiff.diff({ foo: 1 }, {}, { invertible: false }); 136 | assert.equals(patch.length, 1); 137 | assert.equals(patch[0].op, 'remove'); 138 | }, 139 | 140 | 'should not generate extra test ops for replace': function() { 141 | var patch = jiff.diff({ foo: 1 }, { foo: 2 }, { invertible: false }); 142 | assert.equals(patch.length, 1); 143 | assert.equals(patch[0].op, 'replace'); 144 | } 145 | } 146 | }, 147 | 148 | 'on mixed types': { 149 | 'should generate replace': function() { 150 | var a = { 'test': ['x'] }; 151 | var b = { 'test': { 'a': "x" } }; 152 | 153 | var patch = jiff.diff(a, b, { invertible: false }); 154 | assert.equals(patch.length, 1); 155 | assert.equals(patch[0].op, 'replace'); 156 | assert.equals(b, jiff.patch(patch, a)); 157 | } 158 | } 159 | } 160 | }); 161 | 162 | function deepEqualAfterDiffPatch(hasher) { 163 | return function(a, b) { 164 | var p = jiff.diff(a, b, hasher); 165 | var b2 = jiff.patch(p, a); 166 | return deepEquals(b, b2); 167 | }; 168 | } 169 | -------------------------------------------------------------------------------- /lib/commutePaths.js: -------------------------------------------------------------------------------- 1 | var jsonPointer = require('./jsonPointer'); 2 | 3 | /** 4 | * commute the patch sequence a,b to b,a 5 | * @param {object} a patch operation 6 | * @param {object} b patch operation 7 | */ 8 | module.exports = function commutePaths(a, b) { 9 | // TODO: cases for special paths: '' and '/' 10 | var left = jsonPointer.parse(a.path); 11 | var right = jsonPointer.parse(b.path); 12 | var prefix = getCommonPathPrefix(left, right); 13 | var isArray = isArrayPath(left, right, prefix.length); 14 | 15 | // Never mutate the originals 16 | var ac = copyPatch(a); 17 | var bc = copyPatch(b); 18 | 19 | if(prefix.length === 0 && !isArray) { 20 | // Paths share no common ancestor, simple swap 21 | return [bc, ac]; 22 | } 23 | 24 | if(isArray) { 25 | return commuteArrayPaths(ac, left, bc, right); 26 | } else { 27 | return commuteTreePaths(ac, left, bc, right); 28 | } 29 | }; 30 | 31 | function commuteTreePaths(a, left, b, right) { 32 | if(a.path === b.path) { 33 | throw new TypeError('cannot commute ' + a.op + ',' + b.op + ' with identical object paths'); 34 | } 35 | // FIXME: Implement tree path commutation 36 | return [b, a]; 37 | } 38 | 39 | /** 40 | * Commute two patches whose common ancestor (which may be the immediate parent) 41 | * is an array 42 | * @param a 43 | * @param left 44 | * @param b 45 | * @param right 46 | * @returns {*} 47 | */ 48 | function commuteArrayPaths(a, left, b, right) { 49 | if(left.length === right.length) { 50 | return commuteArraySiblings(a, left, b, right); 51 | } 52 | 53 | if (left.length > right.length) { 54 | // left is longer, commute by "moving" it to the right 55 | left = commuteArrayAncestor(b, right, a, left, -1); 56 | a.path = jsonPointer.absolute(jsonPointer.join(left)); 57 | } else { 58 | // right is longer, commute by "moving" it to the left 59 | right = commuteArrayAncestor(a, left, b, right, 1); 60 | b.path = jsonPointer.absolute(jsonPointer.join(right)); 61 | } 62 | 63 | return [b, a]; 64 | } 65 | 66 | function isArrayPath(left, right, index) { 67 | return jsonPointer.isValidArrayIndex(left[index]) 68 | && jsonPointer.isValidArrayIndex(right[index]); 69 | } 70 | 71 | /** 72 | * Commute two patches referring to items in the same array 73 | * @param l 74 | * @param lpath 75 | * @param r 76 | * @param rpath 77 | * @returns {*[]} 78 | */ 79 | function commuteArraySiblings(l, lpath, r, rpath) { 80 | 81 | var target = lpath.length-1; 82 | var lindex = +lpath[target]; 83 | var rindex = +rpath[target]; 84 | 85 | var commuted; 86 | 87 | if(lindex < rindex) { 88 | // Adjust right path 89 | if(l.op === 'add' || l.op === 'copy') { 90 | commuted = rpath.slice(); 91 | commuted[target] = Math.max(0, rindex - 1); 92 | r.path = jsonPointer.absolute(jsonPointer.join(commuted)); 93 | } else if(l.op === 'remove') { 94 | commuted = rpath.slice(); 95 | commuted[target] = rindex + 1; 96 | r.path = jsonPointer.absolute(jsonPointer.join(commuted)); 97 | } 98 | } else if(r.op === 'add' || r.op === 'copy') { 99 | // Adjust left path 100 | commuted = lpath.slice(); 101 | commuted[target] = lindex + 1; 102 | l.path = jsonPointer.absolute(jsonPointer.join(commuted)); 103 | } else if (lindex > rindex && r.op === 'remove') { 104 | // Adjust left path only if remove was at a (strictly) lower index 105 | commuted = lpath.slice(); 106 | commuted[target] = Math.max(0, lindex - 1); 107 | l.path = jsonPointer.absolute(jsonPointer.join(commuted)); 108 | } 109 | 110 | return [r, l]; 111 | } 112 | 113 | /** 114 | * Commute two patches with a common array ancestor 115 | * @param l 116 | * @param lpath 117 | * @param r 118 | * @param rpath 119 | * @param direction 120 | * @returns {*} 121 | */ 122 | function commuteArrayAncestor(l, lpath, r, rpath, direction) { 123 | // rpath is longer or same length 124 | 125 | var target = lpath.length-1; 126 | var lindex = +lpath[target]; 127 | var rindex = +rpath[target]; 128 | 129 | // Copy rpath, then adjust its array index 130 | var rc = rpath.slice(); 131 | 132 | if(lindex > rindex) { 133 | return rc; 134 | } 135 | 136 | if(l.op === 'add' || l.op === 'copy') { 137 | rc[target] = Math.max(0, rindex - direction); 138 | } else if(l.op === 'remove') { 139 | rc[target] = Math.max(0, rindex + direction); 140 | } 141 | 142 | return rc; 143 | } 144 | 145 | function getCommonPathPrefix(p1, p2) { 146 | var p1l = p1.length; 147 | var p2l = p2.length; 148 | if(p1l === 0 || p2l === 0 || (p1l < 2 && p2l < 2)) { 149 | return []; 150 | } 151 | 152 | // If paths are same length, the last segment cannot be part 153 | // of a common prefix. If not the same length, the prefix cannot 154 | // be longer than the shorter path. 155 | var l = p1l === p2l 156 | ? p1l - 1 157 | : Math.min(p1l, p2l); 158 | 159 | var i = 0; 160 | while(i < l && p1[i] === p2[i]) { 161 | ++i 162 | } 163 | 164 | return p1.slice(0, i); 165 | } 166 | 167 | function copyPatch(p) { 168 | if(p.op === 'remove') { 169 | return { op: p.op, path: p.path }; 170 | } 171 | 172 | if(p.op === 'copy' || p.op === 'move') { 173 | return { op: p.op, path: p.path, from: p.from }; 174 | } 175 | 176 | // test, add, replace 177 | return { op: p.op, path: p.path, value: p.value }; 178 | } -------------------------------------------------------------------------------- /test/jsonPatch-test.js: -------------------------------------------------------------------------------- 1 | var buster = require('buster'); 2 | var assert = buster.referee.assert; 3 | var refute = buster.referee.refute; 4 | 5 | var patches = require('../lib/patches'); 6 | var jsonPatch = require('../lib/jsonPatch'); 7 | var InvalidPatchOperationError = require('../lib/InvalidPatchOperationError'); 8 | var TestFailedError = require('../lib/TestFailedError'); 9 | 10 | buster.testCase('jsonPatch', { 11 | 'add': { 12 | 'should add': function() { 13 | var a = {}; 14 | var result = jsonPatch.apply([{ op: 'add', path: '/value', value: 1 }], a); 15 | assert.equals(result.value, 1); 16 | }, 17 | 18 | 'should replace': function() { 19 | var a = { value: 0 }; 20 | var result = jsonPatch.apply([{ op: 'add', path: '/value', value: 1 }], a); 21 | assert.equals(result.value, 1); 22 | }, 23 | 24 | 'should replace String': function() { 25 | var a = { value: 0 }; 26 | var result = jsonPatch.apply([{ op: 'add', path: '/value', value: new String("value") }], a); 27 | assert.equals(result.value, "value"); 28 | }, 29 | 30 | 'should replace Number': function() { 31 | var a = { value: 0 }; 32 | var result = jsonPatch.apply([{ op: 'add', path: '/value', value: new Number(1) }], a); 33 | assert.equals(result.value, 1); 34 | }, 35 | 36 | 'should replace Boolean': function() { 37 | var a = { value: 0 }; 38 | var result = jsonPatch.apply([{ op: 'add', path: '/value', value: new Boolean(true) }], a); 39 | assert.equals(result.value, true); 40 | }, 41 | 42 | 'should throw': { 43 | 'when path is invalid': function() { 44 | assert.exception(function() { 45 | jsonPatch.apply([{ op: 'add', path: '/a/b', value: 1 }], {}); 46 | }, 'InvalidPatchOperationError'); 47 | }, 48 | 49 | 'when target is null': function() { 50 | assert.exception(function() { 51 | jsonPatch.apply([{ op: 'add', path: '/a', value: 1 }], null); 52 | }, 'InvalidPatchOperationError'); 53 | }, 54 | 55 | 'when target is undefined': function() { 56 | assert.exception(function() { 57 | jsonPatch.apply([{ op: 'add', path: '/a', value: 1 }], void 0); 58 | }); 59 | }, 60 | 61 | 'when target is not an object or array': function() { 62 | assert.exception(function() { 63 | jsonPatch.apply([{ op: 'add', path: '/a/b', value: 1 }], { a: 0 }); 64 | }, 'InvalidPatchOperationError'); 65 | } 66 | } 67 | }, 68 | 69 | 70 | 'remove': { 71 | 'should remove': function() { 72 | var a = { value: 0 }; 73 | var result = jsonPatch.apply([{ op: 'remove', path: '/value'}], a); 74 | refute.defined(result.value); 75 | }, 76 | 77 | 'should throw': { 78 | 'when path is invalid': function() { 79 | assert.exception(function() { 80 | jsonPatch.apply([{ op: 'remove', path: '/a', value: 1 }], {}); 81 | }, 'InvalidPatchOperationError'); 82 | }, 83 | 84 | 'when target is null': function() { 85 | assert.exception(function() { 86 | jsonPatch.apply([{ op: 'remove', path: '/a', value: 1 }], null); 87 | }, 'InvalidPatchOperationError'); 88 | }, 89 | 90 | 'when target is undefined': function() { 91 | assert.exception(function() { 92 | jsonPatch.apply([{ op: 'remove', path: '/a', value: 1 }], void 0); 93 | }); 94 | } 95 | } 96 | }, 97 | 98 | 'replace': { 99 | 'should replace': function() { 100 | var a = { value: 0 }; 101 | var result = jsonPatch.apply([{ op: 'add', path: '/value', value: 1 }], a); 102 | assert.equals(result.value, 1); 103 | }, 104 | 105 | 'should throw': { 106 | 'when path is invalid': function() { 107 | assert.exception(function() { 108 | jsonPatch.apply([{ op: 'replace', path: '/a', value: 1 }], {}); 109 | }, 'InvalidPatchOperationError'); 110 | }, 111 | 112 | 'when target is null': function() { 113 | assert.exception(function() { 114 | jsonPatch.apply([{ op: 'replace', path: '/a', value: 1 }], null); 115 | }, 'InvalidPatchOperationError'); 116 | }, 117 | 118 | 'when target is undefined': function() { 119 | assert.exception(function() { 120 | jsonPatch.apply([{ op: 'replace', path: '/a', value: 1 }], void 0); 121 | }); 122 | } 123 | } 124 | }, 125 | 126 | 'move': { 127 | 'should move': function() { 128 | var a = { x: 1 }; 129 | var result = jsonPatch.apply([{ op: 'move', path: '/y', from: '/x' }], a); 130 | assert.equals(result.y, 1); 131 | refute.defined(result.x); 132 | }, 133 | 134 | 'should not allow moving to ancestor path': function() { 135 | var from = '/a/b/c'; 136 | var to = '/a/b'; 137 | assert.exception(function() { 138 | patches.move.apply({ a: { b: { c: 1 }}}, { op: 'move', from: from, path: to }); 139 | }); 140 | } 141 | }, 142 | 143 | 'copy': { 144 | 'should copy': function() { 145 | var a = { x: { value: 1 } }; 146 | var result = jsonPatch.apply([{ op: 'copy', path: '/y', from: '/x' }], a); 147 | assert.equals(result.x.value, 1); 148 | assert.equals(result.y.value, 1); 149 | refute.same(result.x, result.y); 150 | }, 151 | 152 | 'should not allow copying from non-existent path': function() { 153 | assert.exception(function() { 154 | jsonPatch.apply([{ op: 'copy', from: '/y/z', path: '/x' }], {}); 155 | }); 156 | } 157 | }, 158 | 159 | 'test': { 160 | 'should pass when values are deep equal': function() { 161 | var test = { 162 | num: 1, 163 | string: 'bar', 164 | bool: true, 165 | array: [1, { name: 'x' }, 'baz', true, false, null], 166 | object: { 167 | value: 2 168 | } 169 | }; 170 | var a = { x: test }; 171 | var y = jsonPatch.clone(test); 172 | 173 | refute.exception(function() { 174 | var result = jsonPatch.apply([{ op: 'test', path: '/x', value: y }], a); 175 | assert.equals(JSON.stringify(a), JSON.stringify(result)); 176 | }); 177 | }, 178 | 179 | 'should fail when values are not deep equal': function() { 180 | var test = { array: [1, { name: 'x' }] }; 181 | var y = jsonPatch.clone(test); 182 | 183 | test.array[1].name = 'y'; 184 | var a = { x: test }; 185 | 186 | assert.exception(function() { 187 | jsonPatch.apply([{ op: 'test', path: '/x', value: y }], a); 188 | }, 'TestFailedError'); 189 | }, 190 | 191 | 'should test whole document': { 192 | 'when document and value are not null': function() { 193 | var doc = { a: { b: 123 } }; 194 | refute.exception(function() { 195 | jsonPatch.apply([{ op: 'test', path: '', value: doc }], doc); 196 | }); 197 | }, 198 | 199 | 'when document and value are null': function() { 200 | refute.exception(function() { 201 | jsonPatch.apply([{ op: 'test', path: '', value: null }], null); 202 | }); 203 | }, 204 | 205 | 'when value is null': function() { 206 | var doc = { a: { b: 123 } }; 207 | assert.exception(function() { 208 | jsonPatch.apply([{ op: 'test', path: '', value: doc }], null); 209 | }, function(e) { 210 | return e instanceof TestFailedError; 211 | }); 212 | }, 213 | 214 | 'when document is null': function() { 215 | var doc = { a: { b: 123 } }; 216 | assert.exception(function() { 217 | jsonPatch.apply([{ op: 'test', path: '', value: null }], doc); 218 | }, function(e) { 219 | return e instanceof TestFailedError; 220 | }); 221 | } 222 | } 223 | 224 | } 225 | }); 226 | -------------------------------------------------------------------------------- /jiff.js: -------------------------------------------------------------------------------- 1 | /** @license MIT License (c) copyright 2010-2014 original author or authors */ 2 | /** @author Brian Cavalier */ 3 | /** @author John Hann */ 4 | 5 | var lcs = require('./lib/lcs'); 6 | var array = require('./lib/array'); 7 | var patch = require('./lib/jsonPatch'); 8 | var inverse = require('./lib/inverse'); 9 | var jsonPointer = require('./lib/jsonPointer'); 10 | var encodeSegment = jsonPointer.encodeSegment; 11 | 12 | exports.diff = diff; 13 | exports.patch = patch.apply; 14 | exports.patchInPlace = patch.applyInPlace; 15 | exports.inverse = inverse; 16 | exports.clone = patch.clone; 17 | 18 | // Errors 19 | exports.InvalidPatchOperationError = require('./lib/InvalidPatchOperationError'); 20 | exports.TestFailedError = require('./lib/TestFailedError'); 21 | exports.PatchNotInvertibleError = require('./lib/PatchNotInvertibleError'); 22 | 23 | var isValidObject = patch.isValidObject; 24 | var defaultHash = patch.defaultHash; 25 | 26 | /** 27 | * Compute a JSON Patch representing the differences between a and b. 28 | * @param {object|array|string|number|null} a 29 | * @param {object|array|string|number|null} b 30 | * @param {?function|?object} options if a function, see options.hash 31 | * @param {?function(x:*):String|Number} options.hash used to hash array items 32 | * in order to recognize identical objects, defaults to JSON.stringify 33 | * @param {?function(index:Number, array:Array):object} options.makeContext 34 | * used to generate patch context. If not provided, context will not be generated 35 | * @returns {array} JSON Patch such that patch(diff(a, b), a) ~ b 36 | */ 37 | function diff(a, b, options) { 38 | return appendChanges(a, b, '', initState(options, [])).patch; 39 | } 40 | 41 | /** 42 | * Create initial diff state from the provided options 43 | * @param {?function|?object} options @see diff options above 44 | * @param {array} patch an empty or existing JSON Patch array into which 45 | * the diff should generate new patch operations 46 | * @returns {object} initialized diff state 47 | */ 48 | function initState(options, patch) { 49 | if(typeof options === 'object') { 50 | return { 51 | patch: patch, 52 | hash: orElse(isFunction, options.hash, defaultHash), 53 | makeContext: orElse(isFunction, options.makeContext, defaultContext), 54 | invertible: !(options.invertible === false) 55 | }; 56 | } else { 57 | return { 58 | patch: patch, 59 | hash: orElse(isFunction, options, defaultHash), 60 | makeContext: defaultContext, 61 | invertible: true 62 | }; 63 | } 64 | } 65 | 66 | /** 67 | * Given two JSON values (object, array, number, string, etc.), find their 68 | * differences and append them to the diff state 69 | * @param {object|array|string|number|null} a 70 | * @param {object|array|string|number|null} b 71 | * @param {string} path 72 | * @param {object} state 73 | * @returns {Object} updated diff state 74 | */ 75 | function appendChanges(a, b, path, state) { 76 | if(Array.isArray(a) && Array.isArray(b)) { 77 | return appendArrayChanges(a, b, path, state); 78 | } 79 | 80 | if(isValidObject(a) && isValidObject(b)) { 81 | return appendObjectChanges(a, b, path, state); 82 | } 83 | 84 | return appendValueChanges(a, b, path, state); 85 | } 86 | 87 | /** 88 | * Given two objects, find their differences and append them to the diff state 89 | * @param {object} o1 90 | * @param {object} o2 91 | * @param {string} path 92 | * @param {object} state 93 | * @returns {Object} updated diff state 94 | */ 95 | function appendObjectChanges(o1, o2, path, state) { 96 | var keys = Object.keys(o2); 97 | var patch = state.patch; 98 | var i, key; 99 | 100 | for(i=keys.length-1; i>=0; --i) { 101 | key = keys[i]; 102 | var keyPath = path + '/' + encodeSegment(key); 103 | if(o1[key] !== void 0) { 104 | appendChanges(o1[key], o2[key], keyPath, state); 105 | } else { 106 | patch.push({ op: 'add', path: keyPath, value: o2[key] }); 107 | } 108 | } 109 | 110 | keys = Object.keys(o1); 111 | for(i=keys.length-1; i>=0; --i) { 112 | key = keys[i]; 113 | if(o2[key] === void 0) { 114 | var p = path + '/' + encodeSegment(key); 115 | if(state.invertible) { 116 | patch.push({ op: 'test', path: p, value: o1[key] }); 117 | } 118 | patch.push({ op: 'remove', path: p }); 119 | } 120 | } 121 | 122 | return state; 123 | } 124 | 125 | /** 126 | * Given two arrays, find their differences and append them to the diff state 127 | * @param {array} a1 128 | * @param {array} a2 129 | * @param {string} path 130 | * @param {object} state 131 | * @returns {Object} updated diff state 132 | */ 133 | function appendArrayChanges(a1, a2, path, state) { 134 | var a1hash = array.map(state.hash, a1); 135 | var a2hash = array.map(state.hash, a2); 136 | 137 | var lcsMatrix = lcs.compare(a1hash, a2hash); 138 | 139 | return lcsToJsonPatch(a1, a2, path, state, lcsMatrix); 140 | } 141 | 142 | /** 143 | * Transform an lcsMatrix into JSON Patch operations and append 144 | * them to state.patch, recursing into array elements as necessary 145 | * @param {array} a1 146 | * @param {array} a2 147 | * @param {string} path 148 | * @param {object} state 149 | * @param {object} lcsMatrix 150 | * @returns {object} new state with JSON Patch operations added based 151 | * on the provided lcsMatrix 152 | */ 153 | function lcsToJsonPatch(a1, a2, path, state, lcsMatrix) { 154 | var offset = 0; 155 | return lcs.reduce(function(state, op, i, j) { 156 | var last, context; 157 | var patch = state.patch; 158 | var p = path + '/' + (j + offset); 159 | 160 | if (op === lcs.REMOVE) { 161 | // Coalesce adjacent remove + add into replace 162 | last = patch[patch.length-1]; 163 | context = state.makeContext(j, a1); 164 | 165 | if(state.invertible) { 166 | patch.push({ op: 'test', path: p, value: a1[j], context: context }); 167 | } 168 | 169 | if(last !== void 0 && last.op === 'add' && last.path === p) { 170 | last.op = 'replace'; 171 | last.context = context; 172 | } else { 173 | patch.push({ op: 'remove', path: p, context: context }); 174 | } 175 | 176 | offset -= 1; 177 | 178 | } else if (op === lcs.ADD) { 179 | // See https://tools.ietf.org/html/rfc6902#section-4.1 180 | // May use either index===length *or* '-' to indicate appending to array 181 | patch.push({ op: 'add', path: p, value: a2[i], 182 | context: state.makeContext(j, a1) 183 | }); 184 | 185 | offset += 1; 186 | 187 | } else { 188 | appendChanges(a1[j], a2[i], p, state); 189 | } 190 | 191 | return state; 192 | 193 | }, state, lcsMatrix); 194 | } 195 | 196 | /** 197 | * Given two number|string|null values, if they differ, append to diff state 198 | * @param {string|number|null} a 199 | * @param {string|number|null} b 200 | * @param {string} path 201 | * @param {object} state 202 | * @returns {object} updated diff state 203 | */ 204 | function appendValueChanges(a, b, path, state) { 205 | if(a !== b) { 206 | if(state.invertible) { 207 | state.patch.push({ op: 'test', path: path, value: a }); 208 | } 209 | 210 | state.patch.push({ op: 'replace', path: path, value: b }); 211 | } 212 | 213 | return state; 214 | } 215 | 216 | /** 217 | * @param {function} predicate 218 | * @param {*} x 219 | * @param {*} y 220 | * @returns {*} x if predicate(x) is truthy, otherwise y 221 | */ 222 | function orElse(predicate, x, y) { 223 | return predicate(x) ? x : y; 224 | } 225 | 226 | /** 227 | * Default patch context generator 228 | * @returns {undefined} undefined context 229 | */ 230 | function defaultContext() { 231 | return void 0; 232 | } 233 | 234 | /** 235 | * @param {*} x 236 | * @returns {boolean} true if x is a function, false otherwise 237 | */ 238 | function isFunction(x) { 239 | return typeof x === 'function'; 240 | } 241 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Diff and Patch 2 | 3 | Jiff is an implementation of [JSON Patch RFC6902](https://tools.ietf.org/html/rfc6902), plus a Diff implementation that generates compliant patches. 4 | 5 | It also provides advanced and [experimental APIs](#experimentalapis) based on patch algebra, such as [patch inverses](#inverse) ("reverse" patches), [commutation](#jifflibcommute) (patch reordering), and even [rebasing](#jifflibrebase) (moving patches from one history to another). 6 | 7 | ## Get it 8 | 9 | `npm install --save jiff` 10 | 11 | `bower install --save jiff` 12 | 13 | ## Example 14 | 15 | ```js 16 | var a = [ 17 | { name: 'a' }, 18 | { name: 'b' }, 19 | { name: 'c' }, 20 | ] 21 | 22 | var b = a.slice(); 23 | b.splice(1, 1); 24 | b.push({ name: 'd' }); 25 | 26 | // Generate diff (ie JSON Patch) from a to b 27 | var patch = jiff.diff(a, b); 28 | 29 | // [{"op":"add","path":"/3","value":{"name":"d"}},{"op":"remove","path":"/1"}] 30 | console.log(JSON.stringify(patch)); 31 | 32 | var patched = jiff.patch(patch, a); 33 | 34 | // [{"name":"a"},{"name":"c"},{"name":"d"}] 35 | console.log(JSON.stringify(patched)); 36 | ``` 37 | 38 | ## API 39 | 40 | ### patch 41 | 42 | ```js 43 | var b = jiff.patch(patch, a [, options]); 44 | ``` 45 | 46 | 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. 47 | 48 | * `options` 49 | * `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. 50 | 51 | Throws [InvalidPatchOperationError](#invalidpatchoperationerror) and [TestFailedError](#testfailederror). 52 | 53 | ### patchInPlace 54 | 55 | ```js 56 | a = jiff.patchInPlace(patch, a [, options]); 57 | ``` 58 | 59 | Given an rfc6902 JSON Patch, apply it directly to `a`, *mutating `a`*. 60 | 61 | Note that this is an opt-in violation of the patching algorithm outlined in rfc6902. It may provide some performance benefits as it avoids creating a new clone of `a` before patching. 62 | 63 | However, if patching fails mid-patch, `a` will be left in an inconsistent state. 64 | 65 | Throws [InvalidPatchOperationError](#invalidpatchoperationerror) and [TestFailedError](#testfailederror). 66 | 67 | ### diff 68 | 69 | ```js 70 | var patch = jiff.diff(a, b [, hashFunction | options]); 71 | ``` 72 | 73 | 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`. 74 | 75 | The optional third parameter can be *either* an `options` object (preferably) or a function (deprecated: allowed backward compatibility). 76 | 77 | * `options`: 78 | * `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. 79 | * `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). 80 | * `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. 81 | * `hashFunction(x) -> string|number`: same as `options.hash` above 82 | 83 | 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 | 85 | ### inverse 86 | 87 | ```js 88 | var patchInverse = jiff.inverse(patch); 89 | ``` 90 | 91 | Compute an inverse patch. Applying the inverse of a patch will undo the effect of the original. 92 | 93 | 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: 94 | 95 | 1. Each `remove` and `replace` operation must be preceded by a `test` operation that verifies the `value` at the `path` being removed/replaced. 96 | 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. 97 | 98 | ### clone 99 | 100 | ```js 101 | var b = jiff.clone(a); 102 | ``` 103 | 104 | Creates a deep copy of `a`, which must be a valid JSON object/array/value. 105 | 106 | **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*. 107 | 108 | As of 0.7.0 `jiff.clone` creates exact copies. 109 | 110 | 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. 111 | 112 | ### Patch context 113 | 114 | 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. 115 | 116 | Using patch contexts can greatly improve patch accuracy for arrays, at the cost of increasing the size of patches. 117 | 118 | 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. 119 | 120 | When you supply the optional `makeContext` function to `jiff.diff`, it will be used to generated a context for each change to an array. 121 | 122 | 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. 123 | 124 | 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. 125 | 126 | 127 | ## Experimental APIs 128 | 129 | These APIs are still considered experimental, signatures may change. 130 | 131 | ### jiff/lib/context 132 | 133 | ```js 134 | var context = require('jiff/lib/context'); 135 | 136 | // Create a makeContext function that can be passed to jiff.diff 137 | var makeContext = context.makeContext(size); 138 | 139 | // Create a findContext function that can be passed to jiff.patch 140 | var findContext = context.makeContextFinder(equals); 141 | ``` 142 | 143 | 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. 144 | 145 | `context.makeContext(size)` *returns* a function that can be passed as `options.makeContext` to `jiff.diff`. 146 | * `size: number` is the number of array items before and after each change to include in the patch. 147 | 148 | `context.makeContextFinder(equals)` *returns* a function that can be passed as `options.findContext` to `jiff.patch`. 149 | * `equals: function(a, b) -> boolean` a function to compare two array items, must return truthy when `a` and `b` are equal, falsy otherwise. 150 | 151 | ### jiff/lib/rebase 152 | 153 | ```js 154 | var rebase = require('jiff/lib/rebase'); 155 | var patchRebased = rebase(patchHistory, patch); 156 | ``` 157 | 158 | Yes, this is `git rebase` for JSON Patch. 159 | 160 | Given a patchHistory (Array of patches), and a single patch rooted at the same starting document context, rebase patch onto patchHistory, so that it may be applied after patchHistory. 161 | 162 | Rebasing is dependent on [commutation](#jifflibcommute), and so is also *highly experimental*. If the rebase cannot be performed, it will throw a `TypeError`. 163 | 164 | ### jiff/lib/commute 165 | 166 | ```js 167 | var commute = require('jiff/lib/commute'); 168 | var [p2c, p1c] = commute(p1, p2); 169 | ``` 170 | 171 | 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`. 172 | 173 | 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! 174 | 175 | Commutation does attempt to detect operations that cannot be commuted, and in such cases, will throw a `TypeError`. 176 | 177 | ## Errors 178 | 179 | ### InvalidPatchOperationError 180 | 181 | Thrown when any invalid patch operation is encountered. Invalid patch operations are outlined in [sections 4.x](https://tools.ietf.org/html/rfc6902#section-4) [and 5](https://tools.ietf.org/html/rfc6902#section-5) in rfc6902. For example: non-existent path in a remove operation, array path index out of bounds, etc. 182 | 183 | ### TestFailedError 184 | 185 | Thrown when a [`test` operation](https://tools.ietf.org/html/rfc6902#section-4.6) fails. 186 | 187 | ## License 188 | 189 | MIT 190 | -------------------------------------------------------------------------------- /lib/patches.js: -------------------------------------------------------------------------------- 1 | var jsonPointer = require('./jsonPointer'); 2 | var clone = require('./clone'); 3 | var deepEquals = require('./deepEquals'); 4 | var commutePaths = require('./commutePaths'); 5 | 6 | var array = require('./array'); 7 | 8 | var TestFailedError = require('./TestFailedError'); 9 | var InvalidPatchOperationError = require('./InvalidPatchOperationError'); 10 | var PatchNotInvertibleError = require('./PatchNotInvertibleError'); 11 | 12 | var find = jsonPointer.find; 13 | var parseArrayIndex = jsonPointer.parseArrayIndex; 14 | 15 | exports.test = { 16 | apply: applyTest, 17 | inverse: invertTest, 18 | commute: commuteTest 19 | }; 20 | 21 | exports.add = { 22 | apply: applyAdd, 23 | inverse: invertAdd, 24 | commute: commuteAddOrCopy 25 | }; 26 | 27 | exports.remove = { 28 | apply: applyRemove, 29 | inverse: invertRemove, 30 | commute: commuteRemove 31 | }; 32 | 33 | exports.replace = { 34 | apply: applyReplace, 35 | inverse: invertReplace, 36 | commute: commuteReplace 37 | }; 38 | 39 | exports.move = { 40 | apply: applyMove, 41 | inverse: invertMove, 42 | commute: commuteMove 43 | }; 44 | 45 | exports.copy = { 46 | apply: applyCopy, 47 | inverse: notInvertible, 48 | commute: commuteAddOrCopy 49 | }; 50 | 51 | /** 52 | * Apply a test operation to x 53 | * @param {object|array} x 54 | * @param {object} test test operation 55 | * @throws {TestFailedError} if the test operation fails 56 | */ 57 | 58 | function applyTest(x, test, options) { 59 | var pointer = find(x, test.path, options.findContext, test.context); 60 | var target = pointer.target; 61 | var index, value; 62 | 63 | if(Array.isArray(target)) { 64 | index = parseArrayIndex(pointer.key); 65 | //index = findIndex(options.findContext, index, target, test.context); 66 | value = target[index]; 67 | } else { 68 | value = pointer.key === void 0 ? pointer.target : pointer.target[pointer.key]; 69 | } 70 | 71 | if(!deepEquals(value, test.value)) { 72 | throw new TestFailedError('test failed ' + JSON.stringify(test)); 73 | } 74 | 75 | return x; 76 | } 77 | 78 | /** 79 | * Invert the provided test and add it to the inverted patch sequence 80 | * @param pr 81 | * @param test 82 | * @returns {number} 83 | */ 84 | function invertTest(pr, test) { 85 | pr.push(test); 86 | return 1; 87 | } 88 | 89 | function commuteTest(test, b) { 90 | if(test.path === b.path && b.op === 'remove') { 91 | throw new TypeError('Can\'t commute test,remove -> remove,test for same path'); 92 | } 93 | 94 | if(b.op === 'test' || b.op === 'replace') { 95 | return [b, test]; 96 | } 97 | 98 | return commutePaths(test, b); 99 | } 100 | 101 | /** 102 | * Apply an add operation to x 103 | * @param {object|array} x 104 | * @param {object} change add operation 105 | */ 106 | function applyAdd(x, change, options) { 107 | var pointer = find(x, change.path, options.findContext, change.context); 108 | 109 | if(notFound(pointer)) { 110 | throw new InvalidPatchOperationError('path does not exist ' + change.path); 111 | } 112 | 113 | if(change.value === void 0) { 114 | throw new InvalidPatchOperationError('missing value'); 115 | } 116 | 117 | var val = clone(change.value); 118 | 119 | // If pointer refers to whole document, replace whole document 120 | if(pointer.key === void 0) { 121 | return val; 122 | } 123 | 124 | _add(pointer, val); 125 | return x; 126 | } 127 | 128 | function _add(pointer, value) { 129 | var target = pointer.target; 130 | 131 | if(Array.isArray(target)) { 132 | // '-' indicates 'append' to array 133 | if(pointer.key === '-') { 134 | target.push(value); 135 | } else if (pointer.key > target.length) { 136 | throw new InvalidPatchOperationError('target of add outside of array bounds') 137 | } else { 138 | target.splice(pointer.key, 0, value); 139 | } 140 | } else if(isValidObject(target)) { 141 | target[pointer.key] = value; 142 | } else { 143 | throw new InvalidPatchOperationError('target of add must be an object or array ' + pointer.key); 144 | } 145 | } 146 | 147 | function invertAdd(pr, add) { 148 | var context = add.context; 149 | if(context !== void 0) { 150 | context = { 151 | before: context.before, 152 | after: array.cons(add.value, context.after) 153 | } 154 | } 155 | pr.push({ op: 'test', path: add.path, value: add.value, context: context }); 156 | pr.push({ op: 'remove', path: add.path, context: context }); 157 | return 1; 158 | } 159 | 160 | function commuteAddOrCopy(add, b) { 161 | if(add.path === b.path && b.op === 'remove') { 162 | throw new TypeError('Can\'t commute add,remove -> remove,add for same path'); 163 | } 164 | 165 | return commutePaths(add, b); 166 | } 167 | 168 | /** 169 | * Apply a replace operation to x 170 | * @param {object|array} x 171 | * @param {object} change replace operation 172 | */ 173 | function applyReplace(x, change, options) { 174 | var pointer = find(x, change.path, options.findContext, change.context); 175 | 176 | if(notFound(pointer) || missingValue(pointer)) { 177 | throw new InvalidPatchOperationError('path does not exist ' + change.path); 178 | } 179 | 180 | if(change.value === void 0) { 181 | throw new InvalidPatchOperationError('missing value'); 182 | } 183 | 184 | var value = clone(change.value); 185 | 186 | // If pointer refers to whole document, replace whole document 187 | if(pointer.key === void 0) { 188 | return value; 189 | } 190 | 191 | var target = pointer.target; 192 | 193 | if(Array.isArray(target)) { 194 | target[parseArrayIndex(pointer.key)] = value; 195 | } else { 196 | target[pointer.key] = value; 197 | } 198 | 199 | return x; 200 | } 201 | 202 | function invertReplace(pr, c, i, patch) { 203 | var prev = patch[i-1]; 204 | if(prev === void 0 || prev.op !== 'test' || prev.path !== c.path) { 205 | throw new PatchNotInvertibleError('cannot invert replace w/o test'); 206 | } 207 | 208 | var context = prev.context; 209 | if(context !== void 0) { 210 | context = { 211 | before: context.before, 212 | after: array.cons(prev.value, array.tail(context.after)) 213 | } 214 | } 215 | 216 | pr.push({ op: 'test', path: prev.path, value: c.value }); 217 | pr.push({ op: 'replace', path: prev.path, value: prev.value }); 218 | return 2; 219 | } 220 | 221 | function commuteReplace(replace, b) { 222 | if(replace.path === b.path && b.op === 'remove') { 223 | throw new TypeError('Can\'t commute replace,remove -> remove,replace for same path'); 224 | } 225 | 226 | if(b.op === 'test' || b.op === 'replace') { 227 | return [b, replace]; 228 | } 229 | 230 | return commutePaths(replace, b); 231 | } 232 | 233 | /** 234 | * Apply a remove operation to x 235 | * @param {object|array} x 236 | * @param {object} change remove operation 237 | */ 238 | function applyRemove(x, change, options) { 239 | var pointer = find(x, change.path, options.findContext, change.context); 240 | 241 | // key must exist for remove 242 | if(notFound(pointer) || pointer.target[pointer.key] === void 0) { 243 | throw new InvalidPatchOperationError('path does not exist ' + change.path); 244 | } 245 | 246 | _remove(pointer); 247 | return x; 248 | } 249 | 250 | function _remove (pointer) { 251 | var target = pointer.target; 252 | 253 | var removed; 254 | if (Array.isArray(target)) { 255 | removed = target.splice(parseArrayIndex(pointer.key), 1); 256 | return removed[0]; 257 | 258 | } else if (isValidObject(target)) { 259 | removed = target[pointer.key]; 260 | delete target[pointer.key]; 261 | return removed; 262 | 263 | } else { 264 | throw new InvalidPatchOperationError('target of remove must be an object or array'); 265 | } 266 | } 267 | 268 | function invertRemove(pr, c, i, patch) { 269 | var prev = patch[i-1]; 270 | if(prev === void 0 || prev.op !== 'test' || prev.path !== c.path) { 271 | throw new PatchNotInvertibleError('cannot invert remove w/o test'); 272 | } 273 | 274 | var context = prev.context; 275 | if(context !== void 0) { 276 | context = { 277 | before: context.before, 278 | after: array.tail(context.after) 279 | } 280 | } 281 | 282 | pr.push({ op: 'add', path: prev.path, value: prev.value, context: context }); 283 | return 2; 284 | } 285 | 286 | function commuteRemove(remove, b) { 287 | if(remove.path === b.path && b.op === 'remove') { 288 | return [b, remove]; 289 | } 290 | 291 | return commutePaths(remove, b); 292 | } 293 | 294 | /** 295 | * Apply a move operation to x 296 | * @param {object|array} x 297 | * @param {object} change move operation 298 | */ 299 | function applyMove(x, change, options) { 300 | if(jsonPointer.contains(change.path, change.from)) { 301 | throw new InvalidPatchOperationError('move.from cannot be ancestor of move.path'); 302 | } 303 | 304 | var pto = find(x, change.path, options.findContext, change.context); 305 | var pfrom = find(x, change.from, options.findContext, change.fromContext); 306 | 307 | _add(pto, _remove(pfrom)); 308 | return x; 309 | } 310 | 311 | function invertMove(pr, c) { 312 | pr.push({ op: 'move', 313 | path: c.from, context: c.fromContext, 314 | from: c.path, fromContext: c.context }); 315 | return 1; 316 | } 317 | 318 | function commuteMove(move, b) { 319 | if(move.path === b.path && b.op === 'remove') { 320 | throw new TypeError('Can\'t commute move,remove -> move,replace for same path'); 321 | } 322 | 323 | return commutePaths(move, b); 324 | } 325 | 326 | /** 327 | * Apply a copy operation to x 328 | * @param {object|array} x 329 | * @param {object} change copy operation 330 | */ 331 | function applyCopy(x, change, options) { 332 | var pto = find(x, change.path, options.findContext, change.context); 333 | var pfrom = find(x, change.from, options.findContext, change.fromContext); 334 | 335 | if(notFound(pfrom) || missingValue(pfrom)) { 336 | throw new InvalidPatchOperationError('copy.from must exist'); 337 | } 338 | 339 | var target = pfrom.target; 340 | var value; 341 | 342 | if(Array.isArray(target)) { 343 | value = target[parseArrayIndex(pfrom.key)]; 344 | } else { 345 | value = target[pfrom.key]; 346 | } 347 | 348 | _add(pto, clone(value)); 349 | return x; 350 | } 351 | 352 | // NOTE: Copy is not invertible 353 | // See https://github.com/cujojs/jiff/issues/9 354 | // This needs more thought. We may have to extend/amend JSON Patch. 355 | // At first glance, this seems like it should just be a remove. 356 | // However, that's not correct. It violates the involution: 357 | // invert(invert(p)) ~= p. For example: 358 | // invert(copy) -> remove 359 | // invert(remove) -> add 360 | // thus: invert(invert(copy)) -> add (DOH! this should be copy!) 361 | 362 | function notInvertible(_, c) { 363 | throw new PatchNotInvertibleError('cannot invert ' + c.op); 364 | } 365 | 366 | function notFound (pointer) { 367 | return pointer === void 0 || (pointer.target == null && pointer.key !== void 0); 368 | } 369 | 370 | function missingValue(pointer) { 371 | return pointer.key !== void 0 && pointer.target[pointer.key] === void 0; 372 | } 373 | 374 | /** 375 | * Return true if x is a non-null object 376 | * @param {*} x 377 | * @returns {boolean} 378 | */ 379 | function isValidObject (x) { 380 | return x !== null && typeof x === 'object'; 381 | } 382 | --------------------------------------------------------------------------------