Implemented jsonpatch operations
This commit is contained in:
parent
94671507b5
commit
fe0f98dfe0
|
@ -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": {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
369
src/jsonData.js
369
src/jsonData.js
|
@ -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('/')
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue