Moved all JSON Patch methods into a separate file

This commit is contained in:
jos 2017-09-08 11:14:41 +02:00
parent 12f1543ef9
commit 86f8aa56d0
8 changed files with 904 additions and 873 deletions

View File

@ -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'

View File

@ -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

View File

@ -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
}

389
src/jsonPatchData.js Normal file
View File

@ -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')
}
}

View File

@ -633,4 +633,4 @@ div.jsoneditor-code {
padding: 0;
margin: 0 4px;
background: url('img/jsoneditor-icons.svg') -171px -49px;
}
}

View File

@ -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 = {
'<': ',',
'>': '.',
'?': '/'
}
}

View File

@ -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))
}
}

503
test/jsonPatchData.test.js Normal file
View File

@ -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)
})
}
}