Implemented jsonpatch operations

This commit is contained in:
jos 2016-09-09 11:01:06 +02:00
parent 94671507b5
commit fe0f98dfe0
6 changed files with 704 additions and 90 deletions

View File

@ -26,6 +26,7 @@
"ajv": "4.5.0",
"brace": "0.8.0",
"javascript-natural-sort": "0.7.1",
"lodash": "4.15.0",
"preact": "5.7.0"
},
"devDependencies": {

View File

@ -2,11 +2,12 @@ import { h, Component } from 'preact'
import { setIn, updateIn } from './utils/immutabilityHelpers'
import {
changeValue, changeProperty, changeType,
insert, append, duplicate, remove,
sort,
expand,
jsonToData, dataToJson, toDataPath
changeValue, changeProperty, changeType,
insertAfter, append, duplicate, remove,
sort,
expand,
jsonToData, dataToJson, toDataPath,
createDataEntry
} from './jsonData'
import JSONNode from './JSONNode'
@ -104,7 +105,7 @@ export default class TreeMode extends Component {
}
handleInsert = (path, afterProp, type) => {
this.setData(insert(this.state.data, path, afterProp, type))
this.setData(insertAfter(this.state.data, path, afterProp, type))
}
handleAppend = (path, type) => {
@ -116,7 +117,7 @@ export default class TreeMode extends Component {
}
handleRemove = (path, prop) => {
this.setData(remove(this.state.data, path, prop))
this.setData(remove(this.state.data, path.concat(prop)))
}
handleSort = (path, order = null) => {

View File

@ -3,14 +3,19 @@
* All functions are pure and don't mutate the JSONData.
*/
import { cloneDeep, isObject } from './utils/objectUtils'
import { isObject } from './utils/objectUtils'
import { setIn, updateIn, getIn, deleteIn } from './utils/immutabilityHelpers'
import { compareAsc, compareDesc } from './utils/arrayUtils'
import { stringConvert } from './utils/typeUtils'
import { findUniqueName } from './utils/stringUtils'
import isEqual from 'lodash/isEqual'
import cloneDeep from 'lodash/isEqual'
// TODO: rewrite the functions into jsonpatch functions, including a function `patch`
const expandNever = function (path) {
return false
}
/**
* Change the value of a property or item
@ -24,7 +29,7 @@ export function changeValue (data, path, value) {
const dataPath = toDataPath(data, path)
return setIn(data, dataPath.concat(['value']), value)
return setIn(data, dataPath.concat('value'), value)
}
/**
@ -64,7 +69,7 @@ export function changeType (data, path, type) {
const dataPath = toDataPath(data, path)
const oldEntry = getIn(data, dataPath)
const newEntry = convertDataEntry(oldEntry, type)
const newEntry = convertDataType(oldEntry, type)
return setIn(data, dataPath, newEntry)
}
@ -77,14 +82,15 @@ export function changeType (data, path, type) {
* @param {JSONDataType} type
* @return {JSONData}
*/
export function insert (data, path, afterProp, type) {
// console.log('insert', path, afterProp, type)
// TODO: remove function insertAfter, create insert(data, path, value, afterProp) instead
export function insertAfter (data, path, afterProp, type) {
// console.log('insertAfter', path, afterProp, type)
const dataPath = toDataPath(data, path)
const parent = getIn(data, dataPath)
if (parent.type === 'array') {
return updateIn(data, dataPath.concat(['items']), (items) => {
return updateIn(data, dataPath.concat('items'), (items) => {
const index = parseInt(afterProp)
const updatedItems = items.slice(0)
@ -94,7 +100,7 @@ export function insert (data, path, afterProp, type) {
})
}
else { // parent.type === 'object'
return updateIn(data, dataPath.concat(['props']), (props) => {
return updateIn(data, dataPath.concat('props'), (props) => {
const index = props.findIndex(p => p.name === afterProp)
const updatedProps = props.slice(0)
@ -108,6 +114,46 @@ export function insert (data, path, afterProp, type) {
}
}
/**
* Insert a new item
* @param {JSONData} data
* @param {Path} path
* @param {JSONData} value // TODO: pass json instead of JSONData as value
* @return {JSONData}
*/
export function insert (data, path, value) {
// console.log('insert', path, value)
const parentPath = path.slice(0, path.length - 1)
const prop = path[path.length - 1]
const dataPath = toDataPath(data, parentPath)
const parent = getIn(data, dataPath)
// TODO: throw error if parentPath does not exist?
// FIXME: updateIn should fail when the path doesn't yet exist
if (parent.type === 'array') {
return updateIn(data, dataPath.concat('items'), (items) => {
const index = parseInt(prop)
const updatedItems = items.slice(0)
updatedItems.splice(index, 0, value)
return updatedItems
})
}
else { // parent.type === 'object'
return updateIn(data, dataPath.concat('props'), (props) => {
const newProp = {
name: prop,
value
}
return props.concat(newProp)
})
}
}
/**
* Append a new item at the end of an object or array
* @param {JSONData} data
@ -115,6 +161,7 @@ export function insert (data, path, afterProp, type) {
* @param {JSONDataType} type
* @return {JSONData}
*/
// TODO: remove append, use insert instead
export function append (data, path, type) {
// console.log('append', path, type)
@ -122,7 +169,7 @@ export function append (data, path, type) {
const object = getIn(data, dataPath)
if (object.type === 'array') {
return updateIn(data, dataPath.concat(['items']), (items) => {
return updateIn(data, dataPath.concat('items'), (items) => {
const updatedItems = items.slice(0)
updatedItems.push(createDataEntry(type))
@ -131,7 +178,7 @@ export function append (data, path, type) {
})
}
else { // object.type === 'object'
return updateIn(data, dataPath.concat(['props']), (props) => {
return updateIn(data, dataPath.concat('props'), (props) => {
const updatedProps = props.slice(0)
updatedProps.push({
@ -144,6 +191,17 @@ export function append (data, path, type) {
}
}
/**
* Replace an existing item
* @param {JSONData} data
* @param {Path} path
* @param {JSONData} value // TODO: pass json instead of JSONData as value
* @return {JSONData}
*/
export function replace (data, path, value) {
return setIn(data, toDataPath(data, path), value)
}
/**
* Duplicate a property or item
* @param {JSONData} data
@ -158,7 +216,7 @@ export function duplicate (data, path, prop) {
const object = getIn(data, dataPath)
if (object.type === 'array') {
return updateIn(data, dataPath.concat(['items']), (items) => {
return updateIn(data, dataPath.concat('items'), (items) => {
const index = parseInt(prop)
const updatedItems = items.slice(0)
const original = items[index]
@ -170,16 +228,16 @@ export function duplicate (data, path, prop) {
})
}
else { // object.type === 'object'
return updateIn(data, dataPath.concat(['props']), (props) => {
return updateIn(data, dataPath.concat('props'), (props) => {
const index = props.findIndex(p => p.name === prop)
const updated = props.slice(0)
const original = props[index]
const duplicate = cloneDeep(original)
const clone = cloneDeep(original)
// prevent duplicate property names
duplicate.name = findUniqueName(duplicate.name, props.map(p => p.name))
clone.name = findUniqueName(clone.name, props.map(p => p.name))
updated.splice(index + 1, 0, duplicate)
updated.splice(index + 1, 0, clone)
return updated
})
@ -190,21 +248,23 @@ export function duplicate (data, path, prop) {
* Remove an item or property
* @param {JSONData} data
* @param {Path} path
* @param {string | number} prop
* @return {JSONData}
*/
export function remove (data, path, prop) {
// console.log('remove', path, prop)
export function remove (data, path) {
// console.log('remove', path)
const object = getIn(data, toDataPath(data, path))
const parentPath = path.slice(0, path.length - 1)
const object = getIn(data, toDataPath(data, parentPath))
// TODO: throw error if path does not exist
if (object.type === 'array') {
const dataPath = toDataPath(data, path.concat(prop))
const dataPath = toDataPath(data, path)
return deleteIn(data, dataPath)
}
else { // object.type === 'object'
const dataPath = toDataPath(data, path.concat(prop))
const dataPath = toDataPath(data, path)
dataPath.pop() // remove the 'value' property, we want to remove the whole object property
return deleteIn(data, dataPath)
@ -295,7 +355,7 @@ export function expand (data, callback, expanded) {
* @param {boolean} expanded New expanded state: true to expand, false to collapse
* @return {*}
*/
export function expandRecursive (data, path, callback, expanded) {
function expandRecursive (data, path, callback, expanded) {
switch (data.type) {
case 'array': {
let updatedData = callback(path)
@ -359,6 +419,37 @@ export function toDataPath (data, path) {
}
}
/**
* Test whether a path exists in the json data
* @param {JSONData} data
* @param {Path} path
* @return {boolean} Returns true if the path exists, else returns false
* @private
*/
export function pathExists (data, path) {
if (data === undefined) {
return false
}
if (path.length === 0) {
return true
}
let index
if (data.type === 'array') {
// index of an array
index = path[0]
return pathExists(data.items[index], path.slice(1))
}
else {
// object property. find the index of this property
index = data.props.findIndex(prop => prop.name === path[0])
const prop = data.props[index]
return pathExists(prop && prop.value, path.slice(1))
}
}
/**
* Convert a JSON object into the internally used data model
* @param {Path} path
@ -366,6 +457,7 @@ export function toDataPath (data, path) {
* @param {function(path: Path)} expand
* @return {JSONData}
*/
// TODO: change signature to jsonToData(json, expand=(path) => false, path=[])
export function jsonToData (path, json, expand) {
if (Array.isArray(json)) {
return {
@ -418,6 +510,188 @@ export function dataToJson (data) {
}
}
/**
* @param {JSONData} data
* @param {Path} path
* @param {JSONData} value
* @return {{data: JSONData, revert: Object}}
* @private
*/
function _patchAdd (data, path, value) {
const parentPath = path.slice(0, path.length - 1)
const parent = getIn(data, toDataPath(data, parentPath))
const resolvedPath = resolvePathIndex(data, path)
// FIXME: should not be needed to do try/catch. Create a function exists(data, path), or rewrite toDataPath such that you don't need to pass data
let oldValue = undefined
try {
oldValue = getIn(data, toDataPath(data, resolvedPath))
}
catch (err) {}
const updatedData = insert(data, resolvedPath, value)
return {
data: updatedData,
revert: (parent.type === 'object' && oldValue !== undefined)
? {op: 'replace', path: compileJSONPointer(resolvedPath), value: dataToJson(oldValue)}
: {op: 'remove', path: compileJSONPointer(resolvedPath)}
}
}
/**
* Resolve the index for `arr/-`, replace it with an index value equal to the
* length of the array
* @param {JSONData} data
* @param {Path} path
* @return {Path}
*/
export function resolvePathIndex (data, path) {
if (path[path.length - 1] === '-') {
const parentPath = path.slice(0, path.length - 1)
const parent = getIn(data, toDataPath(data, parentPath))
if (parent.type === 'array') {
const index = parent.items.length
const resolvedPath = path.slice(0)
resolvedPath[resolvedPath.length - 1] = index
return resolvedPath
}
}
return path
}
/**
* Apply a patch to a JSONData object
* @param {JSONData} data
* @param {Array} patch A JSON patch
* @return {{data: JSONData, revert: Array.<Object>, error: Error | null}}
*/
export function patchData (data, patch) {
const expand = expandNever // TODO: customizable expand function
try {
let updatedData = data
let revert = []
patch.forEach(function (action) {
switch (action.op) {
case 'add': {
const path = parseJSONPointer(action.path)
const parentPath = path.slice(0, path.length - 1)
const value = jsonToData(parentPath, action.value, expand)
const result = _patchAdd(updatedData, path, value)
updatedData = result.data
revert.unshift(result.revert)
break
}
case 'remove': {
const path = parseJSONPointer(action.path)
const value = dataToJson(getIn(updatedData, toDataPath(updatedData, path)))
updatedData = remove(updatedData, path)
revert.unshift({
op: 'add',
path: action.path,
value
})
break
}
case 'replace': {
const path = parseJSONPointer(action.path)
const oldValue = dataToJson(getIn(updatedData, toDataPath(updatedData, path)))
const parentPath = path.slice(0, path.length - 1)
const newValue = jsonToData(parentPath, action.value, expand)
updatedData = replace(updatedData, path, newValue)
revert.unshift({
op: 'replace',
path: action.path,
value: oldValue
})
break
}
case 'copy': {
const path = parseJSONPointer(action.path)
const from = parseJSONPointer(action.from)
const value = getIn(updatedData, toDataPath(updatedData, from))
const result = _patchAdd(updatedData, path, value)
updatedData = result.data
revert.unshift(result.revert)
break
}
case 'move': {
if (action.path !== action.from) {
const path = parseJSONPointer(action.path)
const from = parseJSONPointer(action.from)
const value = getIn(updatedData, toDataPath(updatedData, from))
updatedData = remove(updatedData, from)
const result = _patchAdd(updatedData, path, value)
updatedData = result.data
if (result.revert.op === 'replace') {
revert.unshift({op: 'add', path: action.path, value: result.revert.value})
revert.unshift({op: 'move', from: action.path, path: action.from})
}
else {
revert.unshift({op: 'move', from: action.path, path: action.from})
}
}
break
}
case 'test': {
// FIXME: should not be needed to do try/catch. Create a function exists(data, path), or rewrite toDataPath such that you don't need to pass data
const path = parseJSONPointer(action.path)
let value
try {
value = getIn(data, toDataPath(data, path))
}
catch (err) {}
if (action.value === undefined) {
throw new Error('Test failed, no value provided')
}
if (value === undefined) {
throw new Error('Test failed, path not found')
}
if (!isEqual(dataToJson(value), action.value)) {
throw new Error('Test failed, value differs')
}
break
}
default: {
throw new Error('Unknown jsonpatch op ' + JSON.stringify(action.op))
}
}
})
return {
data: updatedData,
revert,
error: null
}
}
catch (error) {
return {data, revert: [], error}
}
}
/**
* Create a new data entry
@ -448,25 +722,25 @@ export function createDataEntry (type) {
}
/**
* Convert an entry into a different type. When possible, data is retained
* @param {JSONData} entry
* Convert a JSONData object into a different type. When possible, data is retained
* @param {JSONData} data
* @param {JSONDataType} type
* @return {JSONData}
*/
export function convertDataEntry (entry, type) {
export function convertDataType (data, type) {
const convertedEntry = createDataEntry(type)
// convert contents from old value to new value where possible
if (type === 'value' && entry.type === 'string') {
convertedEntry.value = stringConvert(entry.value)
if (type === 'value' && data.type === 'string') {
convertedEntry.value = stringConvert(data.value)
}
if (type === 'string' && entry.type === 'value') {
convertedEntry.value = entry.value + ''
if (type === 'string' && data.type === 'value') {
convertedEntry.value = data.value + ''
}
if (type === 'object' && entry.type === 'array') {
convertedEntry.props = entry.items.map((item, index) => {
if (type === 'object' && data.type === 'array') {
convertedEntry.props = data.items.map((item, index) => {
return {
name: index + '',
value: item
@ -474,9 +748,38 @@ export function convertDataEntry (entry, type) {
})
}
if (type === 'array' && entry.type === 'object') {
convertedEntry.items = entry.props.map(prop => prop.value)
if (type === 'array' && data.type === 'object') {
convertedEntry.items = data.props.map(prop => prop.value)
}
return convertedEntry
}
}
/**
* Parse a JSON Pointer
* WARNING: this is not a complete JSONPointer implementation
* @param {string} pointer
* @return {Array}
*/
export function parseJSONPointer (pointer) {
const path = pointer.split('/')
path.shift() // remove the first empty entry
return path.map(p => p.replace(/~1/g, '/').replace(/~0/g, '~'))
}
/**
* Compile a JSON Pointer
* WARNING: this is not a complete JSONPointer implementation
* @param {Path} path
* @return {string}
*/
export function compileJSONPointer (path) {
return '/' + path
.map(p => {
return typeof p === 'string' // TODO: remove this check when the path is all strings
? p.replace(/~/g, '~0').replace(/\//g, '~1')
: p
})
.join('/')
}

View File

@ -54,16 +54,7 @@ export function setIn (object, path, value) {
}
const key = path[0]
let updated
if (typeof key === 'string' && !isObject(object)) {
updated = {}
}
else if (typeof key === 'number' && !Array.isArray(object)) {
updated = []
}
else {
updated = clone(object)
}
const updated = cloneOrCreate(key, object)
const updatedValue = setIn(updated[key], path.slice(1), value)
if (updated[key] === updatedValue) {
@ -90,16 +81,7 @@ export function updateIn (object, path, callback) {
}
const key = path[0]
let updated
if (typeof key === 'string' && !isObject(object)) {
updated = {} // change into an object
}
else if (typeof key === 'number' && !Array.isArray(object)) {
updated = [] // change into an array
}
else {
updated = clone(object)
}
const updated = cloneOrCreate(key, object)
const updatedValue = updateIn(object[key], path.slice(1), callback)
if (updated[key] === updatedValue) {
@ -150,3 +132,23 @@ export function deleteIn (object, path) {
return object
}
}
/**
* Helper function to clone an array or object, or to create a new object
* when `object` is undefined. When object is anything else, the function will
* throw an error
* @param {string | number} key
* @param {Object | Array | undefined} object
* @return {Array | Object}
*/
function cloneOrCreate (key, object) {
if (object === undefined) {
return (typeof key === 'number') ? [] : {} // create new object or array
}
if (typeof object === 'object' || Array.isArray(object)) {
return clone(object)
}
throw new Error('Cannot override existing property ' + JSON.stringify(object))
}

View File

@ -72,22 +72,15 @@ test('setIn non existing path', t => {
})
})
test('setIn replace value with object', t => {
test('setIn replace value with object should throw an exception', t => {
const obj = {
a: 42,
d: 3
}
const updated = setIn(obj, ['a', 'b', 'c'], 4)
t.deepEqual (updated, {
a: {
b: {
c: 4
}
},
d: 3
})
t.throws(function () {
const updated = setIn(obj, ['a', 'b', 'c'], 4)
}, /Cannot override existing property/)
})
test('setIn replace value inside nested array', t => {
@ -118,24 +111,6 @@ test('setIn replace value inside nested array', t => {
})
})
test('setIn change array into object', t => {
const obj = [1,2,3]
const updated = setIn(obj, ['foo'], 'bar')
t.deepEqual (updated, {
foo: 'bar'
})
})
test('setIn change object into array', t => {
const obj = {a:1, b:2}
const updated = setIn(obj, [2], 'foo')
t.deepEqual (updated, [, , 'foo'])
})
test('setIn identical value should return the original object', t => {
const obj = {a:1, b:2}

View File

@ -1,5 +1,8 @@
import test from 'ava';
import { jsonToData, dataToJson, expand } from '../src/jsonData'
import {
jsonToData, dataToJson, expand, patchData, pathExists,
parseJSONPointer, compileJSONPointer
} from '../src/jsonData'
// TODO: test all functions like append, insert, duplicate etc.
@ -273,5 +276,334 @@ test('expand a callback should not change the object when nothing happens', t =>
t.is(collapsed, JSON_DATA_EXAMPLE)
})
test('pathExists', t => {
t.is(pathExists(JSON_DATA_EXAMPLE, ['obj', 'arr', 2, 'a']), true)
t.is(pathExists(JSON_DATA_EXAMPLE, ['obj', 'foo']), false)
t.is(pathExists(JSON_DATA_EXAMPLE, ['obj', 'foo', 'bar']), false)
t.is(pathExists(JSON_DATA_EXAMPLE, []), true)
})
test('parseJSONPointer', t => {
t.deepEqual(parseJSONPointer('/obj/a'), ['obj', 'a'])
t.deepEqual(parseJSONPointer('/arr/-'), ['arr', '-'])
t.deepEqual(parseJSONPointer('/foo/~1~0 ~0~1'), ['foo', '/~ ~/'])
})
test('compileJSONPointer', t => {
t.deepEqual(compileJSONPointer(['foo', 'bar']), '/foo/bar')
t.deepEqual(compileJSONPointer(['foo', '/~ ~/']), '/foo/~1~0 ~0~1')
})
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, (path) => false)
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, (path) => false)
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, (path) => false)
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},
{op: 'add', path: '/obj/a', value: 4}
])
// test revert
const data2 = jsonToData([], patchedJson, (path) => false)
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, (path) => false)
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},
{op: 'replace', path: '/obj/a', value: 4}
])
// test revert
const data2 = jsonToData([], patchedJson, (path) => false)
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 copy', t => {
const json = {
arr: [1,2,3],
obj: {a : 4}
}
const patch = [
{op: 'copy', from: '/obj', path: '/arr/2'},
]
const data = jsonToData([], json, (path) => false)
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, (path) => false)
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}}
])
})
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, (path) => false)
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]
})
t.deepEqual(revert, [
{op: 'move', from: '/arr/2', path: '/obj'}
])
// test revert
const data2 = jsonToData([], patchedJson, (path) => false)
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 = {
arr: [1,2,3],
obj: {a : 4}
}
const patch = [
{op: 'move', from: '/obj', path: '/arr'},
]
const data = jsonToData([], json, (path) => false)
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]}
])
// test revert
const data2 = jsonToData([], patchedJson, (path) => false)
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: 'remove', path: '/arr'},
{op: 'move', from: '/obj', path: '/arr'}
])
})
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, (path) => false)
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, (path) => false)
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, (path) => false)
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')
})