├── .editorconfig ├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── bower.json ├── jiff.js ├── lib ├── InvalidPatchOperationError.js ├── PatchNotInvertibleError.js ├── TestFailedError.js ├── array.js ├── clone.js ├── commute.js ├── commutePaths.js ├── context.js ├── deepEquals.js ├── inverse.js ├── jsonPatch.js ├── jsonPointer.js ├── jsonPointerParse.js ├── lcs.js ├── patches.js └── rebase.js ├── package.json ├── perf ├── diff.js └── jsonPointer-perf.js └── test ├── buster.js ├── commute-test.js ├── commutePaths-test.js ├── inverse-test.js ├── jiff-test.js ├── json-patch-tests-test.js ├── jsonPatch-test.js ├── jsonPointerParse-test.js └── rebase-test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = LF 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /node_modules/ 3 | /experiments/ -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /test/ 3 | /experiments/ 4 | .* 5 | bower.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | branches: 5 | only: 6 | - master -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Open Source Initiative OSI - The MIT License 2 | 3 | http://www.opensource.org/licenses/mit-license.php 4 | 5 | Copyright (c) 2011 Brian Cavalier 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | "Software"), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /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/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/array.js: -------------------------------------------------------------------------------- 1 | /** @license MIT License (c) copyright 2010-2014 original author or authors */ 2 | /** @author Brian Cavalier */ 3 | /** @author John Hann */ 4 | 5 | exports.cons = cons; 6 | exports.tail = tail; 7 | exports.map = map; 8 | 9 | /** 10 | * Prepend x to a, without mutating a. Faster than a.unshift(x) 11 | * @param {*} x 12 | * @param {Array} a array-like 13 | * @returns {Array} new Array with x prepended 14 | */ 15 | function cons(x, a) { 16 | var l = a.length; 17 | var b = new Array(l+1); 18 | b[0] = x; 19 | 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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | exports.findPosition = findPosition; 2 | exports.matchContext = matchContext; 3 | exports.makeContextFinder = makeContextFinder; 4 | exports.makeContext = makeContext; 5 | 6 | /** 7 | * Creates a findContext function that expects a context 8 | * { before: [...], after: [...] } containing some number of items before 9 | * and after the change. Uses the provided equals function to compare 10 | * array itesm and find the best fit position in the array to 11 | * apply the patch. Uses a similar algorithm to GNU patch. 12 | * @param {function(a:*, b:*):boolean} equals return truthy if a and b are equal 13 | * @returns {Function} a findContext function that can be passed to jiff.patch 14 | */ 15 | function makeContextFinder(equals) { 16 | return function(index, array, context) { 17 | return findPosition(equals, index, array, context); 18 | }; 19 | } 20 | 21 | /** 22 | * Creates a makeContext function that will generate patch contexts 23 | * { before: [...], after: [...] } containing `size` number of items 24 | * before and after the change. 25 | * @param {number} size max number of items before/after the change to include 26 | * @returns {Function} a makeContext function that can be passed to jiff.diff 27 | */ 28 | function makeContext(size) { 29 | return function (index, array) { 30 | return { 31 | before: array.slice(Math.max(0, index - size), index), 32 | after: array.slice(Math.min(array.length, index + 1), index + size + 1) 33 | }; 34 | }; 35 | } 36 | 37 | // TODO: Include removed items in the patch context when patch is a remove 38 | 39 | function findPosition (equals, start, array, context) { 40 | var index; 41 | var before = context.before; 42 | var blen = before.length; 43 | var bmax = 0; 44 | var after = context.after; 45 | var amax = after.length; 46 | 47 | while(amax > 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= 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/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. 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 | } -------------------------------------------------------------------------------- /lib/jsonPointerParse.js: -------------------------------------------------------------------------------- 1 | /** @license MIT License (c) copyright 2010-2014 original author or authors */ 2 | /** @author Brian Cavalier */ 3 | /** @author John Hann */ 4 | 5 | module.exports = jsonPointerParse; 6 | 7 | var parseRx = /\/|~1|~0/g; 8 | var separator = '/'; 9 | var escapeChar = '~'; 10 | var encodedSeparator = '~1'; 11 | 12 | /** 13 | * Parse through an encoded JSON Pointer string, decoding each path segment 14 | * and passing it to an onSegment callback function. 15 | * @see https://tools.ietf.org/html/rfc6901#section-4 16 | * @param {string} path encoded JSON Pointer string 17 | * @param {{function(segment:string):boolean}} onSegment callback function 18 | * @returns {string} original path 19 | */ 20 | function jsonPointerParse(path, onSegment) { 21 | var pos, accum, matches, match; 22 | 23 | pos = path.charAt(0) === separator ? 1 : 0; 24 | accum = ''; 25 | parseRx.lastIndex = pos; 26 | 27 | while(matches = parseRx.exec(path)) { 28 | 29 | match = matches[0]; 30 | accum += path.slice(pos, parseRx.lastIndex - match.length); 31 | pos = parseRx.lastIndex; 32 | 33 | if(match === separator) { 34 | if (onSegment(accum) === false) return path; 35 | accum = ''; 36 | } else { 37 | accum += match === encodedSeparator ? separator : escapeChar; 38 | } 39 | } 40 | 41 | accum += path.slice(pos); 42 | onSegment(accum); 43 | 44 | return path; 45 | } 46 | -------------------------------------------------------------------------------- /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 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 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 | }); -------------------------------------------------------------------------------- /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 | }); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/json-patch-tests-test.js: -------------------------------------------------------------------------------- 1 | var buster = require('buster'); 2 | var assert = buster.referee.assert; 3 | var deepEquals = require('../lib/deepEquals'); 4 | 5 | var jsonPatch = require('../lib/jsonPatch'); 6 | 7 | var specTests = require('json-patch-test-suite/spec_tests.json'); 8 | var tests = require('json-patch-test-suite/tests.json'); 9 | 10 | buster.testCase('json-patch-tests', { 11 | 'spec_tests.json': jsonToBuster(specTests), 12 | 'tests.json': jsonToBuster(tests) 13 | }); 14 | 15 | /** 16 | * Converts json-based tests in json-patch-tests to buster testCase objects 17 | * @param {array} tests 18 | * @returns {object} 19 | */ 20 | function jsonToBuster(tests) { 21 | return tests.reduce(function(testCase, test) { 22 | if(test.disabled) { 23 | testCase['//' + test.comment] = noop; 24 | } else { 25 | var doc = test.doc; 26 | var patch = test.patch; 27 | if(test.error) { 28 | testCase[test.comment] = function() { 29 | assert.exception(function() { 30 | jsonPatch.apply(patch, doc); 31 | }); 32 | } 33 | } else if(test.expected) { 34 | testCase[test.comment] = function() { 35 | assert(deepEquals(test.expected, jsonPatch.apply(patch, doc))); 36 | } 37 | } 38 | } 39 | 40 | return testCase; 41 | }, {}); 42 | } 43 | 44 | function noop() {} 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | }); --------------------------------------------------------------------------------