diff --git a/src/components/TextMode.js b/src/components/TextMode.js index 906fea7..2a47807 100644 --- a/src/components/TextMode.js +++ b/src/components/TextMode.js @@ -5,7 +5,8 @@ import Ajv from 'ajv' import { parseJSON } from '../utils/jsonUtils' import { escapeUnicodeChars } from '../utils/stringUtils' import { enrichSchemaError, limitErrors } from '../utils/schemaUtils' -import { jsonToData, dataToJson, patchData } from '../jsonData' +import { jsonToData, dataToJson } from '../jsonData' +import { patchData } from '../jsonPatchData' import { createFindKeyBinding } from '../utils/keyBindings' import { KEY_BINDINGS } from '../constants' diff --git a/src/components/TreeMode.js b/src/components/TreeMode.js index 9e2d262..24d8af7 100644 --- a/src/components/TreeMode.js +++ b/src/components/TreeMode.js @@ -9,11 +9,12 @@ import { parseJSON } from '../utils/jsonUtils' import { allButLast } from '../utils/arrayUtils' import { enrichSchemaError } from '../utils/schemaUtils' import { - jsonToData, dataToJson, toDataPath, patchData, pathExists, + jsonToData, dataToJson, toDataPath, pathExists, expand, expandPath, addErrors, search, addSearchResults, nextSearchResult, previousSearchResult, compileJSONPointer } from '../jsonData' +import { patchData } from '../jsonPatchData' import { duplicate, insert, append, remove, changeType, changeValue, changeProperty, sort diff --git a/src/jsonData.js b/src/jsonData.js index 9e5acc2..a966a50 100644 --- a/src/jsonData.js +++ b/src/jsonData.js @@ -5,15 +5,12 @@ * All functions are pure and don't mutate the JSONData. */ -import { setIn, updateIn, getIn, deleteIn, insertAt } from './utils/immutabilityHelpers' +import { setIn, getIn } from './utils/immutabilityHelpers' import { isObject } from './utils/typeUtils' import { last, allButLast } from './utils/arrayUtils' import isEqual from 'lodash/isEqual' -import type { - JSONData, ObjectData, ItemData, DataPointer, Path, - JSONPatch, JSONPatchAction, PatchOptions, JSONPatchResult -} from './types' +import type { JSONData, ObjectData, ItemData, DataPointer, Path } from './types' type RecurseCallback = (value: JSONData, path: Path, root: JSONData) => JSONData @@ -130,382 +127,6 @@ export function toDataPath (data: JSONData, path: Path) : Path { } } -/** - * Apply a patch to a JSONData object - * @param {JSONData} data - * @param {Array} patch A JSON patch - * @param {function(path: Path)} [expand] Optional function to determine - * what nodes must be expanded - * @return {{data: JSONData, revert: Object[], error: Error | null}} - */ -export function patchData (data: JSONData, patch: JSONPatchAction[], expand = expandAll) { - let updatedData = data - let revert = [] - - for (let i = 0; i < patch.length; i++) { - const action = patch[i] - const options = action.jsoneditor - - // TODO: check whether action.op and action.path exist - - switch (action.op) { - case 'add': { - const path = parseJSONPointer(action.path) - const newValue = jsonToData(action.value, expand, path, options && options.type) - const result = add(updatedData, action.path, newValue, options) - updatedData = result.data - revert = result.revert.concat(revert) - - break - } - - case 'remove': { - const result = remove(updatedData, action.path) - updatedData = result.data - revert = result.revert.concat(revert) - - break - } - - case 'replace': { - const path = parseJSONPointer(action.path) - const newValue = jsonToData(action.value, expand, path, options && options.type) - const result = replace(updatedData, path, newValue) - updatedData = result.data - revert = result.revert.concat(revert) - - break - } - - case 'copy': { - if (!action.from) { - return { - data, - revert: [], - error: new Error('Property "from" expected in copy action ' + JSON.stringify(action)) - } - } - - const result = copy(updatedData, action.path, action.from, options) - updatedData = result.data - revert = result.revert.concat(revert) - - break - } - - case 'move': { - if (!action.from) { - return { - data, - revert: [], - error: new Error('Property "from" expected in move action ' + JSON.stringify(action)) - } - } - - const result = move(updatedData, action.path, action.from, options) - updatedData = result.data - revert = result.revert.concat(revert) - - break - } - - case 'test': { - // when a test fails, cancel the whole patch and return the error - const error = test(updatedData, action.path, action.value) - if (error) { - return { data, revert: [], error} - } - - break - } - - default: { - // unknown jsonpatch operation. Cancel the whole patch and return an error - return { - data, - revert: [], - error: new Error('Unknown jsonpatch op ' + JSON.stringify(action.op)) - } - } - } - } - - // TODO: Simplify revert when possible: - // when a previous action takes place on the same path, remove the first - return { - data: updatedData, - revert: simplifyPatch(revert), - error: null - } -} - -/** - * Replace an existing item - * @param {JSONData} data - * @param {Path} path - * @param {JSONData} value - * @return {{data: JSONData, revert: JSONPatch}} - */ -export function replace (data: JSONData, path: Path, value: JSONData) { - const dataPath = toDataPath(data, path) - const oldValue = getIn(data, dataPath) - - return { - data: setIn(data, dataPath, value), - revert: [{ - op: 'replace', - path: compileJSONPointer(path), - value: dataToJson(oldValue), - jsoneditor: { - type: oldValue.type - } - }] - } -} - -/** - * Remove an item or property - * @param {JSONData} data - * @param {string} path - * @return {{data: JSONData, revert: JSONPatch}} - */ -export function remove (data: JSONData, path: string) { - // console.log('remove', path) - const pathArray = parseJSONPointer(path) - - const parentPath = pathArray.slice(0, pathArray.length - 1) - const parent = getIn(data, toDataPath(data, parentPath)) - const dataValue = getIn(data, toDataPath(data, pathArray)) - const value = dataToJson(dataValue) - - if (parent.type === 'Array') { - const dataPath = toDataPath(data, pathArray) - - // remove the 'value' property, we want to remove the whole item from the items array - dataPath.pop() - - return { - data: deleteIn(data, dataPath), - revert: [{ - op: 'add', - path, - value, - jsoneditor: { - type: dataValue.type - } - }] - } - } - else { // object.type === 'Object' - const dataPath = toDataPath(data, pathArray) - const prop = pathArray[pathArray.length - 1] - - // remove the 'value' property, we want to remove the whole object property from props - dataPath.pop() - - return { - data: deleteIn(data, dataPath), - revert: [{ - op: 'add', - path, - value, - jsoneditor: { - type: dataValue.type, - before: findNextProp(parent, prop) - } - }] - } - } -} - -/** - * Remove redundant actions from a JSONPatch array. - * Actions are redundant when they are followed by an action - * acting on the same path. - * @param {JSONPatch} patch - * @return {Array} - */ -export function simplifyPatch(patch: JSONPatch) { - const simplifiedPatch = [] - const paths = {} - - // loop over the patch from last to first - for (let i = patch.length - 1; i >= 0; i--) { - const action = patch[i] - if (action.op === 'test') { - // ignore test actions - simplifiedPatch.unshift(action) - } - else { - // test whether this path was already used - // if not, add this action to the simplified patch - if (paths[action.path] === undefined) { - paths[action.path] = true - simplifiedPatch.unshift(action) - } - } - } - - return simplifiedPatch -} - - -/** - * @param {JSONData} data - * @param {string} path - * @param {JSONData} value - * @param {{before?: string}} [options] - * @param {number} [id] Optional id for the new item - * @return {{data: JSONData, revert: JSONPatch}} - * @private - */ -export function add (data: JSONData, path: string, value: JSONData, options, id = getId()) { - const pathArray = parseJSONPointer(path) - const parentPath = pathArray.slice(0, pathArray.length - 1) - const dataPath = toDataPath(data, parentPath) - const parent = getIn(data, dataPath) - const resolvedPath = resolvePathIndex(data, pathArray) - const prop = resolvedPath[resolvedPath.length - 1] - - let updatedData - if (parent.type === 'Array') { - const newItem = { - id, // TODO: create a unique id within current id's instead of using a global, ever incrementing id - value - } - updatedData = insertAt(data, dataPath.concat('items', prop), newItem) - } - else { // parent.type === 'Object' - updatedData = updateIn(data, dataPath, (object) => { - const existingIndex = findPropertyIndex(object, prop) - if (existingIndex !== -1) { - // replace existing item - return setIn(object, ['props', existingIndex, 'value'], value) - } - else { - // insert new item - const newProp = { id, name: prop, value } - const index = (options && typeof options.before === 'string') - ? findPropertyIndex(object, options.before) // insert before - : object.props.length // append - - return insertAt(object, ['props', index], newProp) - } - }) - } - - if (parent.type === 'Object' && pathExists(data, resolvedPath)) { - const oldValue = getIn(data, toDataPath(data, resolvedPath)) - - return { - data: updatedData, - revert: [{ - op: 'replace', - path: compileJSONPointer(resolvedPath), - value: dataToJson(oldValue), - jsoneditor: { type: oldValue.type } - }] - } - } - else { - return { - data: updatedData, - revert: [{ - op: 'remove', - path: compileJSONPointer(resolvedPath) - }] - } - } -} - -/** - * Copy a value - * @param {JSONData} data - * @param {string} path - * @param {string} from - * @param {{before?: string}} [options] - * @return {{data: JSONData, revert: JSONPatch}} - * @private - */ -export function copy (data: JSONData, path: string, from: string, options) { - const value = getIn(data, toDataPath(data, parseJSONPointer(from))) - - return add(data, path, value, options) -} - -/** - * Move a value - * @param {JSONData} data - * @param {string} path - * @param {string} from - * @param {{before?: string}} [options] - * @return {{data: JSONData, revert: JSONPatch}} - * @private - */ -export function move (data: JSONData, path: string, from: string, options) { - const fromArray = parseJSONPointer(from) - const prop = getIn(data, allButLast(toDataPath(data, fromArray))) - const dataValue = prop.value - const id = prop.id // we want to use the existing id in case the move is a renaming a property - // FIXME: only reuse the existing id when move is renaming a property in the same object - - const parentPathFrom = allButLast(fromArray) - const parent = getIn(data, toDataPath(data, parentPathFrom)) - - const result1 = remove(data, from) - const result2 = add(result1.data, path, dataValue, options, id) - // FIXME: passing id as parameter is ugly, make that redundant (use replace instead of remove/add? (that would give less predictive output :( )) - - const before = result1.revert[0].jsoneditor.before - const beforeNeeded = (parent.type === 'Object' && before) - - if (result2.revert[0].op === 'replace') { - const value = result2.revert[0].value - const type = result2.revert[0].jsoneditor.type - const options = beforeNeeded ? { type, before } : { type } - - return { - data: result2.data, - revert: [ - { op: 'move', from: path, path: from }, - { op: 'add', path, value, jsoneditor: options} - ] - } - } - else { // result2.revert[0].op === 'remove' - return { - data: result2.data, - revert: beforeNeeded - ? [{ op: 'move', from: path, path: from, jsoneditor: {before} }] - : [{ op: 'move', from: path, path: from }] - } - } -} - -/** - * Test whether the data contains the provided value at the specified path. - * Throws an error when the test fails. - * @param {JSONData} data - * @param {string} path - * @param {*} value - * @return {null | Error} Returns an error when the tests, returns null otherwise - */ -export function test (data: JSONData, path: string, value: any) { - if (value === undefined) { - return new Error('Test failed, no value provided') - } - - const pathArray = parseJSONPointer(path) - if (!pathExists(data, pathArray)) { - return new Error('Test failed, path not found') - } - - const actualValue = getIn(data, toDataPath(data, pathArray)) - if (!isEqual(dataToJson(actualValue), value)) { - return new Error('Test failed, value differs') - } -} - type ExpandCallback = (Path) => boolean /** @@ -533,7 +154,7 @@ export function expand (data: JSONData, callback: Path | (Path) => boolean, expa }) } else if (Array.isArray(callback)) { - const dataPath = toDataPath(data, callback) + const dataPath: Path = toDataPath(data, callback) return setIn(data, dataPath.concat(['expanded']), expanded) } @@ -898,7 +519,7 @@ export function compileJSONPointer (path: Path) { * @return {number} */ // TODO: use createUniqueId instead of getId() -function getId () : number { +export function getId () : number { _id++ return _id } diff --git a/src/jsonPatchData.js b/src/jsonPatchData.js new file mode 100644 index 0000000..9941d20 --- /dev/null +++ b/src/jsonPatchData.js @@ -0,0 +1,389 @@ +import isEqual from 'lodash/isEqual' + +import type { + JSONData, Path, + JSONPatch, JSONPatchAction, PatchOptions, JSONPatchResult +} from './types' +import { setIn, updateIn, getIn, deleteIn, insertAt } from './utils/immutabilityHelpers' +import { allButLast } from './utils/arrayUtils' +import { + jsonToData, dataToJson, toDataPath, + parseJSONPointer, compileJSONPointer, + expandAll, pathExists, resolvePathIndex, findPropertyIndex, findNextProp, getId +} from './jsonData' + +/** + * Apply a patch to a JSONData object + * @param {JSONData} data + * @param {Array} patch A JSON patch + * @param {function(path: Path)} [expand] Optional function to determine + * what nodes must be expanded + * @return {{data: JSONData, revert: Object[], error: Error | null}} + */ +export function patchData (data: JSONData, patch: JSONPatchAction[], expand = expandAll) { + let updatedData = data + let revert = [] + + for (let i = 0; i < patch.length; i++) { + const action = patch[i] + const options = action.jsoneditor + + // TODO: check whether action.op and action.path exist + + switch (action.op) { + case 'add': { + const path = parseJSONPointer(action.path) + const newValue = jsonToData(action.value, expand, path, options && options.type) + const result = add(updatedData, action.path, newValue, options) + updatedData = result.data + revert = result.revert.concat(revert) + + break + } + + case 'remove': { + const result = remove(updatedData, action.path) + updatedData = result.data + revert = result.revert.concat(revert) + + break + } + + case 'replace': { + const path = parseJSONPointer(action.path) + const newValue = jsonToData(action.value, expand, path, options && options.type) + const result = replace(updatedData, path, newValue) + updatedData = result.data + revert = result.revert.concat(revert) + + break + } + + case 'copy': { + if (!action.from) { + return { + data, + revert: [], + error: new Error('Property "from" expected in copy action ' + JSON.stringify(action)) + } + } + + const result = copy(updatedData, action.path, action.from, options) + updatedData = result.data + revert = result.revert.concat(revert) + + break + } + + case 'move': { + if (!action.from) { + return { + data, + revert: [], + error: new Error('Property "from" expected in move action ' + JSON.stringify(action)) + } + } + + const result = move(updatedData, action.path, action.from, options) + updatedData = result.data + revert = result.revert.concat(revert) + + break + } + + case 'test': { + // when a test fails, cancel the whole patch and return the error + const error = test(updatedData, action.path, action.value) + if (error) { + return { data, revert: [], error} + } + + break + } + + default: { + // unknown jsonpatch operation. Cancel the whole patch and return an error + return { + data, + revert: [], + error: new Error('Unknown jsonpatch op ' + JSON.stringify(action.op)) + } + } + } + } + + // TODO: Simplify revert when possible: + // when a previous action takes place on the same path, remove the first + return { + data: updatedData, + revert: simplifyPatch(revert), + error: null + } +} + +/** + * Replace an existing item + * @param {JSONData} data + * @param {Path} path + * @param {JSONData} value + * @return {{data: JSONData, revert: JSONPatch}} + */ +export function replace (data: JSONData, path: Path, value: JSONData) { + const dataPath = toDataPath(data, path) + const oldValue = getIn(data, dataPath) + + return { + data: setIn(data, dataPath, value), + revert: [{ + op: 'replace', + path: compileJSONPointer(path), + value: dataToJson(oldValue), + jsoneditor: { + type: oldValue.type + } + }] + } +} + +/** + * Remove an item or property + * @param {JSONData} data + * @param {string} path + * @return {{data: JSONData, revert: JSONPatch}} + */ +export function remove (data: JSONData, path: string) { + // console.log('remove', path) + const pathArray = parseJSONPointer(path) + + const parentPath = pathArray.slice(0, pathArray.length - 1) + const parent = getIn(data, toDataPath(data, parentPath)) + const dataValue = getIn(data, toDataPath(data, pathArray)) + const value = dataToJson(dataValue) + + if (parent.type === 'Array') { + const dataPath = toDataPath(data, pathArray) + + // remove the 'value' property, we want to remove the whole item from the items array + dataPath.pop() + + return { + data: deleteIn(data, dataPath), + revert: [{ + op: 'add', + path, + value, + jsoneditor: { + type: dataValue.type + } + }] + } + } + else { // object.type === 'Object' + const dataPath = toDataPath(data, pathArray) + const prop = pathArray[pathArray.length - 1] + + // remove the 'value' property, we want to remove the whole object property from props + dataPath.pop() + + return { + data: deleteIn(data, dataPath), + revert: [{ + op: 'add', + path, + value, + jsoneditor: { + type: dataValue.type, + before: findNextProp(parent, prop) + } + }] + } + } +} + +/** + * Remove redundant actions from a JSONPatch array. + * Actions are redundant when they are followed by an action + * acting on the same path. + * @param {JSONPatch} patch + * @return {Array} + */ +function simplifyPatch(patch: JSONPatch) { + const simplifiedPatch = [] + const paths = {} + + // loop over the patch from last to first + for (let i = patch.length - 1; i >= 0; i--) { + const action = patch[i] + if (action.op === 'test') { + // ignore test actions + simplifiedPatch.unshift(action) + } + else { + // test whether this path was already used + // if not, add this action to the simplified patch + if (paths[action.path] === undefined) { + paths[action.path] = true + simplifiedPatch.unshift(action) + } + } + } + + return simplifiedPatch +} + + +/** + * @param {JSONData} data + * @param {string} path + * @param {JSONData} value + * @param {{before?: string}} [options] + * @param {number} [id] Optional id for the new item + * @return {{data: JSONData, revert: JSONPatch}} + * @private + */ +export function add (data: JSONData, path: string, value: JSONData, options, id = getId()) { + const pathArray = parseJSONPointer(path) + const parentPath = pathArray.slice(0, pathArray.length - 1) + const dataPath = toDataPath(data, parentPath) + const parent = getIn(data, dataPath) + const resolvedPath = resolvePathIndex(data, pathArray) + const prop = resolvedPath[resolvedPath.length - 1] + + let updatedData + if (parent.type === 'Array') { + const newItem = { + id, // TODO: create a unique id within current id's instead of using a global, ever incrementing id + value + } + updatedData = insertAt(data, dataPath.concat('items', prop), newItem) + } + else { // parent.type === 'Object' + updatedData = updateIn(data, dataPath, (object) => { + const existingIndex = findPropertyIndex(object, prop) + if (existingIndex !== -1) { + // replace existing item + return setIn(object, ['props', existingIndex, 'value'], value) + } + else { + // insert new item + const newProp = { id, name: prop, value } + const index = (options && typeof options.before === 'string') + ? findPropertyIndex(object, options.before) // insert before + : object.props.length // append + + return insertAt(object, ['props', index], newProp) + } + }) + } + + if (parent.type === 'Object' && pathExists(data, resolvedPath)) { + const oldValue = getIn(data, toDataPath(data, resolvedPath)) + + return { + data: updatedData, + revert: [{ + op: 'replace', + path: compileJSONPointer(resolvedPath), + value: dataToJson(oldValue), + jsoneditor: { type: oldValue.type } + }] + } + } + else { + return { + data: updatedData, + revert: [{ + op: 'remove', + path: compileJSONPointer(resolvedPath) + }] + } + } +} + +/** + * Copy a value + * @param {JSONData} data + * @param {string} path + * @param {string} from + * @param {{before?: string}} [options] + * @return {{data: JSONData, revert: JSONPatch}} + * @private + */ +export function copy (data: JSONData, path: string, from: string, options) { + const value = getIn(data, toDataPath(data, parseJSONPointer(from))) + + return add(data, path, value, options) +} + +/** + * Move a value + * @param {JSONData} data + * @param {string} path + * @param {string} from + * @param {{before?: string}} [options] + * @return {{data: JSONData, revert: JSONPatch}} + * @private + */ +export function move (data: JSONData, path: string, from: string, options) { + const fromArray = parseJSONPointer(from) + const prop = getIn(data, allButLast(toDataPath(data, fromArray))) + const dataValue = prop.value + const id = prop.id // we want to use the existing id in case the move is a renaming a property + // FIXME: only reuse the existing id when move is renaming a property in the same object + + const parentPathFrom = allButLast(fromArray) + const parent = getIn(data, toDataPath(data, parentPathFrom)) + + const result1 = remove(data, from) + const result2 = add(result1.data, path, dataValue, options, id) + // FIXME: passing id as parameter is ugly, make that redundant (use replace instead of remove/add? (that would give less predictive output :( )) + + const before = result1.revert[0].jsoneditor.before + const beforeNeeded = (parent.type === 'Object' && before) + + if (result2.revert[0].op === 'replace') { + const value = result2.revert[0].value + const type = result2.revert[0].jsoneditor.type + const options = beforeNeeded ? { type, before } : { type } + + return { + data: result2.data, + revert: [ + { op: 'move', from: path, path: from }, + { op: 'add', path, value, jsoneditor: options} + ] + } + } + else { // result2.revert[0].op === 'remove' + return { + data: result2.data, + revert: beforeNeeded + ? [{ op: 'move', from: path, path: from, jsoneditor: {before} }] + : [{ op: 'move', from: path, path: from }] + } + } +} + +/** + * Test whether the data contains the provided value at the specified path. + * Throws an error when the test fails. + * @param {JSONData} data + * @param {string} path + * @param {*} value + * @return {null | Error} Returns an error when the tests, returns null otherwise + */ +export function test (data: JSONData, path: string, value: any) { + if (value === undefined) { + return new Error('Test failed, no value provided') + } + + const pathArray = parseJSONPointer(path) + if (!pathExists(data, pathArray)) { + return new Error('Test failed, path not found') + } + + const actualValue = getIn(data, toDataPath(data, pathArray)) + if (!isEqual(dataToJson(actualValue), value)) { + return new Error('Test failed, value differs') + } +} diff --git a/src/jsoneditor.less b/src/jsoneditor.less index fe31ec5..70034dd 100644 --- a/src/jsoneditor.less +++ b/src/jsoneditor.less @@ -633,4 +633,4 @@ div.jsoneditor-code { padding: 0; margin: 0 4px; background: url('img/jsoneditor-icons.svg') -171px -49px; -} \ No newline at end of file +} diff --git a/src/utils/keyBindings.js b/src/utils/keyBindings.js index 1bf69f0..3de5594 100644 --- a/src/utils/keyBindings.js +++ b/src/utils/keyBindings.js @@ -53,7 +53,6 @@ export function createFindKeyBinding (keyBindings) { return function findKeyBinding (event) { const keyCombo = keyComboFromEvent(event) - console.log('keyCombo', keyCombo) return keyCombos[normalizeKeyCombo(keyCombo)] || null } @@ -213,4 +212,4 @@ const aliases = { '<': ',', '>': '.', '?': '/' -} \ No newline at end of file +} diff --git a/test/jsonData.test.js b/test/jsonData.test.js index a54dc38..010b649 100644 --- a/test/jsonData.test.js +++ b/test/jsonData.test.js @@ -1,6 +1,6 @@ import test from 'ava'; import { - jsonToData, dataToJson, patchData, pathExists, transform, traverse, + jsonToData, dataToJson, pathExists, transform, traverse, parseJSONPointer, compileJSONPointer, expand, addErrors, search, addSearchResults, nextSearchResult, previousSearchResult } from '../src/jsonData' @@ -586,489 +586,6 @@ test('compileJSONPointer', t => { t.deepEqual(compileJSONPointer([]), '') }) -test('jsonpatch add', t => { - const json = { - arr: [1,2,3], - obj: {a : 2} - } - - const patch = [ - {op: 'add', path: '/obj/b', value: {foo: 'bar'}} - ] - - const data = jsonToData(json) - const result = patchData(data, patch) - const patchedData = result.data - const revert = result.revert - const patchedJson = dataToJson(patchedData) - - t.deepEqual(patchedJson, { - arr: [1,2,3], - obj: {a : 2, b: {foo: 'bar'}} - }) - t.deepEqual(revert, [ - {op: 'remove', path: '/obj/b'} - ]) -}) - -test('jsonpatch add: append to matrix', t => { - const json = { - arr: [1,2,3], - obj: {a : 2} - } - - const patch = [ - {op: 'add', path: '/arr/-', value: 4} - ] - - const data = jsonToData(json) - const result = patchData(data, patch) - const patchedData = result.data - const revert = result.revert - const patchedJson = dataToJson(patchedData) - - t.deepEqual(patchedJson, { - arr: [1,2,3,4], - obj: {a : 2} - }) - t.deepEqual(revert, [ - {op: 'remove', path: '/arr/3'} - ]) -}) - - -test('jsonpatch remove', t => { - const json = { - arr: [1,2,3], - obj: {a : 4} - } - - const patch = [ - {op: 'remove', path: '/obj/a'}, - {op: 'remove', path: '/arr/1'}, - ] - - const data = jsonToData(json) - const result = patchData(data, patch) - const patchedData = result.data - const revert = result.revert - const patchedJson = dataToJson(patchedData) - - t.deepEqual(patchedJson, { - arr: [1,3], - obj: {} - }) - t.deepEqual(revert, [ - {op: 'add', path: '/arr/1', value: 2, jsoneditor: {type: 'value'}}, - {op: 'add', path: '/obj/a', value: 4, jsoneditor: {type: 'value', before: null}} - ]) - - // test revert - const data2 = jsonToData(patchedJson) - const result2 = patchData(data2, revert) - const patchedData2 = result2.data - const revert2 = result2.revert - const patchedJson2 = dataToJson(patchedData2) - - t.deepEqual(patchedJson2, json) - t.deepEqual(revert2, patch) -}) - -test('jsonpatch replace', t => { - const json = { - arr: [1,2,3], - obj: {a : 4} - } - - const patch = [ - {op: 'replace', path: '/obj/a', value: 400}, - {op: 'replace', path: '/arr/1', value: 200}, - ] - - const data = jsonToData(json) - const result = patchData(data, patch) - const patchedData = result.data - const revert = result.revert - const patchedJson = dataToJson(patchedData) - - t.deepEqual(patchedJson, { - arr: [1,200,3], - obj: {a: 400} - }) - t.deepEqual(revert, [ - {op: 'replace', path: '/arr/1', value: 2, jsoneditor: {type: 'value'}}, - {op: 'replace', path: '/obj/a', value: 4, jsoneditor: {type: 'value'}} - ]) - - // test revert - const data2 = jsonToData(patchedJson) - const result2 = patchData(data2, revert) - const patchedData2 = result2.data - const revert2 = result2.revert - const patchedJson2 = dataToJson(patchedData2) - - t.deepEqual(patchedJson2, json) - t.deepEqual(revert2, [ - {op: 'replace', path: '/obj/a', value: 400, jsoneditor: {type: 'value'}}, - {op: 'replace', path: '/arr/1', value: 200, jsoneditor: {type: 'value'}} - ]) -}) - -test('jsonpatch replace (keep ids intact)', t => { - const json = { value: 42 } - const patch = [ - {op: 'replace', path: '/value', value: 100} - ] - - const data = jsonToData(json) - const valueId = data.props[0].id - - const patchedData = patchData(data, patch).data - const patchedValueId = patchedData.props[0].id - - t.is(patchedValueId, valueId) -}) - -test('jsonpatch copy', t => { - const json = { - arr: [1,2,3], - obj: {a : 4} - } - - const patch = [ - {op: 'copy', from: '/obj', path: '/arr/2'}, - ] - - const data = jsonToData(json) - const result = patchData(data, patch) - const patchedData = result.data - const revert = result.revert - const patchedJson = dataToJson(patchedData) - - t.deepEqual(patchedJson, { - arr: [1, 2, {a:4}, 3], - obj: {a: 4} - }) - t.deepEqual(revert, [ - {op: 'remove', path: '/arr/2'} - ]) - - // test revert - const data2 = jsonToData(patchedJson) - const result2 = patchData(data2, revert) - const patchedData2 = result2.data - const revert2 = result2.revert - const patchedJson2 = dataToJson(patchedData2) - - t.deepEqual(patchedJson2, json) - t.deepEqual(revert2, [ - {op: 'add', path: '/arr/2', value: {a: 4}, jsoneditor: {type: 'Object'}} - ]) -}) - -test('jsonpatch copy (keeps the same ids)', t => { - const json = { foo: { bar: 42 } } - const patch = [ - {op: 'copy', from: '/foo', path: '/copied'} - ] - - const data = jsonToData(json) - const fooId = data.props[0].id - const barId = data.props[0].value.props[0].id - - const patchedData = patchData(data, patch).data - const patchedFooId = patchedData.props[0].id - const patchedBarId = patchedData.props[0].value.props[0].id - const copiedId = patchedData.props[1].id - const patchedCopiedBarId = patchedData.props[1].value.props[0].id - - t.is(patchedData.props[0].name, 'foo') - t.is(patchedData.props[1].name, 'copied') - - t.is(patchedFooId, fooId, 'same foo id') - t.is(patchedBarId, barId, 'same bar id') - - t.not(copiedId, fooId, 'different id of property copied') - - // The id's of the copied childs are the same, that's okish, they will not bite each other - // FIXME: better solution for id's either always unique, or unique per object/array - t.is(patchedCopiedBarId, patchedBarId, 'same copied bar id') -}) - -test('jsonpatch move', t => { - const json = { - arr: [1,2,3], - obj: {a : 4} - } - - const patch = [ - {op: 'move', from: '/obj', path: '/arr/2'}, - ] - - const data = jsonToData(json) - const result = patchData(data, patch) - const patchedData = result.data - const revert = result.revert - const patchedJson = dataToJson(patchedData) - - t.is(result.error, null) - t.deepEqual(patchedJson, { - arr: [1, 2, {a:4}, 3] - }) - t.deepEqual(revert, [ - {op: 'move', from: '/arr/2', path: '/obj'} - ]) - - // test revert - const data2 = jsonToData(patchedJson) - const result2 = patchData(data2, revert) - const patchedData2 = result2.data - const revert2 = result2.revert - const patchedJson2 = dataToJson(patchedData2) - - t.deepEqual(patchedJson2, json) - t.deepEqual(revert2, patch) -}) - -test('jsonpatch move before', t => { - const json = { - arr: [1,2,3], - obj: {a : 4}, - zzz: 'zzz' - } - - const patch = [ - {op: 'move', from: '/obj', path: '/arr/2'}, - ] - - const data = jsonToData(json) - const result = patchData(data, patch) - const patchedData = result.data - const revert = result.revert - const patchedJson = dataToJson(patchedData) - - t.is(result.error, null) - t.deepEqual(patchedJson, { - arr: [1, 2, {a:4}, 3], - zzz: 'zzz' - }) - t.deepEqual(revert, [ - {op: 'move', from: '/arr/2', path: '/obj', jsoneditor: {before: 'zzz'}} - ]) - - // test revert - const data2 = jsonToData(patchedJson) - const result2 = patchData(data2, revert) - const patchedData2 = result2.data - const revert2 = result2.revert - const patchedJson2 = dataToJson(patchedData2) - - t.deepEqual(patchedJson2, json) - t.deepEqual(revert2, patch) -}) - -test('jsonpatch move and replace', t => { - const json = { a: 2, b: 3 } - - const patch = [ - {op: 'move', from: '/a', path: '/b'}, - ] - - const data = jsonToData(json) - - const result = patchData(data, patch) - const patchedData = result.data - const revert = result.revert - const patchedJson = dataToJson(patchedData) - - // id of the replaced B must be kept intact - t.is(patchedData.props[0].id, data.props[1].id) - - replaceIds(patchedData) - t.deepEqual(patchedData, { - "type": "Object", - "expanded": true, - "props": [ - { - "id": "[ID]", - "name": "b", - "value": { - "type": "value", - "value": 2 - } - } - ] - }) - - t.deepEqual(patchedJson, { b : 2 }) - t.deepEqual(revert, [ - {op:'move', from: '/b', path: '/a'}, - {op:'add', path:'/b', value: 3, jsoneditor: {type: 'value', before: 'b'}} - ]) - - // test revert - const data2 = jsonToData(patchedJson) - const result2 = patchData(data2, revert) - const patchedData2 = result2.data - const revert2 = result2.revert - const patchedJson2 = dataToJson(patchedData2) - - t.deepEqual(patchedJson2, json) - t.deepEqual(revert2, [ - {op: 'move', from: '/a', path: '/b'} - ]) -}) - -test('jsonpatch move and replace (nested)', t => { - const json = { - arr: [1,2,3], - obj: {a : 4} - } - - const patch = [ - {op: 'move', from: '/obj', path: '/arr'}, - ] - - const data = jsonToData(json) - const result = patchData(data, patch) - const patchedData = result.data - const revert = result.revert - const patchedJson = dataToJson(patchedData) - - t.deepEqual(patchedJson, { - arr: {a:4} - }) - t.deepEqual(revert, [ - {op:'move', from: '/arr', path: '/obj'}, - {op:'add', path:'/arr', value: [1,2,3], jsoneditor: {type: 'Array'}} - ]) - - // test revert - const data2 = jsonToData(patchedJson) - const result2 = patchData(data2, revert) - const patchedData2 = result2.data - const revert2 = result2.revert - const patchedJson2 = dataToJson(patchedData2) - - t.deepEqual(patchedJson2, json) - t.deepEqual(revert2, [ - {op: 'move', from: '/obj', path: '/arr'} - ]) -}) - -test('jsonpatch move (keep id intact)', t => { - const json = { value: 42 } - const patch = [ - {op: 'move', from: '/value', path: '/moved'} - ] - - const data = jsonToData(json) - const valueId = data.props[0].id - - const patchedData = patchData(data, patch).data - const patchedValueId = patchedData.props[0].id - - t.is(patchedValueId, valueId) -}) - -test('jsonpatch move and replace (keep ids intact)', t => { - const json = { a: 2, b: 3 } - const patch = [ - {op: 'move', from: '/a', path: '/b'} - ] - - const data = jsonToData(json) - const bId = data.props[1].id - - t.is(data.props[0].name, 'a') - t.is(data.props[1].name, 'b') - - const patchedData = patchData(data, patch).data - - t.is(patchedData.props[0].name, 'b') - t.is(patchedData.props[0].id, bId) -}) - -test('jsonpatch test (ok)', t => { - const json = { - arr: [1,2,3], - obj: {a : 4} - } - - const patch = [ - {op: 'test', path: '/arr', value: [1,2,3]}, - {op: 'add', path: '/added', value: 'ok'} - ] - - const data = jsonToData(json) - const result = patchData(data, patch) - const patchedData = result.data - const revert = result.revert - const patchedJson = dataToJson(patchedData) - - t.deepEqual(patchedJson, { - arr: [1,2,3], - obj: {a : 4}, - added: 'ok' - }) - t.deepEqual(revert, [ - {op: 'remove', path: '/added'} - ]) - -}) - -test('jsonpatch test (fail: path not found)', t => { - const json = { - arr: [1,2,3], - obj: {a : 4} - } - - const patch = [ - {op: 'test', path: '/arr/5', value: [1,2,3]}, - {op: 'add', path: '/added', value: 'ok'} - ] - - const data = jsonToData(json) - const result = patchData(data, patch) - const patchedData = result.data - const revert = result.revert - const patchedJson = dataToJson(patchedData) - - // patch shouldn't be applied - t.deepEqual(patchedJson, { - arr: [1,2,3], - obj: {a : 4} - }) - t.deepEqual(revert, []) - t.is(result.error.toString(), 'Error: Test failed, path not found') -}) - -test('jsonpatch test (fail: value not equal)', t => { - const json = { - arr: [1,2,3], - obj: {a : 4} - } - - const patch = [ - {op: 'test', path: '/obj', value: {a:4, b: 6}}, - {op: 'add', path: '/added', value: 'ok'} - ] - - const data = jsonToData(json) - const result = patchData(data, patch) - const patchedData = result.data - const revert = result.revert - const patchedJson = dataToJson(patchedData) - - // patch shouldn't be applied - t.deepEqual(patchedJson, { - arr: [1,2,3], - obj: {a : 4} - }) - t.deepEqual(revert, []) - t.is(result.error.toString(), 'Error: Test failed, value differs') -}) - test('add and remove errors', t => { const dataWithErrors = addErrors(JSON_DATA_EXAMPLE, JSON_SCHEMA_ERRORS) t.deepEqual(dataWithErrors, JSON_DATA_EXAMPLE_ERRORS) @@ -1240,4 +757,4 @@ function printJSON (json, message = null) { console.log(message) } console.log(JSON.stringify(json, null, 2)) -} \ No newline at end of file +} diff --git a/test/jsonPatchData.test.js b/test/jsonPatchData.test.js new file mode 100644 index 0000000..de4f338 --- /dev/null +++ b/test/jsonPatchData.test.js @@ -0,0 +1,503 @@ +import test from 'ava'; +import { jsonToData, dataToJson } from '../src/jsonData' +import { patchData } from '../src/jsonPatchData' + +test('jsonpatch add', t => { + const json = { + arr: [1,2,3], + obj: {a : 2} + } + + const patch = [ + {op: 'add', path: '/obj/b', value: {foo: 'bar'}} + ] + + const data = jsonToData(json) + const result = patchData(data, patch) + const patchedData = result.data + const revert = result.revert + const patchedJson = dataToJson(patchedData) + + t.deepEqual(patchedJson, { + arr: [1,2,3], + obj: {a : 2, b: {foo: 'bar'}} + }) + t.deepEqual(revert, [ + {op: 'remove', path: '/obj/b'} + ]) +}) + +test('jsonpatch add: append to matrix', t => { + const json = { + arr: [1,2,3], + obj: {a : 2} + } + + const patch = [ + {op: 'add', path: '/arr/-', value: 4} + ] + + const data = jsonToData(json) + const result = patchData(data, patch) + const patchedData = result.data + const revert = result.revert + const patchedJson = dataToJson(patchedData) + + t.deepEqual(patchedJson, { + arr: [1,2,3,4], + obj: {a : 2} + }) + t.deepEqual(revert, [ + {op: 'remove', path: '/arr/3'} + ]) +}) + + +test('jsonpatch remove', t => { + const json = { + arr: [1,2,3], + obj: {a : 4} + } + + const patch = [ + {op: 'remove', path: '/obj/a'}, + {op: 'remove', path: '/arr/1'}, + ] + + const data = jsonToData(json) + const result = patchData(data, patch) + const patchedData = result.data + const revert = result.revert + const patchedJson = dataToJson(patchedData) + + t.deepEqual(patchedJson, { + arr: [1,3], + obj: {} + }) + t.deepEqual(revert, [ + {op: 'add', path: '/arr/1', value: 2, jsoneditor: {type: 'value'}}, + {op: 'add', path: '/obj/a', value: 4, jsoneditor: {type: 'value', before: null}} + ]) + + // test revert + const data2 = jsonToData(patchedJson) + const result2 = patchData(data2, revert) + const patchedData2 = result2.data + const revert2 = result2.revert + const patchedJson2 = dataToJson(patchedData2) + + t.deepEqual(patchedJson2, json) + t.deepEqual(revert2, patch) +}) + +test('jsonpatch replace', t => { + const json = { + arr: [1,2,3], + obj: {a : 4} + } + + const patch = [ + {op: 'replace', path: '/obj/a', value: 400}, + {op: 'replace', path: '/arr/1', value: 200}, + ] + + const data = jsonToData(json) + const result = patchData(data, patch) + const patchedData = result.data + const revert = result.revert + const patchedJson = dataToJson(patchedData) + + t.deepEqual(patchedJson, { + arr: [1,200,3], + obj: {a: 400} + }) + t.deepEqual(revert, [ + {op: 'replace', path: '/arr/1', value: 2, jsoneditor: {type: 'value'}}, + {op: 'replace', path: '/obj/a', value: 4, jsoneditor: {type: 'value'}} + ]) + + // test revert + const data2 = jsonToData(patchedJson) + const result2 = patchData(data2, revert) + const patchedData2 = result2.data + const revert2 = result2.revert + const patchedJson2 = dataToJson(patchedData2) + + t.deepEqual(patchedJson2, json) + t.deepEqual(revert2, [ + {op: 'replace', path: '/obj/a', value: 400, jsoneditor: {type: 'value'}}, + {op: 'replace', path: '/arr/1', value: 200, jsoneditor: {type: 'value'}} + ]) +}) + +test('jsonpatch replace (keep ids intact)', t => { + const json = { value: 42 } + const patch = [ + {op: 'replace', path: '/value', value: 100} + ] + + const data = jsonToData(json) + const valueId = data.props[0].id + + const patchedData = patchData(data, patch).data + const patchedValueId = patchedData.props[0].id + + t.is(patchedValueId, valueId) +}) + +test('jsonpatch copy', t => { + const json = { + arr: [1,2,3], + obj: {a : 4} + } + + const patch = [ + {op: 'copy', from: '/obj', path: '/arr/2'}, + ] + + const data = jsonToData(json) + const result = patchData(data, patch) + const patchedData = result.data + const revert = result.revert + const patchedJson = dataToJson(patchedData) + + t.deepEqual(patchedJson, { + arr: [1, 2, {a:4}, 3], + obj: {a: 4} + }) + t.deepEqual(revert, [ + {op: 'remove', path: '/arr/2'} + ]) + + // test revert + const data2 = jsonToData(patchedJson) + const result2 = patchData(data2, revert) + const patchedData2 = result2.data + const revert2 = result2.revert + const patchedJson2 = dataToJson(patchedData2) + + t.deepEqual(patchedJson2, json) + t.deepEqual(revert2, [ + {op: 'add', path: '/arr/2', value: {a: 4}, jsoneditor: {type: 'Object'}} + ]) +}) + +test('jsonpatch copy (keeps the same ids)', t => { + const json = { foo: { bar: 42 } } + const patch = [ + {op: 'copy', from: '/foo', path: '/copied'} + ] + + const data = jsonToData(json) + const fooId = data.props[0].id + const barId = data.props[0].value.props[0].id + + const patchedData = patchData(data, patch).data + const patchedFooId = patchedData.props[0].id + const patchedBarId = patchedData.props[0].value.props[0].id + const copiedId = patchedData.props[1].id + const patchedCopiedBarId = patchedData.props[1].value.props[0].id + + t.is(patchedData.props[0].name, 'foo') + t.is(patchedData.props[1].name, 'copied') + + t.is(patchedFooId, fooId, 'same foo id') + t.is(patchedBarId, barId, 'same bar id') + + t.not(copiedId, fooId, 'different id of property copied') + + // The id's of the copied childs are the same, that's okish, they will not bite each other + // FIXME: better solution for id's either always unique, or unique per object/array + t.is(patchedCopiedBarId, patchedBarId, 'same copied bar id') +}) + +test('jsonpatch move', t => { + const json = { + arr: [1,2,3], + obj: {a : 4} + } + + const patch = [ + {op: 'move', from: '/obj', path: '/arr/2'}, + ] + + const data = jsonToData(json) + const result = patchData(data, patch) + const patchedData = result.data + const revert = result.revert + const patchedJson = dataToJson(patchedData) + + t.is(result.error, null) + t.deepEqual(patchedJson, { + arr: [1, 2, {a:4}, 3] + }) + t.deepEqual(revert, [ + {op: 'move', from: '/arr/2', path: '/obj'} + ]) + + // test revert + const data2 = jsonToData(patchedJson) + const result2 = patchData(data2, revert) + const patchedData2 = result2.data + const revert2 = result2.revert + const patchedJson2 = dataToJson(patchedData2) + + t.deepEqual(patchedJson2, json) + t.deepEqual(revert2, patch) +}) + +test('jsonpatch move before', t => { + const json = { + arr: [1,2,3], + obj: {a : 4}, + zzz: 'zzz' + } + + const patch = [ + {op: 'move', from: '/obj', path: '/arr/2'}, + ] + + const data = jsonToData(json) + const result = patchData(data, patch) + const patchedData = result.data + const revert = result.revert + const patchedJson = dataToJson(patchedData) + + t.is(result.error, null) + t.deepEqual(patchedJson, { + arr: [1, 2, {a:4}, 3], + zzz: 'zzz' + }) + t.deepEqual(revert, [ + {op: 'move', from: '/arr/2', path: '/obj', jsoneditor: {before: 'zzz'}} + ]) + + // test revert + const data2 = jsonToData(patchedJson) + const result2 = patchData(data2, revert) + const patchedData2 = result2.data + const revert2 = result2.revert + const patchedJson2 = dataToJson(patchedData2) + + t.deepEqual(patchedJson2, json) + t.deepEqual(revert2, patch) +}) + +test('jsonpatch move and replace', t => { + const json = { a: 2, b: 3 } + + const patch = [ + {op: 'move', from: '/a', path: '/b'}, + ] + + const data = jsonToData(json) + + const result = patchData(data, patch) + const patchedData = result.data + const revert = result.revert + const patchedJson = dataToJson(patchedData) + + // id of the replaced B must be kept intact + t.is(patchedData.props[0].id, data.props[1].id) + + replaceIds(patchedData) + t.deepEqual(patchedData, { + "type": "Object", + "expanded": true, + "props": [ + { + "id": "[ID]", + "name": "b", + "value": { + "type": "value", + "value": 2 + } + } + ] + }) + + t.deepEqual(patchedJson, { b : 2 }) + t.deepEqual(revert, [ + {op:'move', from: '/b', path: '/a'}, + {op:'add', path:'/b', value: 3, jsoneditor: {type: 'value', before: 'b'}} + ]) + + // test revert + const data2 = jsonToData(patchedJson) + const result2 = patchData(data2, revert) + const patchedData2 = result2.data + const revert2 = result2.revert + const patchedJson2 = dataToJson(patchedData2) + + t.deepEqual(patchedJson2, json) + t.deepEqual(revert2, [ + {op: 'move', from: '/a', path: '/b'} + ]) +}) + +test('jsonpatch move and replace (nested)', t => { + const json = { + arr: [1,2,3], + obj: {a : 4} + } + + const patch = [ + {op: 'move', from: '/obj', path: '/arr'}, + ] + + const data = jsonToData(json) + const result = patchData(data, patch) + const patchedData = result.data + const revert = result.revert + const patchedJson = dataToJson(patchedData) + + t.deepEqual(patchedJson, { + arr: {a:4} + }) + t.deepEqual(revert, [ + {op:'move', from: '/arr', path: '/obj'}, + {op:'add', path:'/arr', value: [1,2,3], jsoneditor: {type: 'Array'}} + ]) + + // test revert + const data2 = jsonToData(patchedJson) + const result2 = patchData(data2, revert) + const patchedData2 = result2.data + const revert2 = result2.revert + const patchedJson2 = dataToJson(patchedData2) + + t.deepEqual(patchedJson2, json) + t.deepEqual(revert2, [ + {op: 'move', from: '/obj', path: '/arr'} + ]) +}) + +test('jsonpatch move (keep id intact)', t => { + const json = { value: 42 } + const patch = [ + {op: 'move', from: '/value', path: '/moved'} + ] + + const data = jsonToData(json) + const valueId = data.props[0].id + + const patchedData = patchData(data, patch).data + const patchedValueId = patchedData.props[0].id + + t.is(patchedValueId, valueId) +}) + +test('jsonpatch move and replace (keep ids intact)', t => { + const json = { a: 2, b: 3 } + const patch = [ + {op: 'move', from: '/a', path: '/b'} + ] + + const data = jsonToData(json) + const bId = data.props[1].id + + t.is(data.props[0].name, 'a') + t.is(data.props[1].name, 'b') + + const patchedData = patchData(data, patch).data + + t.is(patchedData.props[0].name, 'b') + t.is(patchedData.props[0].id, bId) +}) + +test('jsonpatch test (ok)', t => { + const json = { + arr: [1,2,3], + obj: {a : 4} + } + + const patch = [ + {op: 'test', path: '/arr', value: [1,2,3]}, + {op: 'add', path: '/added', value: 'ok'} + ] + + const data = jsonToData(json) + const result = patchData(data, patch) + const patchedData = result.data + const revert = result.revert + const patchedJson = dataToJson(patchedData) + + t.deepEqual(patchedJson, { + arr: [1,2,3], + obj: {a : 4}, + added: 'ok' + }) + t.deepEqual(revert, [ + {op: 'remove', path: '/added'} + ]) + +}) + +test('jsonpatch test (fail: path not found)', t => { + const json = { + arr: [1,2,3], + obj: {a : 4} + } + + const patch = [ + {op: 'test', path: '/arr/5', value: [1,2,3]}, + {op: 'add', path: '/added', value: 'ok'} + ] + + const data = jsonToData(json) + const result = patchData(data, patch) + const patchedData = result.data + const revert = result.revert + const patchedJson = dataToJson(patchedData) + + // patch shouldn't be applied + t.deepEqual(patchedJson, { + arr: [1,2,3], + obj: {a : 4} + }) + t.deepEqual(revert, []) + t.is(result.error.toString(), 'Error: Test failed, path not found') +}) + +test('jsonpatch test (fail: value not equal)', t => { + const json = { + arr: [1,2,3], + obj: {a : 4} + } + + const patch = [ + {op: 'test', path: '/obj', value: {a:4, b: 6}}, + {op: 'add', path: '/added', value: 'ok'} + ] + + const data = jsonToData(json) + const result = patchData(data, patch) + const patchedData = result.data + const revert = result.revert + const patchedJson = dataToJson(patchedData) + + // patch shouldn't be applied + t.deepEqual(patchedJson, { + arr: [1,2,3], + obj: {a : 4} + }) + t.deepEqual(revert, []) + t.is(result.error.toString(), 'Error: Test failed, value differs') +}) + +// helper function to replace all id properties with a constant value +function replaceIds (data, value = '[ID]') { + if (data.type === 'Object') { + data.props.forEach(prop => { + prop.id = value + replaceIds(prop.value, value) + }) + } + + if (data.type === 'Array') { + data.items.forEach(item => { + item.id = value + replaceIds(item.value, value) + }) + } +}