diff --git a/.travis.yml b/.travis.yml index 4b8f518a..7c35fe92 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,4 +5,4 @@ before_script: - npm install - npm run serve & script: - - npm run test-sauce + - npm run test diff --git a/README.md b/README.md index 9c93221a..e1e9f262 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,24 @@ var patch = jsonpatch.generate(observer); // ]; ``` +Generating patches with test operations for values in the first object: + +```js +var document = { firstName: "Joachim", lastName: "Wester", contactDetails: { phoneNumbers: [ { number:"555-123" }] } }; +var observer = jsonpatch.observe(document); +document.firstName = "Albert"; +document.contactDetails.phoneNumbers[0].number = "123"; +document.contactDetails.phoneNumbers.push({ number:"456" }); +var patch = jsonpatch.generate(observer, true); +// patch == [ +// { op: "test", path: "/firstName", value: "Joachim"}, +// { op: "replace", path: "/firstName", value: "Albert"}, +// { op: "test", path: "/contactDetails/phoneNumbers/0/number", value: "555-123" }, +// { op: "replace", path: "/contactDetails/phoneNumbers/0/number", value: "123" }, +// { op: "add", path: "/contactDetails/phoneNumbers/1", value: {number:"456"}} +// ]; +``` + Comparing two object trees: ```js @@ -163,6 +181,18 @@ var diff = jsonpatch.compare(documentA, documentB); //diff == [{op: "replace", path: "/user/lastName", value: "Collins"}] ``` +Comparing two object trees with test operations for values in the first object: + +```js +var documentA = {user: {firstName: "Albert", lastName: "Einstein"}}; +var documentB = {user: {firstName: "Albert", lastName: "Collins"}}; +var diff = jsonpatch.compare(documentA, documentB, true); +//diff == [ +// {op: "test", path: "/user/lastName", value: "Einstein"}, +// {op: "replace", path: "/user/lastName", value: "Collins"} +// ]; +``` + Validating a sequence of patches: ```js @@ -269,10 +299,10 @@ callback is called with the generated patches array as the parameter. Returns `observer`. -#### `jsonpatch.generate(document: any, observer: Observer): Operation[]` +#### `jsonpatch.generate(document: any, observer: Observer, invertible = false): Operation[]` If there are pending changes in `obj`, returns them synchronously. If a `callback` was defined in `observe` -method, it will be triggered synchronously as well. +method, it will be triggered synchronously as well. If `invertible` is true, then each change will be preceded by a test operation of the value before the change. If there are no pending changes in `obj`, returns an empty array (length 0). @@ -282,9 +312,9 @@ Destroys the observer set up on `document`. Any remaining changes are delivered synchronously (as in `jsonpatch.generate`). Note: this is different that ES6/7 `Object.unobserve`, which delivers remaining changes asynchronously. -#### `jsonpatch.compare(document1: any, document2: any): Operation[]` +#### `jsonpatch.compare(document1: any, document2: any, invertible = false): Operation[]` -Compares object trees `document1` and `document2` and returns the difference relative to `document1` as a patches array. +Compares object trees `document1` and `document2` and returns the difference relative to `document1` as a patches array. If `invertible` is true, then each change will be preceded by a test operation of the value in `document1`. If there are no differences, returns an empty array (length 0). @@ -346,7 +376,7 @@ Functions `applyPatch`, `applyOperation`, and `validate` accept a `validate`/ `v If you pass a validator, it will be called with four parameters for each operation, `function(operation, index, tree, existingPath)` and it is expected to throw `JsonPatchError` when your conditions are not met. - `operation` The operation it self. -- `index` `operation`'s index in the patch array (if application). +- `index` `operation`'s index in the patch array (if application). - `tree` The object that is supposed to be patched. - `existingPath` the path `operation` points to. diff --git a/dist/fast-json-patch.js b/dist/fast-json-patch.js index 54ee455d..4048e979 100644 --- a/dist/fast-json-patch.js +++ b/dist/fast-json-patch.js @@ -674,7 +674,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); */ var helpers_1 = __webpack_require__(0); var core_1 = __webpack_require__(1); -/* export all core functions */ +/* export all core functions and types */ var core_2 = __webpack_require__(1); exports.applyOperation = core_2.applyOperation; exports.applyPatch = core_2.applyPatch; @@ -722,7 +722,8 @@ exports.unobserve = unobserve; /** * Observes changes made to an object, which can then be retrieved using generate */ -function observe(obj, callback) { +function observe(obj, callback, inversible) { + if (inversible === void 0) { inversible = false; } var patches = []; var observer; var mirror = getMirror(obj); @@ -737,7 +738,7 @@ function observe(obj, callback) { if (observer) { return observer; } - observer = {}; + observer = { inversible: inversible }; mirror.value = helpers_1._deepClone(obj); if (callback) { observer.callback = callback; @@ -794,9 +795,11 @@ exports.observe = observe; /** * Generate an array of patches from an observer */ -function generate(observer) { +function generate(observer, opts) { + if (opts === void 0) { opts = {}; } var mirror = beforeDict.get(observer.object); - _generate(mirror.value, observer.object, observer.patches, ""); + var inversible = typeof opts.inversible !== "undefined" ? opts.inversible : observer.inversible; + _generate(mirror.value, observer.object, observer.patches, "", { inversible: inversible }); if (observer.patches.length) { core_1.applyPatch(mirror.value, observer.patches); } @@ -811,7 +814,8 @@ function generate(observer) { } exports.generate = generate; // Dirty check if obj is different from mirror, generate patches and update mirror -function _generate(mirror, obj, patches, path) { +function _generate(mirror, obj, patches, path, opts) { + if (opts === void 0) { opts = { inversible: false }; } if (obj === mirror) { return; } @@ -822,6 +826,7 @@ function _generate(mirror, obj, patches, path) { var oldKeys = helpers_1._objectKeys(mirror); var changed = false; var deleted = false; + var inversible = opts.inversible; //if ever "move" operation is implemented here, make sure this test runs OK: "should not generate the same patch twice (move)" for (var t = oldKeys.length - 1; t >= 0; t--) { var key = oldKeys[t]; @@ -829,20 +834,26 @@ function _generate(mirror, obj, patches, path) { if (helpers_1.hasOwnProperty(obj, key) && !(obj[key] === undefined && oldVal !== undefined && Array.isArray(obj) === false)) { var newVal = obj[key]; if (typeof oldVal == "object" && oldVal != null && typeof newVal == "object" && newVal != null) { - _generate(oldVal, newVal, patches, path + "/" + helpers_1.escapePathComponent(key)); + _generate(oldVal, newVal, patches, path + "/" + helpers_1.escapePathComponent(key), opts); } else { if (oldVal !== newVal) { changed = true; + if (inversible) + patches.push({ op: "test", path: path + "/" + helpers_1.escapePathComponent(key), value: helpers_1._deepClone(oldVal) }); patches.push({ op: "replace", path: path + "/" + helpers_1.escapePathComponent(key), value: helpers_1._deepClone(newVal) }); } } } else if (Array.isArray(mirror) === Array.isArray(obj)) { + if (inversible) + patches.push({ op: "test", path: path + "/" + helpers_1.escapePathComponent(key), value: helpers_1._deepClone(oldVal) }); patches.push({ op: "remove", path: path + "/" + helpers_1.escapePathComponent(key) }); deleted = true; // property has been deleted } else { + if (inversible) + patches.push({ op: "test", path: path, value: mirror }); patches.push({ op: "replace", path: path, value: obj }); changed = true; } @@ -860,9 +871,9 @@ function _generate(mirror, obj, patches, path) { /** * Create an array of patches from the differences in two objects */ -function compare(tree1, tree2) { +function compare(tree1, tree2, opts) { var patches = []; - _generate(tree1, tree2, patches, ''); + _generate(tree1, tree2, patches, '', opts); return patches; } exports.compare = compare; diff --git a/dist/fast-json-patch.min.js b/dist/fast-json-patch.min.js index a3eb6c16..46eef41b 100644 --- a/dist/fast-json-patch.min.js +++ b/dist/fast-json-patch.min.js @@ -5,10 +5,10 @@ var jsonpatch=function(e){var t={};function n(r){if(t[r])return t[r].exports;var * (c) 2017 Joachim Wester * MIT license */ -var n,r=this&&this.__extends||(n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(e,t)},function(e,t){function r(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)});Object.defineProperty(t,"__esModule",{value:!0});var o=Object.prototype.hasOwnProperty;function a(e,t){return o.call(e,t)}function i(e){if(Array.isArray(e)){for(var t=new Array(e.length),n=0;n=48&&t<=57))return!1;n++}return!0},t.escapePathComponent=u,t.unescapePathComponent=function(e){return e.replace(/~1/g,"/").replace(/~0/g,"~")},t._getPathRecursive=p,t.getPath=function(e,t){if(e===t)return"/";var n=p(e,t);if(""===n)throw new Error("Object not found in root");return"/"+n},t.hasUndefined=function e(t){if(void 0===t)return!0;if(t)if(Array.isArray(t)){for(var n=0,r=t.length;n=y){if(p&&"add"===n.op&&E>v.length)throw new t.JsonPatchError("The specified index MUST NOT be greater than the number of elements in the array","OPERATION_VALUE_OUT_OF_BOUNDS",l,n,e);if(!1===(d=i[n.op].call(n,v,E,e)).test)throw new t.JsonPatchError("Test operation failed","TEST_OPERATION_FAILED",l,n,e);return d}}else if(E&&-1!=E.indexOf("~")&&(E=o.unescapePathComponent(E)),m>=y){if(!1===(d=a[n.op].call(n,v,E,e)).test)throw new t.JsonPatchError("Test operation failed","TEST_OPERATION_FAILED",l,n,e);return d}v=v[E]}}function c(e,n,r,a,i){if(void 0===a&&(a=!0),void 0===i&&(i=!0),r&&!Array.isArray(n))throw new t.JsonPatchError("Patch sequence must be an array","SEQUENCE_NOT_AN_ARRAY");a||(e=o._deepClone(e));for(var u=new Array(n.length),c=0,s=n.length;c0)throw new t.JsonPatchError('Operation `path` property must start with "/"',"OPERATION_PATH_INVALID",n,e,r);if(("move"===e.op||"copy"===e.op)&&"string"!=typeof e.from)throw new t.JsonPatchError("Operation `from` property is not present (applicable in `move` and `copy` operations)","OPERATION_FROM_REQUIRED",n,e,r);if(("add"===e.op||"replace"===e.op||"test"===e.op)&&void 0===e.value)throw new t.JsonPatchError("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)","OPERATION_VALUE_REQUIRED",n,e,r);if(("add"===e.op||"replace"===e.op||"test"===e.op)&&o.hasUndefined(e.value))throw new t.JsonPatchError("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)","OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED",n,e,r);if(r)if("add"==e.op){var u=e.path.split("/").length,p=i.split("/").length;if(u!==p+1&&u!==p)throw new t.JsonPatchError("Cannot perform an `add` operation at the desired path","OPERATION_PATH_CANNOT_ADD",n,e,r)}else if("replace"===e.op||"remove"===e.op||"_get"===e.op){if(e.path!==i)throw new t.JsonPatchError("Cannot perform the operation at a path that does not exist","OPERATION_PATH_UNRESOLVABLE",n,e,r)}else if("move"===e.op||"copy"===e.op){var c=f([{op:"_get",path:e.from,value:void 0}],r);if(c&&"OPERATION_PATH_UNRESOLVABLE"===c.name)throw new t.JsonPatchError("Cannot perform the operation from a path that does not exist","OPERATION_FROM_UNRESOLVABLE",n,e,r)}}function f(e,n,r){try{if(!Array.isArray(e))throw new t.JsonPatchError("Patch sequence must be an array","SEQUENCE_NOT_AN_ARRAY");if(n)c(o._deepClone(n),o._deepClone(e),r||!0);else{r=r||s;for(var a=0;a=48&&t<=57))return!1;n++}return!0},t.escapePathComponent=p,t.unescapePathComponent=function(e){return e.replace(/~1/g,"/").replace(/~0/g,"~")},t._getPathRecursive=u,t.getPath=function(e,t){if(e===t)return"/";var n=u(e,t);if(""===n)throw new Error("Object not found in root");return"/"+n},t.hasUndefined=function e(t){if(void 0===t)return!0;if(t)if(Array.isArray(t)){for(var n=0,r=t.length;n=y){if(u&&"add"===n.op&&E>v.length)throw new t.JsonPatchError("The specified index MUST NOT be greater than the number of elements in the array","OPERATION_VALUE_OUT_OF_BOUNDS",l,n,e);if(!1===(d=i[n.op].call(n,v,E,e)).test)throw new t.JsonPatchError("Test operation failed","TEST_OPERATION_FAILED",l,n,e);return d}}else if(E&&-1!=E.indexOf("~")&&(E=o.unescapePathComponent(E)),m>=y){if(!1===(d=a[n.op].call(n,v,E,e)).test)throw new t.JsonPatchError("Test operation failed","TEST_OPERATION_FAILED",l,n,e);return d}v=v[E]}}function c(e,n,r,a,i){if(void 0===a&&(a=!0),void 0===i&&(i=!0),r&&!Array.isArray(n))throw new t.JsonPatchError("Patch sequence must be an array","SEQUENCE_NOT_AN_ARRAY");a||(e=o._deepClone(e));for(var p=new Array(n.length),c=0,s=n.length;c0)throw new t.JsonPatchError('Operation `path` property must start with "/"',"OPERATION_PATH_INVALID",n,e,r);if(("move"===e.op||"copy"===e.op)&&"string"!=typeof e.from)throw new t.JsonPatchError("Operation `from` property is not present (applicable in `move` and `copy` operations)","OPERATION_FROM_REQUIRED",n,e,r);if(("add"===e.op||"replace"===e.op||"test"===e.op)&&void 0===e.value)throw new t.JsonPatchError("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)","OPERATION_VALUE_REQUIRED",n,e,r);if(("add"===e.op||"replace"===e.op||"test"===e.op)&&o.hasUndefined(e.value))throw new t.JsonPatchError("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)","OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED",n,e,r);if(r)if("add"==e.op){var p=e.path.split("/").length,u=i.split("/").length;if(p!==u+1&&p!==u)throw new t.JsonPatchError("Cannot perform an `add` operation at the desired path","OPERATION_PATH_CANNOT_ADD",n,e,r)}else if("replace"===e.op||"remove"===e.op||"_get"===e.op){if(e.path!==i)throw new t.JsonPatchError("Cannot perform the operation at a path that does not exist","OPERATION_PATH_UNRESOLVABLE",n,e,r)}else if("move"===e.op||"copy"===e.op){var c=f([{op:"_get",path:e.from,value:void 0}],r);if(c&&"OPERATION_PATH_UNRESOLVABLE"===c.name)throw new t.JsonPatchError("Cannot perform the operation from a path that does not exist","OPERATION_FROM_UNRESOLVABLE",n,e,r)}}function f(e,n,r){try{if(!Array.isArray(e))throw new t.JsonPatchError("Patch sequence must be an array","SEQUENCE_NOT_AN_ARRAY");if(n)c(o._deepClone(n),o._deepClone(e),r||!0);else{r=r||s;for(var a=0;a0&&(e.patches=[],e.callback&&e.callback(n)),n}function f(e,t,n,o){if(t!==e){"function"==typeof t.toJSON&&(t=t.toJSON());for(var a=r._objectKeys(t),i=r._objectKeys(e),u=!1,p=i.length-1;p>=0;p--){var c=e[l=i[p]];if(!r.hasOwnProperty(t,l)||void 0===t[l]&&void 0!==c&&!1===Array.isArray(t))Array.isArray(e)===Array.isArray(t)?(n.push({op:"remove",path:o+"/"+r.escapePathComponent(l)}),u=!0):(n.push({op:"replace",path:o,value:t}),!0);else{var s=t[l];"object"==typeof c&&null!=c&&"object"==typeof s&&null!=s?f(c,s,n,o+"/"+r.escapePathComponent(l)):c!==s&&(!0,n.push({op:"replace",path:o+"/"+r.escapePathComponent(l),value:r._deepClone(s)}))}}if(u||a.length!=i.length)for(p=0;p0&&(e.patches=[],e.callback&&e.callback(a)),a}function f(e,t,n,o,a){if(void 0===a&&(a={inversible:!1}),t!==e){"function"==typeof t.toJSON&&(t=t.toJSON());for(var i=r._objectKeys(t),p=r._objectKeys(e),u=!1,c=a.inversible,s=p.length-1;s>=0;s--){var l=e[h=p[s]];if(!r.hasOwnProperty(t,h)||void 0===t[h]&&void 0!==l&&!1===Array.isArray(t))Array.isArray(e)===Array.isArray(t)?(c&&n.push({op:"test",path:o+"/"+r.escapePathComponent(h),value:r._deepClone(l)}),n.push({op:"remove",path:o+"/"+r.escapePathComponent(h)}),u=!0):(c&&n.push({op:"test",path:o,value:e}),n.push({op:"replace",path:o,value:t}),!0);else{var d=t[h];"object"==typeof l&&null!=l&&"object"==typeof d&&null!=d?f(l,d,n,o+"/"+r.escapePathComponent(h),a):l!==d&&(!0,c&&n.push({op:"test",path:o+"/"+r.escapePathComponent(h),value:r._deepClone(l)}),n.push({op:"replace",path:o+"/"+r.escapePathComponent(h),value:r._deepClone(d)}))}}if(u||i.length!=p.length)for(s=0;s(obj: Object|Array, callback?: (patches: Operation[ /** * Generate an array of patches from an observer */ -export function generate(observer: Observer): Operation[] { +export function generate(observer: Observer, invertible = false): Operation[] { var mirror = beforeDict.get(observer.object); - _generate(mirror.value, observer.object, observer.patches, ""); + + _generate(mirror.value, observer.object, observer.patches, "", invertible); if (observer.patches.length) { applyPatch(mirror.value, observer.patches); } @@ -161,7 +162,7 @@ export function generate(observer: Observer): Operation[] { } // Dirty check if obj is different from mirror, generate patches and update mirror -function _generate(mirror, obj, patches, path) { +function _generate(mirror, obj, patches, path, invertible) { if (obj === mirror) { return; } @@ -185,19 +186,28 @@ function _generate(mirror, obj, patches, path) { var newVal = obj[key]; if (typeof oldVal == "object" && oldVal != null && typeof newVal == "object" && newVal != null) { - _generate(oldVal, newVal, patches, path + "/" + escapePathComponent(key)); + _generate(oldVal, newVal, patches, path + "/" + escapePathComponent(key), invertible); } else { if (oldVal !== newVal) { changed = true; + if (invertible) { + patches.push({ op: "test", path: path + "/" + escapePathComponent(key), value: _deepClone(oldVal) }); + } patches.push({ op: "replace", path: path + "/" + escapePathComponent(key), value: _deepClone(newVal) }); } } } else if(Array.isArray(mirror) === Array.isArray(obj)) { + if (invertible) { + patches.push({ op: "test", path: path + "/" + escapePathComponent(key), value: _deepClone(oldVal) }); + } patches.push({ op: "remove", path: path + "/" + escapePathComponent(key) }); deleted = true; // property has been deleted } else { + if (invertible) { + patches.push({ op: "test", path, value: mirror }); + } patches.push({ op: "replace", path, value: obj }); changed = true; } @@ -217,8 +227,8 @@ function _generate(mirror, obj, patches, path) { /** * Create an array of patches from the differences in two objects */ -export function compare(tree1: Object | Array, tree2: Object | Array): Operation[] { +export function compare(tree1: Object | Array, tree2: Object | Array, invertible = false): Operation[] { var patches = []; - _generate(tree1, tree2, patches, ''); + _generate(tree1, tree2, patches, '', invertible); return patches; } diff --git a/test/spec/duplexBenchmark.js b/test/spec/duplexBenchmark.js index 4a25e31d..7e583793 100644 --- a/test/spec/duplexBenchmark.js +++ b/test/spec/duplexBenchmark.js @@ -145,6 +145,136 @@ suite.add('compare operation same but deep objects', { } }); +// Benchmark generating test operations +suite.add('generate operation, invertible = true', { + setup: function() { + var obj = { + firstName: 'Albert', + lastName: 'Einstein', + phoneNumbers: [ + { + number: '12345' + }, + { + number: '45353' + } + ] + }; + var observer = jsonpatch.observe(obj); + }, + fn: function() { + obj.firstName = 'Joachim'; + obj.lastName = 'Wester'; + obj.phoneNumbers[0].number = '123'; + obj.phoneNumbers[1].number = '456'; + + var patches = jsonpatch.generate(observer, true); + } +}); +suite.add('generate operation and re-apply, invertible = true', { + setup: function() { + var obj = { + firstName: 'Albert', + lastName: 'Einstein', + phoneNumbers: [ + { + number: '12345' + }, + { + number: '45353' + } + ] + }; + var observer = jsonpatch.observe(obj); + }, + fn: function() { + obj.firstName = 'Joachim'; + obj.lastName = 'Wester'; + obj.phoneNumbers[0].number = '123'; + obj.phoneNumbers[1].number = '456'; + + var patches = jsonpatch.generate(observer, true); + obj2 = { + firstName: 'Albert', + lastName: 'Einstein', + phoneNumbers: [ + { + number: '12345' + }, + { + number: '45353' + } + ] + }; + + jsonpatch.applyPatch(obj2, patches); + } +}); +suite.add('compare operation, invertible = true', { + setup: function() { + var obj = { + firstName: 'Albert', + lastName: 'Einstein', + phoneNumbers: [ + { + number: '12345' + }, + { + number: '45353' + } + ] + }; + var obj2 = { + firstName: 'Joachim', + lastName: 'Wester', + mobileNumbers: [ + { + number: '12345' + }, + { + number: '45353' + } + ] + }; + }, + fn: function() { + var patches = jsonpatch.compare(obj, obj2, true); + } +}); + +suite.add('compare operation same but deep objects, invertible = true', { + setup: function() { + var depth = 10; + + function shallowObj() { + return { + shallow: { + firstName: 'Tomek', + lastName: 'Wytrebowicz', + mobileNumbers: [ + { + number: '12345' + }, + { + number: '45353' + } + ] + } + }; + } + var obj = shallowObj(); + var node = obj; + while (depth-- > 0) { + node.nested = shallowObj(); + node = node.nested; + } + var obj2 = obj; + }, + fn: function() { + var patches = jsonpatch.compare(obj, obj2, true); + } +}); + // if we are in the browser with benchmark < 2.1.2 if (typeof benchmarkReporter !== 'undefined') { benchmarkReporter(suite); diff --git a/test/spec/duplexSpec.js b/test/spec/duplexSpec.js index 19f52b1f..223d3c3a 100644 --- a/test/spec/duplexSpec.js +++ b/test/spec/duplexSpec.js @@ -74,6 +74,16 @@ var customMatchers = { } }; +function variantIt(name, variants, fn) { + variants.forEach(variant => { + it(`${name} | variant: ${variant[0]}`, fn(...variant.slice(1))); + }); +} + +function insertIf(condition, ...elements) { + return condition ? elements : []; +} + describe('duplex', function() { beforeEach(function() { jasmine.addMatchers(customMatchers); @@ -183,139 +193,184 @@ describe('duplex', function() { expect(obj2).toReallyEqual(obj); }); - it('should generate replace (2 observers)', function() { - var person1 = { - firstName: 'Alexandra', - lastName: 'Galbreath' - }; - var person2 = { - firstName: 'Lisa', - lastName: 'Mendoza' - }; - - var observer1 = jsonpatch.observe(person1); - var observer2 = jsonpatch.observe(person2); + variantIt('should generate replace (2 observers)', [ + ['invertible = FALSE', false], + ['invertible = TRUE', true] + ], function (testInvertible) { + return function () { + var person1 = { + firstName: 'Alexandra', + lastName: 'Galbreath' + }; + var person2 = { + firstName: 'Lisa', + lastName: 'Mendoza' + }; - person1.firstName = 'Alexander'; - person2.firstName = 'Lucas'; + var observer1 = jsonpatch.observe(person1); + var observer2 = jsonpatch.observe(person2); - var patch1 = jsonpatch.generate(observer1); - var patch2 = jsonpatch.generate(observer2); + person1.firstName = 'Alexander'; + person2.firstName = 'Lucas'; - expect(patch1).toReallyEqual([ - { - op: 'replace', - path: '/firstName', - value: 'Alexander' - } - ]); - expect(patch2).toReallyEqual([ - { - op: 'replace', - path: '/firstName', - value: 'Lucas' - } - ]); - }); + var patch1 = jsonpatch.generate(observer1, testInvertible); + var patch2 = jsonpatch.generate(observer2, testInvertible); - it('should generate replace (double change, shallow object)', function() { - obj = { - firstName: 'Albert', - lastName: 'Einstein', - phoneNumbers: [ + expect(patch1).toReallyEqual([ + ...insertIf(testInvertible, { + op: "test", + path: "/firstName", + value: "Alexandra" + }), { - number: '12345' - }, + op: 'replace', + path: '/firstName', + value: 'Alexander' + } + ]); + expect(patch2).toReallyEqual([ + ...insertIf(testInvertible, { + op: "test", + path: "/firstName", + value: "Lisa" + }), { - number: '45353' + op: 'replace', + path: '/firstName', + value: 'Lucas' } - ] - }; - - var observer = jsonpatch.observe(obj); - obj.firstName = 'Marcin'; + ]); + } + }); - var patches = jsonpatch.generate(observer); - expect(patches).toReallyEqual([ - { - op: 'replace', - path: '/firstName', - value: 'Marcin' - } - ]); + variantIt('should generate test and replace (double change, shallow object)', [ + ['invertible = FALSE', false], + ['invertible = TRUE', true] + ], function (testInvertible) { + return function () { + obj = { + firstName: 'Albert', + lastName: 'Einstein', + phoneNumbers: [ + { + number: '12345' + }, + { + number: '45353' + } + ] + }; - obj.lastName = 'Warp'; - patches = jsonpatch.generate(observer); //first patch should NOT be reported again here - expect(patches).toReallyEqual([ - { - op: 'replace', - path: '/lastName', - value: 'Warp' - } - ]); + var observer = jsonpatch.observe(obj); + obj.firstName = 'Marcin'; - expect(obj).toReallyEqual({ - firstName: 'Marcin', - lastName: 'Warp', - phoneNumbers: [ - { - number: '12345' - }, + var patches = jsonpatch.generate(observer, testInvertible); + expect(patches).toReallyEqual([ + ...insertIf(testInvertible, { + op: "test", + path: "/firstName", + value: "Albert" + }), { - number: '45353' + op: 'replace', + path: '/firstName', + value: 'Marcin' } - ] - }); //objects should be still the same - }); + ]); - it('should generate replace (double change, deep object)', function() { - obj = { - firstName: 'Albert', - lastName: 'Einstein', - phoneNumbers: [ - { - number: '12345' - }, + obj.lastName = 'Warp'; + patches = jsonpatch.generate(observer, testInvertible); //first patch should NOT be reported again here + expect(patches).toReallyEqual([ + ...insertIf(testInvertible, { + op: "test", + path: "/lastName", + value: "Einstein" + }), { - number: '45353' + op: 'replace', + path: '/lastName', + value: 'Warp' } - ] - }; + ]); - var observer = jsonpatch.observe(obj); - obj.phoneNumbers[0].number = '123'; + expect(obj).toReallyEqual({ + firstName: 'Marcin', + lastName: 'Warp', + phoneNumbers: [ + { + number: '12345' + }, + { + number: '45353' + } + ] + }); //objects should be still the same + } + }); - var patches = jsonpatch.generate(observer); - expect(patches).toReallyEqual([ - { - op: 'replace', - path: '/phoneNumbers/0/number', - value: '123' - } - ]); + variantIt('should generate test and replace (double change, shallow object)', [ + ['invertible = FALSE', false], + ['invertible = TRUE', true] + ], function (testInvertible) { + return function () { + obj = { + firstName: 'Albert', + lastName: 'Einstein', + phoneNumbers: [ + { + number: '12345' + }, + { + number: '45353' + } + ] + }; - obj.phoneNumbers[1].number = '456'; - patches = jsonpatch.generate(observer); //first patch should NOT be reported again here - expect(patches).toReallyEqual([ - { - op: 'replace', - path: '/phoneNumbers/1/number', - value: '456' - } - ]); + var observer = jsonpatch.observe(obj); + obj.phoneNumbers[0].number = '123'; - expect(obj).toReallyEqual({ - firstName: 'Albert', - lastName: 'Einstein', - phoneNumbers: [ + var patches = jsonpatch.generate(observer, testInvertible); + expect(patches).toReallyEqual([ + ...insertIf(testInvertible, { + op: "test", + path: "/phoneNumbers/0/number", + value: "12345" + }), { - number: '123' - }, + op: 'replace', + path: '/phoneNumbers/0/number', + value: '123' + } + ]); + + obj.phoneNumbers[1].number = '456'; + patches = jsonpatch.generate(observer, testInvertible); //first patch should NOT be reported again here + expect(patches).toReallyEqual([ + ...insertIf(testInvertible, { + op: "test", + path: "/phoneNumbers/1/number", + value: "45353" + }), { - number: '456' + op: 'replace', + path: '/phoneNumbers/1/number', + value: '456' } - ] - }); //objects should be still the same + ]); + + expect(obj).toReallyEqual({ + firstName: 'Albert', + lastName: 'Einstein', + phoneNumbers: [ + { + number: '123' + }, + { + number: '456' + } + ] + }); //objects should be still the same + } }); it('should generate replace (changes in new array cell, primitive values)', function() { @@ -475,34 +530,49 @@ describe('duplex', function() { expect(obj2).toEqualInJson(obj); }); - it('should generate remove (array indexes should be sorted descending)', function() { - obj = { - items: ['a', 'b', 'c'] - }; - var observer = jsonpatch.observe(obj); + variantIt('should generate test and replace (double change, shallow object)', [ + ['invertible = FALSE', false], + ['invertible = TRUE', true] + ], function (testInvertible) { + return function () { + obj = { + items: ['a', 'b', 'c'] + }; + var observer = jsonpatch.observe(obj); - obj.items.pop(); - obj.items.pop(); + obj.items.pop(); + obj.items.pop(); - patches = jsonpatch.generate(observer); + patches = jsonpatch.generate(observer, testInvertible); - //array indexes must be sorted descending, otherwise there is an index collision in apply - expect(patches).toReallyEqual([ - { - op: 'remove', - path: '/items/2' - }, - { - op: 'remove', - path: '/items/1' - } - ]); + //array indexes must be sorted descending, otherwise there is an index collision in apply + expect(patches).toReallyEqual([ + ...insertIf(testInvertible, { + op: 'test', + path: '/items/2', + value: 'c' + }), + { + op: 'remove', + path: '/items/2' + }, + ...insertIf(testInvertible, { + op: 'test', + path: '/items/1', + value: 'b' + }), + { + op: 'remove', + path: '/items/1' + }, + ]); - obj2 = { - items: ['a', 'b', 'c'] - }; - jsonpatch.applyPatch(obj2, patches); - expect(obj).toEqualInJson(obj2); + obj2 = { + items: ['a', 'b', 'c'] + }; + jsonpatch.applyPatch(obj2, patches); + expect(obj).toEqualInJson(obj2); + } }); it('should not generate the same patch twice (replace)', function() { @@ -1428,172 +1498,262 @@ describe('duplex', function() { }); }); describe('compare', function() { - it('Replacing a root array with an object should be handled well', function() { - - const obj = {}; - var patches = jsonpatch.compare(['jack'], obj); - expect(patches).toEqual([ - { - op: 'replace', - path: '', - value: obj - } - ]); - + variantIt('Replacing a root array with an object should be handled well', [ + ['invertible = FALSE', false], + ['invertible = TRUE', true] + ], function (testInvertible) { + return function () { + + const objA = ['jack']; + const objB = {}; + var patches = jsonpatch.compare(objA, objB, testInvertible); + expect(patches).toEqual([ + ...insertIf(testInvertible, { + op: 'test', + path: '', + value: objA + }), + { + op: 'replace', + path: '', + value: objB + } + ]); + } }); - it('Replacing an array with an object should be handled well', function() { - - const obj = {}; - var patches = jsonpatch.compare({arr: ['jack']}, {arr: obj}); - expect(patches).toEqual([ - { - op: 'replace', - path: '/arr', - value: obj - } - ]); - + variantIt('Replacing an array with an object should be handled well', [ + ['invertible = FALSE', false], + ['invertible = TRUE', true] + ], function (testInvertible) { + return function () { + const arr = ['jack']; + const obj = {}; + var patches = jsonpatch.compare({ arr: arr }, { arr: obj }, testInvertible); + expect(patches).toEqual([ + ...insertIf(testInvertible, { + op: 'test', + path: '/arr', + value: arr + }), + { + op: 'replace', + path: '/arr', + value: obj + } + ]); + } }); - it('Replacing an array that nested in an object with an object nested in an an object should be handled well', function() { - - const obj = {}; - var patches = jsonpatch.compare({arr: {deeperArray: ['jack']}}, {arr: {deeperArray: obj}}); - - expect(patches).toEqual([ - { - op: 'replace', - path: '/arr/deeperArray', - value: obj - } - ]); - + variantIt('Replacing an array that nested in an object with an object nested in an an object should be handled well', [ + ['invertible = FALSE', false], + ['invertible = TRUE', true] + ], function (testInvertible) { + return function () { + + const arr = ['jack']; + const obj = {}; + var patches = jsonpatch.compare({ arr: { deeperArray: arr } }, { arr: { deeperArray: obj } }, testInvertible); + + expect(patches).toEqual([ + ...insertIf(testInvertible, { + op: 'test', + path: '/arr/deeperArray', + value: arr + }), + { + op: 'replace', + path: '/arr/deeperArray', + value: obj + } + ]); + } }); - it('should return an add for a property that does not exist in the first obj', function() { - var objA = { - user: { - firstName: 'Albert' - } - }; - var objB = { - user: { - firstName: 'Albert', - lastName: 'Einstein' - } - }; + variantIt('should return an add for a property that does not exist in the first obj, without a test operation', [ + ['invertible = FALSE', false], + ['invertible = TRUE', true] + ], function (testInvertible) { + return function () { + var objA = { + user: { + firstName: 'Albert' + } + }; + var objB = { + user: { + firstName: 'Albert', + lastName: 'Einstein' + } + }; - expect(jsonpatch.compare(objA, objB)).toReallyEqual([ - { - op: 'add', - path: '/user/lastName', - value: 'Einstein' - } - ]); + expect(jsonpatch.compare(objA, objB, testInvertible)).toReallyEqual([ + // no test operation, because undefined values are not serialized to JSON + { + op: 'add', + path: '/user/lastName', + value: 'Einstein' + } + ]); + } }); - it('should return a remove for a property that does not exist in the second obj', function() { - var objA = { - user: { - firstName: 'Albert', - lastName: 'Einstein' - } - }; - var objB = { - user: { - firstName: 'Albert' - } - }; + variantIt('should return test and remove for a property that does not exist in the second obj', [ + ['invertible = FALSE', false], + ['invertible = TRUE', true] + ], function (testInvertible) { + return function () { + var objA = { + user: { + firstName: 'Albert', + lastName: 'Einstein' + } + }; + var objB = { + user: { + firstName: 'Albert' + } + }; - expect(jsonpatch.compare(objA, objB)).toReallyEqual([ - { - op: 'remove', - path: '/user/lastName' - } - ]); + expect(jsonpatch.compare(objA, objB, testInvertible)).toReallyEqual([ + ...insertIf(testInvertible, { + op: 'test', + path: '/user/lastName', + value: 'Einstein' + }), + { + op: 'remove', + path: '/user/lastName' + } + ]); + } }); - it('should return a replace for a property that exists in both', function() { - var objA = { - user: { - firstName: 'Albert', - lastName: 'Einstein' - } - }; - var objB = { - user: { - firstName: 'Albert', - lastName: 'Collins' - } - }; + variantIt('should return test and replace for a property that exists in both', [ + ['invertible = FALSE', false], + ['invertible = TRUE', true] + ], function (testInvertible) { + return function () { + var objA = { + user: { + firstName: 'Albert', + lastName: 'Einstein' + } + }; + var objB = { + user: { + firstName: 'Albert', + lastName: 'Collins' + } + }; - expect(jsonpatch.compare(objA, objB)).toReallyEqual([ - { - op: 'replace', - path: '/user/lastName', - value: 'Collins' - } - ]); + expect(jsonpatch.compare(objA, objB, testInvertible)).toReallyEqual([ + ...insertIf(testInvertible, { + op: 'test', + path: '/user/lastName', + value: 'Einstein' + }), + { + op: 'replace', + path: '/user/lastName', + value: 'Collins' + } + ]); + } }); - it('should replace null with object', function() { - var objA = { - user: null - }; - var objB = { - user: {} - }; + variantIt('should replace null with object', [ + ['invertible = FALSE', false], + ['invertible = TRUE', true] + ], function (testInvertible) { + return function () { + var objA = { + user: null + }; + var objB = { + user: {} + }; - expect(jsonpatch.compare(objA, objB)).toReallyEqual([ - { - op: 'replace', - path: '/user', - value: {} - } - ]); - }); + expect(jsonpatch.compare(objA, objB, testInvertible)).toReallyEqual([ + ...insertIf(testInvertible, { + op: 'test', + path: '/user', + value: null + }), + { + op: 'replace', + path: '/user', + value: {} + } + ]); + } + }); - it('should replace object with null', function() { - var objA = { - user: {} - }; - var objB = { - user: null - }; + variantIt('should replace object with null', [ + ['invertible = FALSE', false], + ['invertible = TRUE', true] + ], function (testInvertible) { + return function () { + var objA = { + user: {} + }; + var objB = { + user: null + }; - expect(jsonpatch.compare(objA, objB)).toReallyEqual([ - { - op: 'replace', - path: '/user', - value: null - } - ]); + expect(jsonpatch.compare(objA, objB, testInvertible)).toReallyEqual([ + ...insertIf(testInvertible, { + op: 'test', + path: '/user', + value: {} + }), + { + op: 'replace', + path: '/user', + value: null + } + ]); + } }); - it('should not remove undefined', function() { - var objA = { - user: undefined - }; - var objB = { - user: undefined - }; + variantIt('should not remove undefined', [ + ['invertible = FALSE', false], + ['invertible = TRUE', true] + ], function (testInvertible) { + return function () { + var objA = { + user: undefined + }; + var objB = { + user: undefined + }; - expect(jsonpatch.compare(objA, objB)).toReallyEqual([]); + expect(jsonpatch.compare(objA, objB, testInvertible)).toReallyEqual([]); + } }); - it('should replace 0 with empty string', function() { - var objA = { - user: 0 - }; - var objB = { - user: '' - }; + variantIt('should replace 0 with empty string', [ + ['invertible = FALSE', false], + ['invertible = TRUE', true] + ], function (testInvertible) { + return function () { + var objA = { + user: 0 + }; + var objB = { + user: '' + }; - expect(jsonpatch.compare(objA, objB)).toReallyEqual([ - { - op: 'replace', - path: '/user', - value: '' - } - ]); + expect(jsonpatch.compare(objA, objB, testInvertible)).toReallyEqual([ + ...insertIf(testInvertible, { + op: 'test', + path: '/user', + value: 0 + }), + { + op: 'replace', + path: '/user', + value: '' + } + ]); + }; }); });