Moved all JSON Patch methods into a separate file
This commit is contained in:
parent
12f1543ef9
commit
86f8aa56d0
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
387
src/jsonData.js
387
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
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
}
|
|
@ -633,4 +633,4 @@ div.jsoneditor-code {
|
|||
padding: 0;
|
||||
margin: 0 4px;
|
||||
background: url('img/jsoneditor-icons.svg') -171px -49px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = {
|
|||
'<': ',',
|
||||
'>': '.',
|
||||
'?': '/'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue