Flatten `META` symbol, remove ordering of object keys from ESON model, simplify patch actions (WIP)
This commit is contained in:
parent
d6ad4c87d0
commit
56124cf17f
File diff suppressed because it is too large
Load Diff
|
@ -19,9 +19,10 @@
|
|||
"bugs": "https://github.com/josdejong/jsoneditor/issues",
|
||||
"private": false,
|
||||
"dependencies": {
|
||||
"ajv": "6.5.2",
|
||||
"ajv": "6.5.3",
|
||||
"brace": "0.11.1",
|
||||
"javascript-natural-sort": "0.7.1",
|
||||
"jest": "23.5.0",
|
||||
"lodash": "4.17.10",
|
||||
"mitt": "1.1.3",
|
||||
"prop-types": "15.6.2",
|
||||
|
@ -43,8 +44,8 @@
|
|||
"css-loader": "1.0.0",
|
||||
"node-sass-chokidar": "1.3.3",
|
||||
"npm-run-all": "4.1.3",
|
||||
"preact": "8.3.0",
|
||||
"preact-compat": "3.18.2",
|
||||
"preact": "8.3.1",
|
||||
"preact-compat": "3.18.3",
|
||||
"react": "16.4.2",
|
||||
"react-dom": "16.4.2",
|
||||
"react-scripts": "1.1.4",
|
||||
|
|
|
@ -159,7 +159,7 @@ class App extends Component {
|
|||
|
||||
handlePatch = (patch, revert) => {
|
||||
this.log('onPatch patch=', patch, ', revert=', revert)
|
||||
window.patch = patch
|
||||
window.immutableJsonPatch = patch
|
||||
window.revert = revert
|
||||
}
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
name: 'myObject',
|
||||
onPatch: function (patch, revert) {
|
||||
log('onPatch patch=', patch, ', revert=', revert)
|
||||
window.patch = patch
|
||||
window.immutableJsonPatch = patch
|
||||
window.revert = revert
|
||||
},
|
||||
onPatchText: function (patch, revert) {
|
||||
|
|
|
@ -1,72 +1,61 @@
|
|||
import last from 'lodash/last'
|
||||
import initial from 'lodash/initial'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import {
|
||||
META,
|
||||
compileJSONPointer, esonToJson, findNextProp,
|
||||
pathsFromSelection, findRootPath, findSelectionIndices
|
||||
} from './eson'
|
||||
import { findRootPath, findSelectionIndices, pathsFromSelection } from './eson'
|
||||
import { getIn } from './utils/immutabilityHelpers'
|
||||
import { findUniqueName } from './utils/stringUtils'
|
||||
import { isObject, stringConvert } from './utils/typeUtils'
|
||||
import { findUniqueName } from './utils/stringUtils'
|
||||
import { isObject, stringConvert } from './utils/typeUtils'
|
||||
import { compareAsc, compareDesc } from './utils/arrayUtils'
|
||||
|
||||
import { compileJSONPointer } from './jsonPointer'
|
||||
|
||||
/**
|
||||
* Create a JSONPatch to change the value of a property or item
|
||||
* @param {ESON} eson
|
||||
* @param {JSON} eson
|
||||
* @param {Path} path
|
||||
* @param {*} value
|
||||
* @return {Array}
|
||||
*/
|
||||
export function changeValue (eson, path, value) {
|
||||
// console.log('changeValue', data, value)
|
||||
const oldDataValue = getIn(eson, path)
|
||||
|
||||
return [{
|
||||
op: 'replace',
|
||||
path: compileJSONPointer(path),
|
||||
value: value,
|
||||
meta: {
|
||||
type: oldDataValue[META].type
|
||||
}
|
||||
value
|
||||
}]
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSONPatch to change a property name
|
||||
* @param {ESON} eson
|
||||
* @param {JSON} json
|
||||
* @param {Path} parentPath
|
||||
* @param {string} oldProp
|
||||
* @param {string} newProp
|
||||
* @return {Array}
|
||||
*/
|
||||
export function changeProperty (eson, parentPath, oldProp, newProp) {
|
||||
export function changeProperty (json, parentPath, oldProp, newProp) {
|
||||
// console.log('changeProperty', parentPath, oldProp, newProp)
|
||||
const parent = getIn(eson, parentPath)
|
||||
const parent = getIn(json, parentPath)
|
||||
|
||||
// prevent duplicate property names
|
||||
const uniqueNewProp = findUniqueName(newProp, parent[META].props)
|
||||
const uniqueNewProp = findUniqueName(newProp, parent)
|
||||
|
||||
return [{
|
||||
op: 'move',
|
||||
from: compileJSONPointer(parentPath.concat(oldProp)),
|
||||
path: compileJSONPointer(parentPath.concat(uniqueNewProp)),
|
||||
meta: {
|
||||
before: findNextProp(parent, oldProp)
|
||||
}
|
||||
path: compileJSONPointer(parentPath.concat(uniqueNewProp))
|
||||
}]
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JSONPatch to change the type of a property or item
|
||||
* @param {ESON} eson
|
||||
* @param {JSON} json
|
||||
* @param {Path} path
|
||||
* @param {ESONType} type
|
||||
* @return {Array}
|
||||
*/
|
||||
export function changeType (eson, path, type) {
|
||||
const oldValue = esonToJson(getIn(eson, path))
|
||||
export function changeType (json, path, type) {
|
||||
const oldValue = getIn(json, path)
|
||||
const newValue = convertType(oldValue, type)
|
||||
|
||||
// console.log('changeType', path, type, oldValue, newValue)
|
||||
|
@ -74,10 +63,7 @@ export function changeType (eson, path, type) {
|
|||
return [{
|
||||
op: 'replace',
|
||||
path: compileJSONPointer(path),
|
||||
value: newValue,
|
||||
meta: {
|
||||
type
|
||||
}
|
||||
value: newValue
|
||||
}]
|
||||
}
|
||||
|
||||
|
@ -88,42 +74,37 @@ export function changeType (eson, path, type) {
|
|||
* a unique property name for the duplicated node in case of duplicating
|
||||
* and object property
|
||||
*
|
||||
* @param {ESON} eson
|
||||
* @param {JSON} json
|
||||
* @param {Selection} selection
|
||||
* @return {Array}
|
||||
*/
|
||||
export function duplicate (eson, selection) {
|
||||
export function duplicate (json, selection) {
|
||||
// console.log('duplicate', path)
|
||||
if (!selection.start || !selection.end) {
|
||||
return []
|
||||
}
|
||||
|
||||
const rootPath = findRootPath(selection)
|
||||
const root = getIn(eson, rootPath)
|
||||
const root = getIn(json, rootPath)
|
||||
const { maxIndex } = findSelectionIndices(root, rootPath, selection)
|
||||
const paths = pathsFromSelection(eson, selection)
|
||||
const paths = pathsFromSelection(json, selection)
|
||||
|
||||
if (root[META].type === 'Array') {
|
||||
if (Array.isArray(root)) {
|
||||
return paths.map((path, offset) => ({
|
||||
op: 'copy',
|
||||
from: compileJSONPointer(path),
|
||||
path: compileJSONPointer(rootPath.concat(maxIndex + offset))
|
||||
}))
|
||||
}
|
||||
else { // root[META].type === 'Object'
|
||||
const before = root[META].props[maxIndex] || null
|
||||
|
||||
else { // 'object'
|
||||
return paths.map(path => {
|
||||
const prop = last(path)
|
||||
const newProp = findUniqueName(prop, root[META].props)
|
||||
const newProp = findUniqueName(prop, root)
|
||||
|
||||
return {
|
||||
op: 'copy',
|
||||
from: compileJSONPointer(path),
|
||||
path: compileJSONPointer(rootPath.concat(newProp)),
|
||||
meta: {
|
||||
before
|
||||
}
|
||||
path: compileJSONPointer(rootPath.concat(newProp))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -136,38 +117,31 @@ export function duplicate (eson, selection) {
|
|||
* a unique property name for the inserted node in case of duplicating
|
||||
* and object property
|
||||
*
|
||||
* @param {ESON} eson
|
||||
* @param {JSON} json
|
||||
* @param {Path} path
|
||||
* @param {Array.<{name?: string, value: JSON, type?: ESONType}>} values
|
||||
* @param {Array.<{name?: string, value: JSON}>} values
|
||||
* @return {Array}
|
||||
*/
|
||||
export function insertBefore (eson, path, values) { // TODO: find a better name and define datastructure for values
|
||||
export function insertBefore (json, path, values) { // TODO: find a better name and define datastructure for values
|
||||
// TODO: refactor. path should be parent path
|
||||
const parentPath = initial(path)
|
||||
const parent = getIn(eson, parentPath)
|
||||
const parent = getIn(json, parentPath)
|
||||
|
||||
if (parent[META].type === 'Array') {
|
||||
if (Array.isArray(parent)) {
|
||||
const startIndex = parseInt(last(path), 10)
|
||||
return values.map((entry, offset) => ({
|
||||
op: 'add',
|
||||
path: compileJSONPointer(parentPath.concat(startIndex + offset)),
|
||||
value: entry.value,
|
||||
meta: {
|
||||
type: entry.type
|
||||
}
|
||||
value: entry.value
|
||||
}))
|
||||
}
|
||||
else { // parent[META].type === 'Object'
|
||||
const before = last(path)
|
||||
else { // 'object'
|
||||
return values.map(entry => {
|
||||
const newProp = findUniqueName(entry.name, parent[META].props)
|
||||
const newProp = findUniqueName(entry.name, parent)
|
||||
return {
|
||||
op: 'add',
|
||||
path: compileJSONPointer(parentPath.concat(newProp)),
|
||||
value: entry.value,
|
||||
meta: {
|
||||
type: entry.type,
|
||||
before
|
||||
}
|
||||
value: entry.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -180,40 +154,31 @@ export function insertBefore (eson, path, values) { // TODO: find a better name
|
|||
* a unique property name for the inserted node in case of duplicating
|
||||
* and object property
|
||||
*
|
||||
* @param {ESON} eson
|
||||
* @param {JSON} json
|
||||
* @param {Path} path
|
||||
* @param {Array.<{name?: string, value: JSON, type?: ESONType}>} values
|
||||
* @param {Array.<{name?: string, value: JSON}>} values
|
||||
* @return {Array}
|
||||
*/
|
||||
export function insertAfter (eson, path, values) { // TODO: find a better name and define datastructure for values
|
||||
export function insertAfter (json, path, values) { // TODO: find a better name and define datastructure for values
|
||||
// TODO: refactor. path should be parent path
|
||||
const parentPath = initial(path)
|
||||
const parent = getIn(eson, parentPath)
|
||||
const parent = getIn(json, parentPath)
|
||||
|
||||
if (parent[META].type === 'Array') {
|
||||
if (Array.isArray(parent)) {
|
||||
const startIndex = parseInt(last(path), 10)
|
||||
return values.map((entry, offset) => ({
|
||||
op: 'add',
|
||||
path: compileJSONPointer(parentPath.concat(startIndex + 1 + offset)), // +1 to insert after
|
||||
value: entry.value,
|
||||
meta: {
|
||||
type: entry.type
|
||||
}
|
||||
value: entry.value
|
||||
}))
|
||||
}
|
||||
else { // parent[META].type === 'Object'
|
||||
const prop = last(path)
|
||||
const propIndex = parent[META].props.indexOf(prop)
|
||||
const before = parent[META].props[propIndex + 1]
|
||||
else { // 'object'
|
||||
return values.map(entry => {
|
||||
const newProp = findUniqueName(entry.name, parent[META].props)
|
||||
const newProp = findUniqueName(entry.name, parent)
|
||||
return {
|
||||
op: 'add',
|
||||
path: compileJSONPointer(parentPath.concat(newProp)),
|
||||
value: entry.value,
|
||||
meta: {
|
||||
type: entry.type,
|
||||
before
|
||||
}
|
||||
value: entry.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -221,20 +186,20 @@ export function insertAfter (eson, path, values) { // TODO: find a better name
|
|||
|
||||
/**
|
||||
* Insert values at the start of an Object or Array
|
||||
* @param {ESON} eson
|
||||
* @param {JSON} json
|
||||
* @param {Path} parentPath
|
||||
* @param {Array.<{name?: string, value: JSON, type?: ESONType}>} values
|
||||
* @param {Array.<{name?: string, value: JSON}>} values
|
||||
* @return {Array}
|
||||
*/
|
||||
export function insertInside (eson, parentPath, values) {
|
||||
const parent = getIn(eson, parentPath)
|
||||
export function insertInside (json, parentPath, values) {
|
||||
const parent = getIn(json, parentPath)
|
||||
|
||||
if (parent[META].type === 'Array') {
|
||||
return insertBefore(eson, parentPath.concat('0'), values)
|
||||
if (Array.isArray(parent)) {
|
||||
return insertBefore(json, parentPath.concat('0'), values)
|
||||
}
|
||||
else if (parent[META].type === 'Object') {
|
||||
const firstProp = parent[META].props[0] || null
|
||||
return insertBefore(eson, parentPath.concat(firstProp), values)
|
||||
else if (parent && typeof parent === 'object') {
|
||||
// TODO: refactor. path should be parent path
|
||||
return insertBefore(json, parentPath.concat('foobar'), values)
|
||||
}
|
||||
else {
|
||||
throw new Error('Cannot insert in a value, only in an Object or Array')
|
||||
|
@ -248,43 +213,34 @@ export function insertInside (eson, parentPath, values) {
|
|||
* a unique property name for the inserted node in case of duplicating
|
||||
* and object property
|
||||
*
|
||||
* @param {ESON} eson
|
||||
* @param {JSON} json
|
||||
* @param {Selection} selection
|
||||
* @param {Array.<{name?: string, value: JSON, state: Object}>} values
|
||||
* @param {Array.<{name?: string, value: JSON}>} values
|
||||
* @return {Array}
|
||||
*/
|
||||
export function replace (eson, selection, values) { // TODO: find a better name and define datastructure for values
|
||||
export function replace (json, selection, values) { // TODO: find a better name and define datastructure for values
|
||||
const rootPath = findRootPath(selection)
|
||||
const root = getIn(eson, rootPath)
|
||||
const root = getIn(json, rootPath)
|
||||
const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
|
||||
|
||||
if (root[META].type === 'Array') {
|
||||
const removeActions = removeAll(pathsFromSelection(eson, selection))
|
||||
if (Array.isArray(root)) {
|
||||
const removeActions = removeAll(pathsFromSelection(json, selection))
|
||||
const insertActions = values.map((entry, offset) => ({
|
||||
op: 'add',
|
||||
path: compileJSONPointer(rootPath.concat(minIndex + offset)),
|
||||
value: entry.value,
|
||||
meta: {
|
||||
state: entry.state
|
||||
}
|
||||
value: entry.value
|
||||
}))
|
||||
|
||||
return removeActions.concat(insertActions)
|
||||
}
|
||||
else { // root[META].type === 'Object'
|
||||
const before = root[META].props[maxIndex] || null
|
||||
|
||||
const removeActions = removeAll(pathsFromSelection(eson, selection))
|
||||
else { // root is Object
|
||||
const removeActions = removeAll(pathsFromSelection(json, selection))
|
||||
const insertActions = values.map(entry => {
|
||||
const newProp = findUniqueName(entry.name, root[META].props)
|
||||
const newProp = findUniqueName(entry.name, root)
|
||||
return {
|
||||
op: 'add',
|
||||
path: compileJSONPointer(rootPath.concat(newProp)),
|
||||
value: entry.value,
|
||||
meta: {
|
||||
before,
|
||||
state: entry.state
|
||||
}
|
||||
value: entry.value
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -299,37 +255,31 @@ export function replace (eson, selection, values) { // TODO: find a better name
|
|||
* a unique property name for the inserted node in case of duplicating
|
||||
* and object property
|
||||
*
|
||||
* @param {ESON} eson
|
||||
* @param {JSON} json
|
||||
* @param {Path} parentPath
|
||||
* @param {ESONType} type
|
||||
* @return {Array}
|
||||
*/
|
||||
export function append (eson, parentPath, type) {
|
||||
export function append (json, parentPath, type) {
|
||||
// console.log('append', parentPath, value)
|
||||
|
||||
const parent = getIn(eson, parentPath)
|
||||
const parent = getIn(json, parentPath)
|
||||
const value = createEntry(type)
|
||||
|
||||
if (parent[META].type === 'Array') {
|
||||
if (Array.isArray(parent)) {
|
||||
return [{
|
||||
op: 'add',
|
||||
path: compileJSONPointer(parentPath.concat('-')),
|
||||
value,
|
||||
meta: {
|
||||
type
|
||||
}
|
||||
value
|
||||
}]
|
||||
}
|
||||
else { // parent[META].type === 'Object'
|
||||
const newProp = findUniqueName('', parent[META].props)
|
||||
else { // 'object'
|
||||
const newProp = findUniqueName('', parent)
|
||||
|
||||
return [{
|
||||
op: 'add',
|
||||
path: compileJSONPointer(parentPath.concat(newProp)),
|
||||
value,
|
||||
meta: {
|
||||
type
|
||||
}
|
||||
value
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
@ -364,55 +314,35 @@ export function removeAll (paths) {
|
|||
/**
|
||||
* Create a JSONPatch to order the items of an array or the properties of an object in ascending
|
||||
* or descending order
|
||||
* @param {ESON} eson
|
||||
* @param {JSON} json
|
||||
* @param {Path} path
|
||||
* @param {'asc' | 'desc' | null} [order=null] If not provided, will toggle current ordering
|
||||
* @return {Array}
|
||||
*/
|
||||
export function sort (eson, path, order = null) {
|
||||
export function sort (json, path, order = null) {
|
||||
const compare = order === 'desc' ? compareDesc : compareAsc
|
||||
const reverseCompare = (a, b) => -compare(a, b)
|
||||
const object = getIn(eson, path)
|
||||
const object = getIn(json, path)
|
||||
|
||||
if (object[META].type === 'Array') {
|
||||
const items = object.map(item => item[META].value)
|
||||
if (Array.isArray(object)) {
|
||||
const createAction = ({item, fromIndex, toIndex}) => ({
|
||||
op: 'move',
|
||||
from: compileJSONPointer(path.concat(String(fromIndex))),
|
||||
path: compileJSONPointer(path.concat(String(toIndex)))
|
||||
})
|
||||
|
||||
const actions = sortWithComparator(items, compare).map(createAction)
|
||||
const actions = sortWithComparator(object, compare).map(createAction)
|
||||
|
||||
// when no order is provided, test whether ordering ascending
|
||||
// changed anything. If not, sort descending
|
||||
if (!order && isEmpty(actions)) {
|
||||
return sortWithComparator(items, reverseCompare).map(createAction)
|
||||
return sortWithComparator(object, reverseCompare).map(createAction)
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
else { // object[META].type === 'Object'
|
||||
|
||||
const props = object[META].props
|
||||
const createAction = ({item, beforeItem, fromIndex, toIndex}) => ({
|
||||
op: 'move',
|
||||
from: compileJSONPointer(path.concat(item)),
|
||||
path: compileJSONPointer(path.concat(item)),
|
||||
meta: {
|
||||
before: beforeItem
|
||||
}
|
||||
})
|
||||
|
||||
const actions = sortWithComparator(props, compare).map(createAction)
|
||||
|
||||
// when no order is provided, test whether ordering ascending
|
||||
// changed anything. If not, sort descending
|
||||
if (!order && isEmpty(actions)) {
|
||||
return sortWithComparator(props, reverseCompare).map(createAction)
|
||||
}
|
||||
|
||||
return actions
|
||||
else { // object is an Object, we don't allow sorting properties
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -466,10 +396,10 @@ function sortWithComparator (items, comparator) {
|
|||
* @return {Array | Object | string}
|
||||
*/
|
||||
export function createEntry (type) {
|
||||
if (type === 'Array') {
|
||||
if (type === 'array') {
|
||||
return []
|
||||
}
|
||||
else if (type === 'Object') {
|
||||
else if (type === 'object') {
|
||||
return {}
|
||||
}
|
||||
else {
|
||||
|
@ -503,7 +433,7 @@ export function convertType (value, type) {
|
|||
}
|
||||
}
|
||||
|
||||
if (type === 'Object') {
|
||||
if (type === 'object') {
|
||||
let object = {}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
|
@ -513,7 +443,7 @@ export function convertType (value, type) {
|
|||
return object
|
||||
}
|
||||
|
||||
if (type === 'Array') {
|
||||
if (type === 'array') {
|
||||
let array = []
|
||||
|
||||
if (isObject(value)) {
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
'use strict'
|
||||
|
||||
import { sort } from './actions'
|
||||
import { assertDeepEqualEson } from './utils/assertDeepEqualEson'
|
||||
import {esonToJson, expandOne, jsonToEson, META} from './eson'
|
||||
import {patchEson} from './patchEson'
|
||||
import { createAssertEqualEson } from './utils/assertEqualEson'
|
||||
import { ID, syncEson } from './eson'
|
||||
import { immutableJsonPatch } from './immutableJsonPatch'
|
||||
|
||||
const assertEqualEson = createAssertEqualEson(expect)
|
||||
|
||||
// TODO: test changeValue
|
||||
// TODO: test changeProperty
|
||||
|
@ -16,66 +18,28 @@ import {patchEson} from './patchEson'
|
|||
// TODO: test removeAll
|
||||
|
||||
it('sort root Array', () => {
|
||||
const eson = jsonToEson([1,3,2])
|
||||
const eson = syncEson([1,3,2])
|
||||
|
||||
assertDeepEqualEson(patchEson(eson, sort(eson, [])).data, jsonToEson([1,2,3]))
|
||||
assertDeepEqualEson(patchEson(eson, sort(eson, [], 'asc')).data, jsonToEson([1,2,3]))
|
||||
assertDeepEqualEson(patchEson(eson, sort(eson, [], 'desc')).data, jsonToEson([3,2,1]))
|
||||
assertEqualEson(immutableJsonPatch(eson, sort(eson, [])).json, syncEson([1,2,3]))
|
||||
assertEqualEson(immutableJsonPatch(eson, sort(eson, [], 'asc')).json, syncEson([1,2,3]))
|
||||
assertEqualEson(immutableJsonPatch(eson, sort(eson, [], 'desc')).json, syncEson([3,2,1]))
|
||||
})
|
||||
|
||||
it('sort nested Array', () => {
|
||||
const eson = jsonToEson({arr: [4,1,8,5,3,9,2,7,6]})
|
||||
const actual = patchEson(eson, sort(eson, ['arr'])).data
|
||||
const expected = jsonToEson({arr: [1,2,3,4,5,6,7,8,9]})
|
||||
assertDeepEqualEson(actual, expected)
|
||||
const eson = syncEson({arr: [4,1,8,5,3,9,2,7,6]})
|
||||
const actual = immutableJsonPatch(eson, sort(eson, ['arr'])).json
|
||||
const expected = syncEson({arr: [1,2,3,4,5,6,7,8,9]})
|
||||
assertEqualEson(actual, expected)
|
||||
})
|
||||
|
||||
it('sort nested Array reverse order', () => {
|
||||
// no order provided -> order ascending, but if nothing changes, order descending
|
||||
const eson = jsonToEson({arr: [1,2,3,4,5,6,7,8,9]})
|
||||
const actual = patchEson(eson, sort(eson, ['arr'])).data
|
||||
const expected = jsonToEson({arr: [9,8,7,6,5,4,3,2,1]})
|
||||
assertDeepEqualEson(actual, expected)
|
||||
const eson = syncEson({arr: [1,2,3,4,5,6,7,8,9]})
|
||||
const actual = immutableJsonPatch(eson, sort(eson, ['arr'])).json
|
||||
const expected = syncEson({arr: [9,8,7,6,5,4,3,2,1]})
|
||||
assertEqualEson(actual, expected)
|
||||
|
||||
// id's and META should be the same
|
||||
expect(actual.arr[META].id).toEqual(eson.arr[META].id)
|
||||
expect(actual.arr[7][META].id).toEqual(eson.arr[1][META].id)
|
||||
})
|
||||
|
||||
|
||||
it('sort root Object', () => {
|
||||
const eson = jsonToEson({c: 2, b: 3, a:4})
|
||||
|
||||
expect(patchEson(eson, sort(eson, [])).data[META].props).toEqual(['a', 'b', 'c'])
|
||||
expect(patchEson(eson, sort(eson, [], 'asc')).data[META].props).toEqual(['a', 'b', 'c'])
|
||||
expect(patchEson(eson, sort(eson, [], 'desc')).data[META].props).toEqual(['c', 'b', 'a'])
|
||||
})
|
||||
|
||||
it('sort nested Object', () => {
|
||||
const eson = jsonToEson({obj: {c: 2, b: 3, a:4}})
|
||||
eson.obj[META].expanded = true
|
||||
eson.obj.c[META].expanded = true
|
||||
|
||||
const actual = patchEson(eson, sort(eson, ['obj'])).data
|
||||
|
||||
// should keep META data
|
||||
expect(actual.obj[META].props).toEqual(['a', 'b', 'c'])
|
||||
expect(actual.obj[META].expanded).toEqual(true)
|
||||
expect(actual.obj.c[META].expanded).toEqual(true)
|
||||
expect(actual.obj[META].id).toEqual(eson.obj[META].id)
|
||||
expect(actual.obj.a[META].id).toEqual(eson.obj.a[META].id)
|
||||
expect(actual.obj.b[META].id).toEqual(eson.obj.b[META].id)
|
||||
expect(actual.obj.c[META].id).toEqual(eson.obj.c[META].id)
|
||||
|
||||
// asc, desc
|
||||
expect(patchEson(eson, sort(eson, ['obj'])).data.obj[META].props).toEqual(['a', 'b', 'c'])
|
||||
expect(patchEson(eson, sort(eson, ['obj'], 'asc')).data.obj[META].props).toEqual(['a', 'b', 'c'])
|
||||
expect(patchEson(eson, sort(eson, ['obj'], 'desc')).data.obj[META].props).toEqual(['c', 'b', 'a'])
|
||||
})
|
||||
|
||||
it('sort nested Object (larger)', () => {
|
||||
const eson = jsonToEson({obj: {h:1, c:1, e:1, d:1, g:1, b:1, a:1, f:1}})
|
||||
const actual = patchEson(eson, sort(eson, ['obj'])).data
|
||||
|
||||
expect(actual.obj[META].props).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'])
|
||||
expect(actual.arr[ID]).toEqual(eson.arr[ID])
|
||||
expect(actual.arr[7][ID]).toEqual(eson.arr[1][ID])
|
||||
})
|
||||
|
|
|
@ -50,8 +50,6 @@ export default class JSONEditor extends PureComponent {
|
|||
}
|
||||
|
||||
handleChangeMode = (mode) => {
|
||||
console.log('changeMode', mode, this.props.onChangeMode)
|
||||
|
||||
if (this.props.onChangeMode) {
|
||||
this.props.onChangeMode(mode, this.props.mode)
|
||||
}
|
||||
|
|
|
@ -1,24 +1,27 @@
|
|||
import { createElement as h, PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import initial from 'lodash/initial'
|
||||
import naturalSort from 'javascript-natural-sort'
|
||||
|
||||
import FloatingMenu from './menu/FloatingMenu'
|
||||
import { escapeHTML, unescapeHTML } from '../utils/stringUtils'
|
||||
import { getInnerText, insideRect } from '../utils/domUtils'
|
||||
import { stringConvert, valueType, isUrl } from '../utils/typeUtils'
|
||||
import {
|
||||
compileJSONPointer,
|
||||
META,
|
||||
SELECTED, SELECTED_START, SELECTED_END, SELECTED_AFTER, SELECTED_INSIDE,
|
||||
SELECTED_FIRST, SELECTED_LAST
|
||||
} from '../eson'
|
||||
import { compileJSONPointer } from '../jsonPointer'
|
||||
import { ERROR, EXPANDED, ID, SEARCH_PROPERTY, SEARCH_VALUE, SELECTION, TYPE, VALUE } from '../eson'
|
||||
|
||||
export default class JSONNode extends PureComponent {
|
||||
static URL_TITLE = 'Ctrl+Click or Ctrl+Enter to open url'
|
||||
|
||||
static propTypes = {
|
||||
prop: PropTypes.string, // in case of an object property
|
||||
value: PropTypes.oneOfType([ PropTypes.object, PropTypes.array ]).isRequired,
|
||||
parentPath: PropTypes.array,
|
||||
prop: PropTypes.string, // in case of an object property
|
||||
index: PropTypes.number, // in case of an array item
|
||||
eson: PropTypes.any, // enriched JSON object: Object, Array, number, string, or null
|
||||
|
||||
emit: PropTypes.func.isRequired,
|
||||
findKeyBinding: PropTypes.func.isRequired,
|
||||
|
@ -31,10 +34,17 @@ export default class JSONNode extends PureComponent {
|
|||
})
|
||||
}
|
||||
|
||||
state = {
|
||||
menu: null, // can contain object {anchor, root}
|
||||
appendMenu: null, // can contain object {anchor, root}
|
||||
hover: null
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
menu: null, // can contain object {anchor, root}
|
||||
appendMenu: null, // can contain object {anchor, root}
|
||||
hover: null,
|
||||
path: this.props.parentPath
|
||||
? this.props.parentPath.concat('index' in this.props ? this.props.index : this.props.prop)
|
||||
: []
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
|
@ -44,14 +54,12 @@ export default class JSONNode extends PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
// console.log('JSONNode.render ' + JSON.stringify(this.props.value[META].path))
|
||||
const type = this.props.value[META].type
|
||||
if (type === 'Object') {
|
||||
return this.renderJSONObject()
|
||||
}
|
||||
else if (type === 'Array') {
|
||||
if (this.props.eson[TYPE] === 'array') {
|
||||
return this.renderJSONArray()
|
||||
}
|
||||
else if (this.props.eson[TYPE] === 'object') {
|
||||
return this.renderJSONObject()
|
||||
}
|
||||
else { // no Object or Array
|
||||
return this.renderJSONValue()
|
||||
}
|
||||
|
@ -59,8 +67,10 @@ export default class JSONNode extends PureComponent {
|
|||
|
||||
renderJSONObject () {
|
||||
// TODO: refactor renderJSONObject (too large/complex)
|
||||
const meta = this.props.value[META]
|
||||
const props = meta.props
|
||||
const eson = this.props.eson
|
||||
const jsonProps = Object.keys(eson).sort(naturalSort)
|
||||
const jsonPropsCount = jsonProps.length
|
||||
|
||||
const nodeStart = h('div', {
|
||||
key: 'node',
|
||||
onKeyDown: this.handleKeyDown,
|
||||
|
@ -71,26 +81,27 @@ export default class JSONNode extends PureComponent {
|
|||
this.renderProperty(),
|
||||
this.renderSeparator(),
|
||||
this.renderDelimiter('{', 'jsoneditor-delimiter-start'),
|
||||
!meta.expanded
|
||||
!this.props.eson[EXPANDED]
|
||||
? [
|
||||
this.renderTag(`${props.length} ${props.length === 1 ? 'prop' : 'props'}`,
|
||||
`Object containing ${props.length} ${props.length === 1 ? 'property' : 'properties'}`),
|
||||
this.renderTag(`${jsonPropsCount} ${jsonPropsCount === 1 ? 'prop' : 'props'}`,
|
||||
`Object containing ${jsonPropsCount} ${jsonPropsCount === 1 ? 'property' : 'properties'}`),
|
||||
this.renderDelimiter('}', 'jsoneditor-delimiter-end jsoneditor-delimiter-collapsed'),
|
||||
this.renderInsertAfter()
|
||||
]
|
||||
: [
|
||||
this.renderInsertBefore()
|
||||
],
|
||||
this.renderError(meta.error)
|
||||
this.renderError(this.props.eson[ERROR])
|
||||
])
|
||||
|
||||
let childs
|
||||
if (meta.expanded) {
|
||||
if (props.length > 0) {
|
||||
const propsChilds = props.map(prop => h(this.constructor, {
|
||||
key: this.props.value[prop][META].id,
|
||||
if (this.props.eson[EXPANDED]) {
|
||||
if (jsonPropsCount > 0) {
|
||||
const propsChilds = jsonProps.map((prop) => h(this.constructor, {
|
||||
key: eson[prop][ID],
|
||||
parentPath: this.state.path,
|
||||
prop,
|
||||
value: this.props.value[prop],
|
||||
eson: eson[prop],
|
||||
emit: this.props.emit,
|
||||
findKeyBinding: this.props.findKeyBinding,
|
||||
options: this.props.options
|
||||
|
@ -105,8 +116,9 @@ export default class JSONNode extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
const floatingMenu = this.renderFloatingMenu('Object', meta.selected)
|
||||
const nodeEnd = meta.expanded
|
||||
// FIXME
|
||||
const floatingMenu = this.renderFloatingMenu('object', this.props.eson[SELECTION])
|
||||
const nodeEnd = this.props.eson[EXPANDED]
|
||||
? h('div', {key: 'node-end', className: 'jsoneditor-node-end', 'data-area': 'empty'}, [
|
||||
this.renderDelimiter('}', 'jsoneditor-delimiter-end'),
|
||||
this.renderInsertAfter()
|
||||
|
@ -114,9 +126,9 @@ export default class JSONNode extends PureComponent {
|
|||
: null
|
||||
|
||||
return h('div', {
|
||||
'data-path': compileJSONPointer(meta.path),
|
||||
'data-path': compileJSONPointer(this.state.path),
|
||||
'data-area': 'empty',
|
||||
className: this.getContainerClassName(meta.selected, this.state.hover),
|
||||
className: this.getContainerClassName(this.props.eson[SELECTION], this.state.hover),
|
||||
// onMouseOver: this.handleMouseOver,
|
||||
// onMouseLeave: this.handleMouseLeave
|
||||
}, [floatingMenu, nodeStart, childs, nodeEnd])
|
||||
|
@ -124,8 +136,7 @@ export default class JSONNode extends PureComponent {
|
|||
|
||||
renderJSONArray () {
|
||||
// TODO: refactor renderJSONArray (too large/complex)
|
||||
const meta = this.props.value[META]
|
||||
const count = this.props.value.length
|
||||
const count = this.props.eson.length
|
||||
const nodeStart = h('div', {
|
||||
key: 'node',
|
||||
onKeyDown: this.handleKeyDown,
|
||||
|
@ -135,25 +146,27 @@ export default class JSONNode extends PureComponent {
|
|||
this.renderProperty(),
|
||||
this.renderSeparator(),
|
||||
this.renderDelimiter('[', 'jsoneditor-delimiter-start'),
|
||||
!meta.expanded
|
||||
!this.props.eson[EXPANDED]
|
||||
? [
|
||||
this.renderTag(`${count} ${count === 1 ? 'item' : 'items'}`,
|
||||
`Array containing ${count} item${count === 1 ? 'item' : 'items'}`),
|
||||
`Array containing ${count} ${count === 1 ? 'item' : 'items'}`),
|
||||
this.renderDelimiter(']', 'jsoneditor-delimiter-end jsoneditor-delimiter-collapsed'),
|
||||
this.renderInsertAfter(),
|
||||
]
|
||||
: [
|
||||
this.renderInsertBefore()
|
||||
],
|
||||
this.renderError(meta.error)
|
||||
this.renderError(this.props.eson[ERROR])
|
||||
])
|
||||
|
||||
let childs
|
||||
if (meta.expanded) {
|
||||
if (this.props.eson[EXPANDED]) {
|
||||
if (count > 0) {
|
||||
const items = this.props.value.map(item => h(this.constructor, {
|
||||
key : item[META].id,
|
||||
value: item,
|
||||
const items = this.props.eson.map((item, index) => h(this.constructor, {
|
||||
key: item[ID],
|
||||
parentPath: this.state.path,
|
||||
index,
|
||||
eson: item,
|
||||
options: this.props.options,
|
||||
emit: this.props.emit,
|
||||
findKeyBinding: this.props.findKeyBinding
|
||||
|
@ -168,8 +181,8 @@ export default class JSONNode extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
const floatingMenu = this.renderFloatingMenu('Array', meta.selected)
|
||||
const nodeEnd = meta.expanded
|
||||
const floatingMenu = this.renderFloatingMenu('array', this.props.eson[SELECTION])
|
||||
const nodeEnd = this.props.eson[EXPANDED]
|
||||
? h('div', {key: 'node-end', className: 'jsoneditor-node-end', 'data-area': 'empty'}, [
|
||||
this.renderDelimiter(']', 'jsoneditor-delimiter-end'),
|
||||
this.renderInsertAfter()
|
||||
|
@ -177,16 +190,15 @@ export default class JSONNode extends PureComponent {
|
|||
: null
|
||||
|
||||
return h('div', {
|
||||
'data-path': compileJSONPointer(meta.path),
|
||||
'data-path': compileJSONPointer(this.state.path),
|
||||
'data-area': 'empty',
|
||||
className: this.getContainerClassName(meta.selected, this.state.hover),
|
||||
className: this.getContainerClassName(this.props.eson[SELECTION], this.state.hover),
|
||||
// onMouseOver: this.handleMouseOver,
|
||||
// onMouseLeave: this.handleMouseLeave
|
||||
}, [floatingMenu, nodeStart, childs, nodeEnd])
|
||||
}
|
||||
|
||||
renderJSONValue () {
|
||||
const meta = this.props.value[META]
|
||||
const node = h('div', {
|
||||
key: 'node',
|
||||
onKeyDown: this.handleKeyDown,
|
||||
|
@ -195,19 +207,19 @@ export default class JSONNode extends PureComponent {
|
|||
this.renderPlaceholder(),
|
||||
this.renderProperty(),
|
||||
this.renderSeparator(),
|
||||
this.renderValue(meta.value, meta.searchValue, this.props.options),
|
||||
this.renderValue(this.props.eson[VALUE], this.props.eson[SEARCH_VALUE], this.props.options), // FIXME
|
||||
this.renderInsertAfter(),
|
||||
this.renderError(meta.error)
|
||||
this.renderError(this.props.eson[ERROR])
|
||||
])
|
||||
|
||||
const floatingMenu = this.renderFloatingMenu('value', meta.selected)
|
||||
const floatingMenu = this.renderFloatingMenu('value', this.props.eson[SELECTION])
|
||||
|
||||
// const insertArea = this.renderInsertBeforeArea()
|
||||
|
||||
return h('div', {
|
||||
'data-path': compileJSONPointer(meta.path),
|
||||
'data-path': compileJSONPointer(this.state.path),
|
||||
'data-area': 'empty',
|
||||
className: this.getContainerClassName(meta.selected, this.state.hover),
|
||||
className: this.getContainerClassName(this.props.eson[SELECTION], this.state.hover),
|
||||
// onMouseOver: this.handleMouseOver,
|
||||
// onMouseLeave: this.handleMouseLeave
|
||||
}, [node, floatingMenu])
|
||||
|
@ -238,7 +250,7 @@ export default class JSONNode extends PureComponent {
|
|||
*/
|
||||
renderAppend (text) {
|
||||
return h('div', {
|
||||
'data-path': compileJSONPointer(this.props.value[META].path) + '/-',
|
||||
'data-path': compileJSONPointer(this.state.path) + '/-',
|
||||
'data-area': 'empty',
|
||||
className: 'jsoneditor-node',
|
||||
onKeyDown: this.handleKeyDownAppend
|
||||
|
@ -287,10 +299,10 @@ export default class JSONNode extends PureComponent {
|
|||
}
|
||||
|
||||
const editable = !this.props.options.isPropertyEditable ||
|
||||
this.props.options.isPropertyEditable(this.props.value[META].path)
|
||||
this.props.options.isPropertyEditable(this.state.path)
|
||||
|
||||
const emptyClassName = (this.props.prop != null && this.props.prop.length === 0) ? ' jsoneditor-empty' : ''
|
||||
const searchClassName = this.props.prop != null ? JSONNode.getSearchResultClass(this.props.value[META].searchProperty) : ''
|
||||
const searchClassName = this.props.prop != null ? JSONNode.getSearchResultClass(this.props.eson[SEARCH_PROPERTY]) : ''
|
||||
const escapedPropName = this.props.prop != null ? escapeHTML(this.props.prop, this.props.options.escapeUnicode) : null
|
||||
|
||||
if (editable) {
|
||||
|
@ -341,7 +353,7 @@ export default class JSONNode extends PureComponent {
|
|||
const itsAnUrl = isUrl(value)
|
||||
const isEmpty = escapedValue.length === 0
|
||||
|
||||
const editable = !options.isValueEditable || options.isValueEditable(this.props.value[META].path)
|
||||
const editable = !options.isValueEditable || options.isValueEditable(this.state.path)
|
||||
if (editable) {
|
||||
return h('div', {
|
||||
key: 'value',
|
||||
|
@ -460,7 +472,7 @@ export default class JSONNode extends PureComponent {
|
|||
}
|
||||
|
||||
target.className = JSONNode.getValueClass(type, itsAnUrl, isEmpty) +
|
||||
JSONNode.getSearchResultClass(this.props.value[META].searchValue)
|
||||
JSONNode.getSearchResultClass(this.props.eson[SEARCH_VALUE])
|
||||
target.title = itsAnUrl ? JSONNode.URL_TITLE : ''
|
||||
|
||||
// remove all classNames from childs (needed for IE and Edge)
|
||||
|
@ -514,7 +526,7 @@ export default class JSONNode extends PureComponent {
|
|||
}
|
||||
|
||||
renderExpandButton () {
|
||||
const className = `jsoneditor-button jsoneditor-${this.props.value[META].expanded ? 'expanded' : 'collapsed'}`
|
||||
const className = `jsoneditor-button jsoneditor-${this.props.eson[EXPANDED] ? 'expanded' : 'collapsed'}`
|
||||
|
||||
return h('div', {key: 'expand', className: 'jsoneditor-button-container'},
|
||||
h('button', {
|
||||
|
@ -541,7 +553,7 @@ export default class JSONNode extends PureComponent {
|
|||
|
||||
return h(FloatingMenu, {
|
||||
key: 'floating-menu',
|
||||
path: this.props.value[META].path,
|
||||
path: this.state.path,
|
||||
emit: this.props.emit,
|
||||
items: this.getFloatingMenuItems(type, selected),
|
||||
position: isLastOfMultiple || isAfter ? 'bottom' : 'top'
|
||||
|
@ -569,7 +581,7 @@ export default class JSONNode extends PureComponent {
|
|||
]
|
||||
}
|
||||
|
||||
if (type === 'Object') {
|
||||
if (type === 'object') {
|
||||
return [
|
||||
{type: 'sort'},
|
||||
{type: 'duplicate'},
|
||||
|
@ -580,7 +592,7 @@ export default class JSONNode extends PureComponent {
|
|||
]
|
||||
}
|
||||
|
||||
if (type === 'Array') {
|
||||
if (type === 'array') {
|
||||
return [
|
||||
{type: 'sort'},
|
||||
{type: 'duplicate'},
|
||||
|
@ -636,12 +648,12 @@ export default class JSONNode extends PureComponent {
|
|||
static getRootName (value, options) {
|
||||
return typeof options.name === 'string'
|
||||
? options.name
|
||||
: value[META].type
|
||||
: value[TYPE]
|
||||
}
|
||||
|
||||
/** @private */
|
||||
handleChangeProperty = (event) => {
|
||||
const parentPath = initial(this.props.value[META].path)
|
||||
const parentPath = initial(this.state.path)
|
||||
const oldProp = this.props.prop
|
||||
const newProp = unescapeHTML(getInnerText(event.target))
|
||||
|
||||
|
@ -653,9 +665,9 @@ export default class JSONNode extends PureComponent {
|
|||
/** @private */
|
||||
handleChangeValue = (event) => {
|
||||
const value = this.getValueFromEvent(event)
|
||||
const path = this.props.value[META].path
|
||||
const path = this.state.path
|
||||
|
||||
if (value !== this.props.value[META].value) {
|
||||
if (value !== this.props.eson[VALUE]) {
|
||||
this.props.emit('changeValue', {path, value})
|
||||
}
|
||||
}
|
||||
|
@ -670,7 +682,7 @@ export default class JSONNode extends PureComponent {
|
|||
/** @private */
|
||||
handleKeyDown = (event) => {
|
||||
const keyBinding = this.props.findKeyBinding(event)
|
||||
const path = this.props.value[META].path
|
||||
const path = this.state.path
|
||||
|
||||
if (keyBinding === 'duplicate') {
|
||||
event.preventDefault()
|
||||
|
@ -690,7 +702,7 @@ export default class JSONNode extends PureComponent {
|
|||
if (keyBinding === 'expand') {
|
||||
event.preventDefault()
|
||||
const recurse = false
|
||||
const expanded = !this.props.value[META].expanded
|
||||
const expanded = !this.props.eson[EXPANDED]
|
||||
this.props.emit('expand', {path, expanded, recurse})
|
||||
}
|
||||
|
||||
|
@ -703,7 +715,7 @@ export default class JSONNode extends PureComponent {
|
|||
/** @private */
|
||||
handleKeyDownAppend = (event) => {
|
||||
const keyBinding = this.props.findKeyBinding(event)
|
||||
const path = this.props.value[META].path
|
||||
const path = this.state.path
|
||||
|
||||
if (keyBinding === 'insert') {
|
||||
event.preventDefault()
|
||||
|
@ -728,8 +740,8 @@ export default class JSONNode extends PureComponent {
|
|||
/** @private */
|
||||
handleExpand = (event) => {
|
||||
const recurse = event.ctrlKey
|
||||
const path = this.props.value[META].path
|
||||
const expanded = !this.props.value[META].expanded
|
||||
const path = this.state.path
|
||||
const expanded = !this.props.eson[EXPANDED]
|
||||
|
||||
this.props.emit('expand', {path, expanded, recurse})
|
||||
}
|
||||
|
@ -758,7 +770,7 @@ export default class JSONNode extends PureComponent {
|
|||
*/
|
||||
getValueFromEvent (event) {
|
||||
const stringValue = unescapeHTML(getInnerText(event.target))
|
||||
return this.props.value[META].type === 'string'
|
||||
return this.state.type === 'string' // FIXME
|
||||
? stringValue
|
||||
: stringConvert(stringValue)
|
||||
}
|
||||
|
|
|
@ -3,12 +3,11 @@ import Ajv from 'ajv'
|
|||
import { parseJSON } from '../utils/jsonUtils'
|
||||
import { escapeUnicodeChars } from '../utils/stringUtils'
|
||||
import { enrichSchemaError, limitErrors } from '../utils/schemaUtils'
|
||||
import { jsonToEson, esonToJson } from '../eson'
|
||||
import { patchEson } from '../patchEson'
|
||||
import { createFindKeyBinding } from '../utils/keyBindings'
|
||||
import { KEY_BINDINGS } from '../constants'
|
||||
|
||||
import ModeButton from './menu/ModeButton'
|
||||
import { immutableJsonPatch } from '../immutableJsonPatch'
|
||||
|
||||
const AJV_OPTIONS = {
|
||||
allErrors: true,
|
||||
|
@ -331,10 +330,9 @@ export default class TextMode extends Component {
|
|||
patch (actions) {
|
||||
const json = this.get()
|
||||
|
||||
const data = jsonToEson(json)
|
||||
const result = patchEson(data, actions)
|
||||
const result = immutableJsonPatch(json, actions)
|
||||
|
||||
this.set(esonToJson(result.data))
|
||||
this.set(result.data)
|
||||
|
||||
return {
|
||||
patch: actions,
|
||||
|
|
|
@ -8,21 +8,24 @@ import Hammer from 'react-hammerjs'
|
|||
import jump from '../assets/jump.js/src/jump'
|
||||
import Ajv from 'ajv'
|
||||
|
||||
import { getIn, updateIn } from '../utils/immutabilityHelpers'
|
||||
import { existsIn, setIn, updateIn } from '../utils/immutabilityHelpers'
|
||||
import { parseJSON } from '../utils/jsonUtils'
|
||||
import { enrichSchemaError } from '../utils/schemaUtils'
|
||||
import { compileJSONPointer, parseJSONPointer } from '../jsonPointer'
|
||||
import {
|
||||
META,
|
||||
jsonToEson, esonToJson, pathExists,
|
||||
expand, expandOne, expandPath, applyErrors,
|
||||
search, nextSearchResult, previousSearchResult,
|
||||
applySelection, pathsFromSelection, contentsFromPaths,
|
||||
compileJSONPointer, parseJSONPointer
|
||||
} from '../eson'
|
||||
import { patchEson } from '../patchEson'
|
||||
import {
|
||||
duplicate, insertBefore, insertAfter, insertInside, append, remove, removeAll, replace,
|
||||
createEntry, changeType, changeValue, changeProperty, sort
|
||||
append,
|
||||
changeProperty,
|
||||
changeType,
|
||||
changeValue,
|
||||
createEntry,
|
||||
duplicate,
|
||||
insertAfter,
|
||||
insertBefore,
|
||||
insertInside,
|
||||
remove,
|
||||
removeAll,
|
||||
replace,
|
||||
sort
|
||||
} from '../actions'
|
||||
import JSONNode from './JSONNode'
|
||||
import JSONNodeView from './JSONNodeView'
|
||||
|
@ -30,11 +33,32 @@ import JSONNodeForm from './JSONNodeForm'
|
|||
import ModeButton from './menu/ModeButton'
|
||||
import Search from './menu/Search'
|
||||
import {
|
||||
moveUp, moveDown, moveLeft, moveRight, moveDownSibling, moveHome, moveEnd,
|
||||
findNode, findBaseNode, selectFind, searchHasFocus, setSelection
|
||||
findBaseNode,
|
||||
findNode,
|
||||
moveDown,
|
||||
moveDownSibling,
|
||||
moveEnd,
|
||||
moveHome,
|
||||
moveLeft,
|
||||
moveRight,
|
||||
moveUp,
|
||||
searchHasFocus,
|
||||
selectFind,
|
||||
setSelection
|
||||
} from './utils/domSelector'
|
||||
import { createFindKeyBinding } from '../utils/keyBindings'
|
||||
import { KEY_BINDINGS } from '../constants'
|
||||
import { immutableJsonPatch } from '../immutableJsonPatch'
|
||||
import {
|
||||
applyErrors, applySelection, contentsFromPaths,
|
||||
expand,
|
||||
EXPANDED,
|
||||
expandPath,
|
||||
nextSearchResult, pathsFromSelection, previousSearchResult,
|
||||
search,
|
||||
syncEson,
|
||||
toEsonPatchAction
|
||||
} from '../eson'
|
||||
|
||||
const AJV_OPTIONS = {
|
||||
allErrors: true,
|
||||
|
@ -56,12 +80,9 @@ export default class TreeMode extends PureComponent {
|
|||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
// const json = this.props.json || {}
|
||||
// const expandCallback = this.props.expand || TreeMode.expandRoot
|
||||
// const eson = expand(jsonToEson(json), expandCallback)
|
||||
|
||||
const json = {}
|
||||
const eson = jsonToEson(json)
|
||||
const expandCallback = this.props.expand || TreeMode.expandRoot
|
||||
const eson = expand(syncEson(json, {}), expandCallback)
|
||||
|
||||
this.keyDownActions = {
|
||||
'up': this.moveUp,
|
||||
|
@ -155,14 +176,11 @@ export default class TreeMode extends PureComponent {
|
|||
|
||||
// Apply json
|
||||
if (nextProps.json !== this.state.json) {
|
||||
// FIXME: merge meta data from existing eson
|
||||
const callback = this.props.expand || TreeMode.expandRoot
|
||||
const json = nextProps.json
|
||||
const eson = expand(jsonToEson(json), callback)
|
||||
|
||||
this.setState({
|
||||
json,
|
||||
eson
|
||||
eson: syncEson(json, this.state.eson)
|
||||
})
|
||||
// FIXME: use patch again -> patch should keep existing meta data when for the unchanged parts of the json
|
||||
// this.patch([{
|
||||
|
@ -231,9 +249,10 @@ export default class TreeMode extends PureComponent {
|
|||
onMouseDown: this.handleTouchStart,
|
||||
onTouchStart: this.handleTouchStart,
|
||||
className: 'jsoneditor-list jsoneditor-root' +
|
||||
(eson[META].selected ? ' jsoneditor-selected' : '')},
|
||||
(/*eson[META].selected*/ false ? ' jsoneditor-selected' : '')}, // FIXME
|
||||
h(Node, {
|
||||
value: eson,
|
||||
path: [],
|
||||
eson,
|
||||
emit: this.emitter.emit,
|
||||
findKeyBinding: this.findKeyBinding,
|
||||
options: this.state.options
|
||||
|
@ -603,7 +622,7 @@ export default class TreeMode extends PureComponent {
|
|||
}
|
||||
else {
|
||||
this.setState({
|
||||
eson: expandOne(this.state.eson, path, expanded)
|
||||
eson: setIn(this.state.eson, path.concat(EXPANDED), expanded)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -626,6 +645,7 @@ export default class TreeMode extends PureComponent {
|
|||
}
|
||||
|
||||
handleSearch = (text) => {
|
||||
// FIXME
|
||||
// FIXME: also apply search when eson is changed
|
||||
const { eson, searchResult } = search(this.state.eson, text)
|
||||
if (searchResult.matches.length > 0) {
|
||||
|
@ -639,7 +659,7 @@ export default class TreeMode extends PureComponent {
|
|||
}
|
||||
else {
|
||||
this.setState({
|
||||
eson,
|
||||
eson: eson,
|
||||
searchResult
|
||||
})
|
||||
}
|
||||
|
@ -657,7 +677,7 @@ export default class TreeMode extends PureComponent {
|
|||
const { eson, searchResult } = nextSearchResult(this.state.eson, this.state.searchResult)
|
||||
|
||||
this.setState({
|
||||
eson,
|
||||
eson: expandPath(eson, initial(searchResult.active.path)),
|
||||
searchResult
|
||||
})
|
||||
|
||||
|
@ -682,7 +702,7 @@ export default class TreeMode extends PureComponent {
|
|||
const { eson, searchResult } = previousSearchResult(this.state.eson, this.state.searchResult)
|
||||
|
||||
this.setState({
|
||||
eson,
|
||||
eson: expandPath(eson, initial(searchResult.active.path)),
|
||||
searchResult
|
||||
})
|
||||
|
||||
|
@ -700,15 +720,15 @@ export default class TreeMode extends PureComponent {
|
|||
}
|
||||
|
||||
/**
|
||||
* Apply a ESONPatch to the current JSON document and emit a change event
|
||||
* @param {ESONPatch} actions
|
||||
* Apply a JSONPatch to the current JSON document and emit a change event
|
||||
* @param {JSONPatch} actions
|
||||
* @private
|
||||
*/
|
||||
handlePatch = (actions) => {
|
||||
// apply changes
|
||||
const result = this.patch(actions)
|
||||
|
||||
this.emitOnChange (actions, result.revert, result.eson, result.json)
|
||||
this.emitOnChange (actions, result.revert, result.json)
|
||||
}
|
||||
|
||||
handleTouchStart = (event) => {
|
||||
|
@ -717,15 +737,15 @@ export default class TreeMode extends PureComponent {
|
|||
return
|
||||
}
|
||||
|
||||
const pointer = this.findESONPointerFromElement(event.target)
|
||||
const pointer = this.findJSONPointerFromElement(event.target)
|
||||
const clickedOnEmptySpace = (event.target.nodeName === 'DIV') &&
|
||||
(event.target.contentEditable !== 'true')
|
||||
|
||||
// TODO: cleanup
|
||||
// console.log('handleTouchStart', clickedOnEmptySpace && pointer, pointer && this.selectionFromESONPointer(pointer))
|
||||
// console.log('handleTouchStart', clickedOnEmptySpace && pointer, pointer && this.selectionFromJSONPointer(pointer))
|
||||
|
||||
if (clickedOnEmptySpace && pointer) {
|
||||
this.setState({ selection: this.selectionFromESONPointer(pointer)})
|
||||
this.setState({ selection: this.selectionFromJSONPointer(pointer)})
|
||||
}
|
||||
else {
|
||||
this.setState({ selection: null })
|
||||
|
@ -780,11 +800,11 @@ export default class TreeMode extends PureComponent {
|
|||
}
|
||||
|
||||
/**
|
||||
* Find ESON pointer from an HTML element
|
||||
* Find JSON pointer from an HTML element
|
||||
* @param {Element} element
|
||||
* @return {ESONPointer | null}
|
||||
*/
|
||||
findESONPointerFromElement (element) {
|
||||
findJSONPointerFromElement (element) {
|
||||
const path = this.findDataPathFromElement(element)
|
||||
const area = (element && element.getAttribute && element.getAttribute('data-area')) || null
|
||||
|
||||
|
@ -792,11 +812,11 @@ export default class TreeMode extends PureComponent {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get selection from an ESON pointer
|
||||
* Get selection from an JSONPointer
|
||||
* @param {ESONPointer} pointer
|
||||
* @return {Selection}
|
||||
*/
|
||||
selectionFromESONPointer (pointer) {
|
||||
selectionFromJSONPointer (pointer) {
|
||||
// FIXME: does pointer have .area === 'after' ? if so adjust type defs
|
||||
if (pointer.area === 'after') {
|
||||
return {after: pointer.path}
|
||||
|
@ -836,10 +856,9 @@ export default class TreeMode extends PureComponent {
|
|||
* @private
|
||||
* @param {ESONPatch} patch
|
||||
* @param {ESONPatch} revert
|
||||
* @param {ESON} eson
|
||||
* @param {JSON} json
|
||||
*/
|
||||
emitOnChange (patch, revert, eson, json) {
|
||||
emitOnChange (patch, revert, json) {
|
||||
const onPatch = this.props.onPatch
|
||||
if (onPatch) {
|
||||
setTimeout(() => onPatch(patch, revert))
|
||||
|
@ -885,16 +904,18 @@ export default class TreeMode extends PureComponent {
|
|||
const historyIndex = this.state.historyIndex
|
||||
const historyItem = history[historyIndex]
|
||||
|
||||
const result = patchEson(this.state.eson, historyItem.undo)
|
||||
const jsonResult = immutableJsonPatch(this.state.json, historyItem.undo)
|
||||
const esonResult = immutableJsonPatch(this.state.eson, historyItem.undo.map(toEsonPatchAction))
|
||||
|
||||
// FIXME: apply search
|
||||
this.setState({
|
||||
eson: result.data,
|
||||
json: jsonResult.json,
|
||||
eson: esonResult.json,
|
||||
history,
|
||||
historyIndex: historyIndex + 1
|
||||
})
|
||||
|
||||
this.emitOnChange(historyItem.undo, historyItem.redo, result.data, esonToJson(result.data))
|
||||
this.emitOnChange(historyItem.undo, historyItem.redo, jsonResult.json)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -904,45 +925,43 @@ export default class TreeMode extends PureComponent {
|
|||
const historyIndex = this.state.historyIndex - 1
|
||||
const historyItem = history[historyIndex]
|
||||
|
||||
const result = patchEson(this.state.eson, historyItem.redo)
|
||||
const jsonResult = immutableJsonPatch(this.state.json, historyItem.redo)
|
||||
const esonResult = immutableJsonPatch(this.state.eson, historyItem.undo.map(toEsonPatchAction))
|
||||
|
||||
// FIXME: apply search
|
||||
this.setState({
|
||||
eson: result.data,
|
||||
json: jsonResult.json,
|
||||
eson: esonResult.json,
|
||||
history,
|
||||
historyIndex
|
||||
})
|
||||
|
||||
this.emitOnChange(historyItem.redo, historyItem.undo, result.data, esonToJson(result.data))
|
||||
this.emitOnChange(historyItem.redo, historyItem.undo, jsonResult.json)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a ESONPatch to the current JSON document
|
||||
* @param {ESONPatch} actions ESONPatch actions
|
||||
* @param {ESONPatchOptions} [options] If no expand function is provided, the
|
||||
* expanded state will be kept as is for
|
||||
* existing paths. New paths will be fully
|
||||
* expanded.
|
||||
* @return {ESONPatchAction} Returns a ESONPatch result containing the
|
||||
* patch, a patch to revert the action, and
|
||||
* an error object which is null when successful
|
||||
* Apply a JSONPatch to the current JSON document
|
||||
* @param {JSONPatch} actions ESONPatch actions
|
||||
* @return {Object} Returns a object result containing the
|
||||
* patch, a patch to revert the action, and
|
||||
* an error object which is null when successful
|
||||
*/
|
||||
patch (actions, options = {}) {
|
||||
patch (actions) {
|
||||
if (!Array.isArray(actions)) {
|
||||
throw new TypeError('Array with patch actions expected')
|
||||
}
|
||||
|
||||
const expand = options.expand || (path => this.expandKeepOrExpandAll(path))
|
||||
const result = patchEson(this.state.eson, actions, expand)
|
||||
const eson = result.data
|
||||
const json = esonToJson(eson) // FIXME: apply the patch to the json too, instead of completely replacing it
|
||||
console.log('patch', actions)
|
||||
|
||||
const jsonResult = immutableJsonPatch(this.state.json, actions)
|
||||
const esonResult = immutableJsonPatch(this.state.eson, actions.map(toEsonPatchAction))
|
||||
|
||||
if (this.props.history !== false) {
|
||||
// update data and store history
|
||||
const historyItem = {
|
||||
redo: actions,
|
||||
undo: result.revert
|
||||
undo: jsonResult.revert
|
||||
}
|
||||
|
||||
const history = [historyItem]
|
||||
|
@ -951,8 +970,8 @@ export default class TreeMode extends PureComponent {
|
|||
|
||||
// FIXME: apply search
|
||||
this.setState({
|
||||
eson,
|
||||
json,
|
||||
json: jsonResult.json,
|
||||
eson: esonResult.json,
|
||||
history,
|
||||
historyIndex: 0
|
||||
})
|
||||
|
@ -961,18 +980,16 @@ export default class TreeMode extends PureComponent {
|
|||
// update data and don't store history
|
||||
// FIXME: apply search
|
||||
this.setState({
|
||||
eson,
|
||||
json
|
||||
json: jsonResult.json,
|
||||
eson: esonResult.json
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
patch: actions,
|
||||
revert: result.revert,
|
||||
error: result.error,
|
||||
data: eson, // FIXME: shouldn't pass data here?
|
||||
eson, // FIXME: shouldn't pass eson here
|
||||
json // FIXME: shouldn't pass json here
|
||||
revert: jsonResult.revert,
|
||||
error: jsonResult.error,
|
||||
json: jsonResult.json // FIXME: shouldn't pass json here?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -982,13 +999,11 @@ export default class TreeMode extends PureComponent {
|
|||
*/
|
||||
set (json) {
|
||||
// FIXME: when both json and expand are being changed via React, this.props must be updated before set(json) is called
|
||||
// TODO: document option expand
|
||||
const expandCallback = this.props.expand || TreeMode.expandRoot
|
||||
|
||||
// FIXME: apply search
|
||||
this.setState({
|
||||
json: json,
|
||||
eson: expand(jsonToEson(json), expandCallback),
|
||||
json,
|
||||
eson: syncEson(json, this.state.eson), // FIXME: reset eson in set, keep in update?
|
||||
|
||||
// TODO: do we want to keep history when .set(json) is called? (currently we remove history)
|
||||
history: [],
|
||||
|
@ -1090,29 +1105,7 @@ export default class TreeMode extends PureComponent {
|
|||
* @param {Path} path
|
||||
*/
|
||||
exists (path) {
|
||||
return pathExists(this.state.eson, path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether an Array or Object at a certain path is expanded.
|
||||
* When the node does not exist, the function throws an error
|
||||
* @param {Path} path
|
||||
* @return {boolean} Returns true when expanded, false otherwise
|
||||
*/
|
||||
isExpanded (path) {
|
||||
return getIn(this.state.eson, path)[META].expanded
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand function which keeps the expanded state the same as the current data.
|
||||
* When the path doesn't yet exist, it will be expanded.
|
||||
* @param {Path} path
|
||||
* @return {boolean}
|
||||
*/
|
||||
expandKeepOrExpandAll (path) {
|
||||
return this.exists(path)
|
||||
? this.isExpanded(path)
|
||||
: TreeMode.expandAll(path)
|
||||
return existsIn(this.state.json, path)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -101,14 +101,14 @@ const CREATE_TYPE = {
|
|||
insertObjectAfter: (path, emit) => h('button', {
|
||||
key: 'insertObjectAfter',
|
||||
className: MENU_ITEM_CLASS_NAME,
|
||||
onClick: () => emit('insertAfter', {path, type: 'Object'}),
|
||||
onClick: () => emit('insertAfter', {path, type: 'object'}),
|
||||
title: 'Insert Object'
|
||||
}, 'Insert Object'),
|
||||
|
||||
insertArrayAfter: (path, emit) => h('button', {
|
||||
key: 'insertArrayAfter',
|
||||
className: MENU_ITEM_CLASS_NAME,
|
||||
onClick: () => emit('insertAfter', {path, type: 'Array'}),
|
||||
onClick: () => emit('insertAfter', {path, type: 'array'}),
|
||||
title: 'Insert Array'
|
||||
}, 'Insert Array'),
|
||||
|
||||
|
@ -129,14 +129,14 @@ const CREATE_TYPE = {
|
|||
insertObjectInside: (path, emit) => h('button', {
|
||||
key: 'insertObjectInside',
|
||||
className: MENU_ITEM_CLASS_NAME,
|
||||
onClick: () => emit('insertInside', {path, type: 'Object'}),
|
||||
onClick: () => emit('insertInside', {path, type: 'object'}),
|
||||
title: 'Insert Object'
|
||||
}, 'Insert Object'),
|
||||
|
||||
insertArrayInside: (path, emit) => h('button', {
|
||||
key: 'insertArrayInside',
|
||||
className: MENU_ITEM_CLASS_NAME,
|
||||
onClick: () => emit('insertInside', {path, type: 'Array'}),
|
||||
onClick: () => emit('insertInside', {path, type: 'array'}),
|
||||
title: 'Insert Array'
|
||||
}, 'Insert Array'),
|
||||
|
||||
|
@ -164,7 +164,9 @@ export default class FloatingMenu extends PureComponent {
|
|||
})
|
||||
]).isRequired
|
||||
).isRequired,
|
||||
path: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
path: PropTypes.arrayOf(PropTypes.oneOfType([
|
||||
PropTypes.string, PropTypes.number
|
||||
])).isRequired,
|
||||
emit: PropTypes.func.isRequired,
|
||||
position: PropTypes.string // 'top' or 'bottom'
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import {
|
|||
selectContentEditable, hasClassName,
|
||||
findParentWithAttribute, findParentWithClassName
|
||||
} from '../../utils/domUtils'
|
||||
import { compileJSONPointer, parseJSONPointer } from '../../eson'
|
||||
import { compileJSONPointer, parseJSONPointer } from '../../jsonPointer'
|
||||
|
||||
// singleton
|
||||
let lastInputName = null
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
/**
|
||||
* This file contains functions to act on a ESON object.
|
||||
* All functions are pure and don't mutate the ESON.
|
||||
*/
|
||||
|
||||
import { setIn, getIn, updateIn, deleteIn, cloneWithSymbols } from './utils/immutabilityHelpers'
|
||||
import { isObject } from './utils/typeUtils'
|
||||
import isEqual from 'lodash/isEqual'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import { deleteIn, getIn, setIn, shallowCloneWithSymbols, transform, updateIn } from './utils/immutabilityHelpers'
|
||||
import range from 'lodash/range'
|
||||
import times from 'lodash/times'
|
||||
import initial from 'lodash/initial'
|
||||
import { compileJSONPointer, parseJSONPointer } from './jsonPointer'
|
||||
import last from 'lodash/last'
|
||||
import initial from 'lodash/initial'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isEqual from 'lodash/isEqual'
|
||||
import naturalSort from 'javascript-natural-sort'
|
||||
import times from 'lodash/times'
|
||||
|
||||
export const ID = typeof Symbol === 'function' ? Symbol('id') : '@jsoneditor-id'
|
||||
export const TYPE = typeof Symbol === 'function' ? Symbol('type') : '@jsoneditor-type' // 'object', 'array', 'value', or 'undefined'
|
||||
export const VALUE = typeof Symbol === 'function' ? Symbol('value') : '@jsoneditor-value'
|
||||
export const EXPANDED = typeof Symbol === 'function' ? Symbol('expanded') : '@jsoneditor-expanded'
|
||||
export const ERROR = typeof Symbol === 'function' ? Symbol('error') : '@jsoneditor-error'
|
||||
export const SEARCH_PROPERTY = typeof Symbol === 'function' ? Symbol('searchProperty') : '@jsoneditor-search-property'
|
||||
export const SEARCH_VALUE = typeof Symbol === 'function' ? Symbol('searchValue') : '@jsoneditor-search-value'
|
||||
export const SELECTION = typeof Symbol === 'function' ? Symbol('selection') : '@jsoneditor-selection'
|
||||
|
||||
export const SELECTED = 1
|
||||
export const SELECTED_START = 2
|
||||
|
@ -22,122 +27,88 @@ export const SELECTED_AFTER = 64
|
|||
export const SELECTED_EMPTY = 128
|
||||
export const SELECTED_EMPTY_BEFORE = 256
|
||||
|
||||
export const META = Symbol('meta')
|
||||
// TODO: comment
|
||||
export function syncEson(json, eson) {
|
||||
const jsonType = getType(json)
|
||||
const esonType = eson ? eson[TYPE] : 'undefined'
|
||||
const sameType = jsonType === esonType
|
||||
|
||||
/**
|
||||
* Expand function which will expand all nodes
|
||||
* @param {Path} path
|
||||
* @return {boolean}
|
||||
*/
|
||||
export function expandAll (path) {
|
||||
return true
|
||||
}
|
||||
if (jsonType === 'array') {
|
||||
// TODO: instead of creating updatedEson beforehand, only created as soon as we have a changed item
|
||||
let changed = (esonType !== jsonType) || (eson.length !== esonType.length)
|
||||
let updatedEson = []
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {JSON} json
|
||||
* @param {Path} path
|
||||
* @return {ESON}
|
||||
*/
|
||||
export function jsonToEson (json, path = []) {
|
||||
const id = createId()
|
||||
for (let i = 0; i < json.length; i++) {
|
||||
const esonI = eson ? eson[i] : undefined
|
||||
|
||||
if (isObject(json)) {
|
||||
let eson = {}
|
||||
const props = Object.keys(json)
|
||||
props.forEach((prop) => eson[prop] = jsonToEson(json[prop], path.concat(prop)))
|
||||
eson[META] = { id, path, type: 'Object', props }
|
||||
return eson
|
||||
}
|
||||
else if (Array.isArray(json)) {
|
||||
let eson = json.map((value, index) => jsonToEson(value, path.concat(String(index))))
|
||||
eson[META] = { id, path, type: 'Array' }
|
||||
return eson
|
||||
}
|
||||
else { // json is a number, string, boolean, or null
|
||||
let eson = {}
|
||||
eson[META] = { id, path, type: 'value', value: json }
|
||||
return eson
|
||||
}
|
||||
}
|
||||
updatedEson[i] = syncEson(json[i], esonI)
|
||||
|
||||
/**
|
||||
* Convert an ESON object to a JSON object
|
||||
* @param {ESON} eson
|
||||
* @return {Object | Array | string | number | boolean | null} json
|
||||
*/
|
||||
export function esonToJson (eson) {
|
||||
switch (eson[META].type) {
|
||||
case 'Array':
|
||||
return eson.map(item => esonToJson(item))
|
||||
|
||||
case 'Object':
|
||||
const object = {}
|
||||
|
||||
eson[META].props.forEach(prop => {
|
||||
object[prop] = esonToJson(eson[prop])
|
||||
})
|
||||
|
||||
return object
|
||||
|
||||
default: // type 'string' or 'value'
|
||||
return eson[META].value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an eson object, traverse over the whole object (excluding the _meta)
|
||||
* objects, and allow replacing Objects/Arrays/values
|
||||
* @param {ESON} eson
|
||||
* @param {function (ESON, Path) : ESON} callback
|
||||
* @param {Path} [path]
|
||||
* @return {ESON}
|
||||
*/
|
||||
export function transform (eson, callback, path = []) {
|
||||
const updated = callback(eson, path)
|
||||
|
||||
if (updated[META].type === 'Object') {
|
||||
let changed = false
|
||||
let updatedChilds = {}
|
||||
for (let key in updated) {
|
||||
if (updated.hasOwnProperty(key)) {
|
||||
updatedChilds[key] = transform(updated[key], callback, path.concat(key))
|
||||
changed = changed || (updatedChilds[key] !== updated[key])
|
||||
if (updatedEson[i] !== esonI) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
updatedChilds[META] = updated[META]
|
||||
return changed ? updatedChilds : updated
|
||||
}
|
||||
else if (updated[META].type === 'Array') {
|
||||
let changed = false
|
||||
let updatedChilds = []
|
||||
for (let i = 0; i < updated.length; i++) {
|
||||
updatedChilds[i] = transform(updated[i], callback, path.concat(String(i)))
|
||||
changed = changed || (updatedChilds[i] !== updated[i])
|
||||
}
|
||||
updatedChilds[META] = updated[META]
|
||||
return changed ? updatedChilds : updated
|
||||
}
|
||||
else { // eson[META].type === 'value'
|
||||
return updated
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively update all paths in an eson object, array or value
|
||||
* @param {ESON} eson
|
||||
* @param {Path} [path]
|
||||
* @return {ESON}
|
||||
*/
|
||||
export function updatePaths(eson, path = []) {
|
||||
return transform(eson, function (value, path) {
|
||||
if (!isEqual(value[META].path, path)) {
|
||||
return setIn(value, [META, 'path'], path)
|
||||
if (changed) {
|
||||
updatedEson[ID] = sameType ? eson[ID] : createId()
|
||||
updatedEson[TYPE] = jsonType
|
||||
updatedEson[VALUE] = json
|
||||
updatedEson[EXPANDED] = sameType ? eson[EXPANDED] : false
|
||||
|
||||
return updatedEson
|
||||
}
|
||||
else {
|
||||
return value
|
||||
return eson
|
||||
}
|
||||
}, path)
|
||||
}
|
||||
else if (jsonType === 'object') {
|
||||
const jsonKeys = Object.keys(json)
|
||||
const esonKeys = esonType === 'object' ? Object.keys(eson) : []
|
||||
|
||||
// TODO: instead of creating updatedEson beforehand, only created as soon as we have a changed item
|
||||
let changed = (esonType !== jsonType) || (jsonKeys.length !== esonKeys.length)
|
||||
let updatedEson = {}
|
||||
|
||||
for (let i = 0; i < jsonKeys.length; i++) {
|
||||
const key = jsonKeys[i]
|
||||
const esonValue = eson ? eson[key] : undefined
|
||||
|
||||
updatedEson[key] = syncEson(json[key], esonValue)
|
||||
|
||||
if (updatedEson[key] !== esonValue) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
updatedEson[ID] = sameType ? eson[ID] : createId()
|
||||
updatedEson[TYPE] = jsonType
|
||||
updatedEson[VALUE] = json
|
||||
updatedEson[EXPANDED] = sameType ? eson[EXPANDED] : false
|
||||
|
||||
return updatedEson
|
||||
}
|
||||
else {
|
||||
return eson
|
||||
}
|
||||
}
|
||||
else if (jsonType === 'value') { // json is a value
|
||||
if (sameType) {
|
||||
return eson
|
||||
}
|
||||
else {
|
||||
const updatedEson = {}
|
||||
|
||||
updatedEson[ID] = sameType ? eson[ID] : createId()
|
||||
updatedEson[TYPE] = jsonType
|
||||
updatedEson[VALUE] = json
|
||||
updatedEson[EXPANDED] = false
|
||||
|
||||
return updatedEson
|
||||
}
|
||||
}
|
||||
else { // jsonType === 'undefined'
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -151,7 +122,7 @@ export function updatePaths(eson, path = []) {
|
|||
*/
|
||||
export function expand (eson, callback) {
|
||||
return transform(eson, function (value, path) {
|
||||
if (value[META].type === 'Array' || value[META].type === 'Object') {
|
||||
if (value[TYPE] === 'array' || value[TYPE] === 'object') {
|
||||
const expanded = callback(path)
|
||||
return (typeof expanded === 'boolean')
|
||||
? expandOne(value, [], expanded) // adjust expanded state
|
||||
|
@ -171,7 +142,7 @@ export function expand (eson, callback) {
|
|||
* @return {ESON}
|
||||
*/
|
||||
export function expandOne (eson, path, expanded = true) {
|
||||
return setIn(eson, path.concat([META, 'expanded']), expanded)
|
||||
return setIn(eson, path.concat(EXPANDED), expanded)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -206,7 +177,7 @@ export function applyErrors (eson, errors = []) {
|
|||
const esonWithErrors = errors.reduce((eson, error) => {
|
||||
const path = parseJSONPointer(error.dataPath)
|
||||
// TODO: do we want to be able to store multiple errors per item?
|
||||
return setIn(eson, path.concat([META, 'error']), error)
|
||||
return setIn(eson, path.concat(ERROR), error)
|
||||
}, eson)
|
||||
|
||||
// cleanup any old error messages
|
||||
|
@ -216,11 +187,12 @@ export function applyErrors (eson, errors = []) {
|
|||
/**
|
||||
* Cleanup meta data from an eson object
|
||||
* @param {ESON} eson Object to be cleaned up
|
||||
* @param {String} field Field name, for example 'error' or 'selected'
|
||||
* @param {Path[]} [ignorePaths=[]] An optional array with paths to be ignored
|
||||
* @param {String | Symbol} symbol A meta data field name, for example ERROR or SELECTED
|
||||
* @param {Array.<string | Path>} [ignorePaths=[]] An optional array with paths to be ignored
|
||||
* @return {ESON}
|
||||
*/
|
||||
export function cleanupMetaData(eson, field, ignorePaths = []) {
|
||||
export function cleanupMetaData(eson, symbol, ignorePaths = []) {
|
||||
// TODO: change ignorePaths to an object with path as key and true as value
|
||||
const pathsMap = {}
|
||||
ignorePaths.forEach(path => {
|
||||
const pathString = (typeof path === 'string') ? path : compileJSONPointer(path)
|
||||
|
@ -228,8 +200,8 @@ export function cleanupMetaData(eson, field, ignorePaths = []) {
|
|||
})
|
||||
|
||||
return transform(eson, function (value, path) {
|
||||
return (value[META][field] && !pathsMap[compileJSONPointer(path)])
|
||||
? deleteIn(value, [META, field])
|
||||
return (value[symbol] && !pathsMap[compileJSONPointer(path)])
|
||||
? deleteIn(value, [symbol])
|
||||
: value
|
||||
})
|
||||
}
|
||||
|
@ -253,23 +225,23 @@ export function search (eson, text) {
|
|||
// check property name
|
||||
const prop = last(path)
|
||||
if (text !== '' && containsCaseInsensitive(prop, text) &&
|
||||
getIn(eson, initial(path))[META].type === 'Object') { // parent must be an Object
|
||||
getIn(eson, initial(path))[TYPE] === 'object') { // parent must be an Object
|
||||
const searchState = isEmpty(matches) ? 'active' : 'normal'
|
||||
matches.push({path, area: 'property'})
|
||||
updatedValue = setIn(updatedValue, [META, 'searchProperty'], searchState)
|
||||
updatedValue = setIn(updatedValue, [SEARCH_PROPERTY], searchState)
|
||||
}
|
||||
else {
|
||||
updatedValue = deleteIn(updatedValue, [META, 'searchProperty'])
|
||||
updatedValue = deleteIn(updatedValue, [SEARCH_PROPERTY])
|
||||
}
|
||||
|
||||
// check value
|
||||
if (value[META].type === 'value' && text !== '' && containsCaseInsensitive(value[META].value, text)) {
|
||||
if (value[TYPE] === 'value' && text !== '' && containsCaseInsensitive(value[VALUE], text)) {
|
||||
const searchState = isEmpty(matches) ? 'active' : 'normal'
|
||||
matches.push({path, area: 'value'})
|
||||
updatedValue = setIn(updatedValue, [META, 'searchValue'], searchState)
|
||||
updatedValue = setIn(updatedValue, [SEARCH_VALUE], searchState)
|
||||
}
|
||||
else {
|
||||
updatedValue = deleteIn(updatedValue, [META, 'searchValue'])
|
||||
updatedValue = deleteIn(updatedValue, [SEARCH_VALUE])
|
||||
}
|
||||
|
||||
return updatedValue
|
||||
|
@ -346,9 +318,9 @@ export function nextSearchResult (eson, searchResult) {
|
|||
* @return {Object|Array}
|
||||
*/
|
||||
function setSearchStatus (eson, esonPointer, searchStatus) {
|
||||
const metaProp = esonPointer.area === 'property' ? 'searchProperty': 'searchValue'
|
||||
const searchSymbol = esonPointer.area === 'property' ? SEARCH_PROPERTY : SEARCH_VALUE
|
||||
|
||||
return setIn(eson, esonPointer.path.concat([META, metaProp]), searchStatus)
|
||||
return setIn(eson, esonPointer.path.concat([searchSymbol]), searchStatus)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -362,23 +334,19 @@ export function applySelection (eson, selection) {
|
|||
return cleanupMetaData(eson, 'selected')
|
||||
}
|
||||
else if (selection.inside) {
|
||||
const updatedEson = setIn(eson, selection.inside.concat([META, 'selected']),
|
||||
SELECTED_INSIDE)
|
||||
const updatedEson = setIn(eson, selection.inside.concat([SELECTION]), SELECTED_INSIDE)
|
||||
return cleanupMetaData(updatedEson, 'selected', [selection.inside])
|
||||
}
|
||||
else if (selection.after) {
|
||||
const updatedEson = setIn(eson, selection.after.concat([META, 'selected']),
|
||||
SELECTED_AFTER)
|
||||
const updatedEson = setIn(eson, selection.after.concat([SELECTION]), SELECTED_AFTER)
|
||||
return cleanupMetaData(updatedEson, 'selected', [selection.after])
|
||||
}
|
||||
else if (selection.empty) {
|
||||
const updatedEson = setIn(eson, selection.empty.concat([META, 'selected']),
|
||||
SELECTED_EMPTY)
|
||||
const updatedEson = setIn(eson, selection.empty.concat([SELECTION]), SELECTED_EMPTY)
|
||||
return cleanupMetaData(updatedEson, 'selected', [selection.empty])
|
||||
}
|
||||
else if (selection.emptyBefore) {
|
||||
const updatedEson = setIn(eson, selection.emptyBefore.concat([META, 'selected']),
|
||||
SELECTED_EMPTY_BEFORE)
|
||||
const updatedEson = setIn(eson, selection.emptyBefore.concat([SELECTION]), SELECTED_EMPTY_BEFORE)
|
||||
return cleanupMetaData(updatedEson, 'selected', [selection.emptyBefore])
|
||||
}
|
||||
else { // selection.start and selection.end
|
||||
|
@ -392,30 +360,31 @@ export function applySelection (eson, selection) {
|
|||
|
||||
// TODO: simplify the update function. Use pathsFromSelection ?
|
||||
|
||||
if (root[META].type === 'Object') {
|
||||
const startIndex = root[META].props.indexOf(start)
|
||||
const endIndex = root[META].props.indexOf(end)
|
||||
if (root[TYPE] === 'object') {
|
||||
const props = Object.keys(root).sort(naturalSort) // TODO: create a util function getSortedProps
|
||||
const startIndex = props.indexOf(start)
|
||||
const endIndex = props.indexOf(end)
|
||||
|
||||
const firstIndex = Math.min(startIndex, endIndex)
|
||||
const lastIndex = Math.max(startIndex, endIndex)
|
||||
const firstProp = root[META].props[firstIndex]
|
||||
const lastProp = root[META].props[lastIndex]
|
||||
const firstProp = props[firstIndex]
|
||||
const lastProp = props[lastIndex]
|
||||
|
||||
const selectedProps = root[META].props.slice(firstIndex, lastIndex + 1)// include max index itself
|
||||
const selectedProps = props.slice(firstIndex, lastIndex + 1)// include max index itself
|
||||
selectedPaths = selectedProps.map(prop => rootPath.concat(prop))
|
||||
let updatedObj = cloneWithSymbols(root)
|
||||
let updatedObj = shallowCloneWithSymbols(root)
|
||||
selectedProps.forEach(prop => {
|
||||
const selected = SELECTED +
|
||||
(prop === start ? SELECTED_START : 0) +
|
||||
(prop === end ? SELECTED_END : 0) +
|
||||
(prop === firstProp ? SELECTED_FIRST : 0) +
|
||||
(prop === lastProp ? SELECTED_LAST : 0)
|
||||
updatedObj[prop] = setIn(updatedObj[prop], [META, 'selected'], selected)
|
||||
updatedObj[prop] = setIn(updatedObj[prop], [SELECTION], selected)
|
||||
})
|
||||
|
||||
return updatedObj
|
||||
}
|
||||
else { // root[META].type === 'Array'
|
||||
else { // root[TYPE] === 'array'
|
||||
const startIndex = parseInt(start, 10)
|
||||
const endIndex = parseInt(end, 10)
|
||||
|
||||
|
@ -426,14 +395,14 @@ export function applySelection (eson, selection) {
|
|||
selectedPaths = selectedIndices.map(index => rootPath.concat(String(index)))
|
||||
|
||||
let updatedArr = root.slice()
|
||||
updatedArr = cloneWithSymbols(root)
|
||||
updatedArr = shallowCloneWithSymbols(root)
|
||||
selectedIndices.forEach(index => {
|
||||
const selected = SELECTED +
|
||||
(index === startIndex ? SELECTED_START : 0) +
|
||||
(index === endIndex ? SELECTED_END : 0) +
|
||||
(index === firstIndex ? SELECTED_FIRST : 0) +
|
||||
(index === lastIndex ? SELECTED_LAST : 0)
|
||||
updatedArr[index] = setIn(updatedArr[index], [META, 'selected'], selected)
|
||||
updatedArr[index] = setIn(updatedArr[index], [SELECTION], selected)
|
||||
})
|
||||
|
||||
return updatedArr
|
||||
|
@ -459,8 +428,9 @@ export function findSelectionIndices (root, rootPath, selection) {
|
|||
const end = (selection.after || selection.inside || selection.end)[rootPath.length]
|
||||
|
||||
// if no object we assume it's an Array
|
||||
const startIndex = root[META].type === 'Object' ? root[META].props.indexOf(start) : parseInt(start, 10)
|
||||
const endIndex = root[META].type === 'Object' ? root[META].props.indexOf(end) : parseInt(end, 10)
|
||||
const props = Object.keys(root).sort(naturalSort) // TODO: create a util function getSortedProps
|
||||
const startIndex = root[TYPE] === 'object' ? props.indexOf(start) : parseInt(start, 10)
|
||||
const endIndex = root[TYPE] === 'object' ? props.indexOf(end) : parseInt(end, 10)
|
||||
|
||||
const minIndex = Math.min(startIndex, endIndex)
|
||||
const maxIndex = Math.max(startIndex, endIndex) +
|
||||
|
@ -469,92 +439,23 @@ export function findSelectionIndices (root, rootPath, selection) {
|
|||
return { minIndex, maxIndex }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON paths from a selection, sorted from first to last
|
||||
* @param {ESON} eson
|
||||
* @param {Selection} selection
|
||||
* @return {Path[]}
|
||||
*/
|
||||
export function pathsFromSelection (eson, selection) {
|
||||
// find the parent node shared by both start and end of the selection
|
||||
const rootPath = findRootPath(selection)
|
||||
const root = getIn(eson, rootPath)
|
||||
|
||||
const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
|
||||
|
||||
if (root[META].type === 'Object') {
|
||||
return times(maxIndex - minIndex, i => rootPath.concat(root[META].props[i + minIndex]))
|
||||
}
|
||||
else { // root[META].type === 'Array'
|
||||
return times(maxIndex - minIndex, i => rootPath.concat(String(i + minIndex)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents of a list with paths
|
||||
* @param {ESON} data
|
||||
* @param {ESON} eson
|
||||
* @param {Path[]} paths
|
||||
* @return {Array.<{name: string, value: JSON, state: Object}>}
|
||||
*/
|
||||
export function contentsFromPaths (data, paths) {
|
||||
export function contentsFromPaths (eson, paths) {
|
||||
return paths.map(path => {
|
||||
const esonValue = getIn(data, path)
|
||||
const value = getIn(eson, path.concat(VALUE))
|
||||
return {
|
||||
name: last(path),
|
||||
value: esonToJson(esonValue),
|
||||
state: getEsonState(esonValue)
|
||||
value,
|
||||
state: {} // FIXME: state
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an object with paths and state (expanded, type) of an eson object
|
||||
* @param {ESON} eson
|
||||
* @return {Object.<key, Object>} An object with compiled JSON paths as key,
|
||||
* And a META object as state
|
||||
*/
|
||||
export function getEsonState (eson) {
|
||||
let state = {}
|
||||
|
||||
transform(eson, function (eson, path) {
|
||||
let meta = {}
|
||||
if (eson[META].expanded === true) {
|
||||
meta.expanded = true
|
||||
}
|
||||
if (eson[META].type === 'string') {
|
||||
meta.type = 'string'
|
||||
}
|
||||
if (!isEmpty(meta)) {
|
||||
state[compileJSONPointer(path)] = meta
|
||||
}
|
||||
|
||||
return eson
|
||||
})
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge ESON meta data to an ESON object: expanded state, type
|
||||
* @param {ESON} data
|
||||
* @param {Object.<String, Object>} state
|
||||
* @return {ESON}
|
||||
*/
|
||||
export function applyEsonState(data, state) {
|
||||
let updatedData = data
|
||||
|
||||
for (let path in state) {
|
||||
if (state.hasOwnProperty(path)) {
|
||||
const metaPath = parseJSONPointer(path).concat(META)
|
||||
updatedData = updateIn(updatedData, metaPath, function (meta) {
|
||||
return Object.assign({}, meta, state[path])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return updatedData
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the root path of a selection: the parent node shared by both start
|
||||
* and end of the selection
|
||||
|
@ -605,93 +506,56 @@ function findSharedPath (path1, path2) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Test whether a path exists in the eson object
|
||||
* Get the JSON paths from a selection, sorted from first to last
|
||||
* @param {ESON} eson
|
||||
* @param {Path} path
|
||||
* @return {boolean} Returns true if the path exists, else returns false
|
||||
* @private
|
||||
* @param {Selection} selection
|
||||
* @return {Path[]}
|
||||
*/
|
||||
export function pathExists (eson, path) {
|
||||
if (eson === undefined) {
|
||||
return false
|
||||
}
|
||||
// TODO: move pathsFromSelection to a separate file clipboard.js or selection.js?
|
||||
export function pathsFromSelection (eson, selection) {
|
||||
// find the parent node shared by both start and end of the selection
|
||||
const rootPath = findRootPath(selection)
|
||||
const root = getIn(eson, rootPath)
|
||||
|
||||
if (path.length === 0) {
|
||||
return true
|
||||
}
|
||||
const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
|
||||
|
||||
if (Array.isArray(eson)) {
|
||||
// index of an array
|
||||
return pathExists(eson[parseInt(path[0], 10)], path.slice(1))
|
||||
if (root[TYPE] === 'object') {
|
||||
const props = Object.keys(root).sort(naturalSort) // TODO: create a util function getSortedProps
|
||||
return times(maxIndex - minIndex, i => rootPath.concat(props[i + minIndex]))
|
||||
}
|
||||
else { // Object
|
||||
// object property. find the index of this property
|
||||
return pathExists(eson[path[0]], path.slice(1))
|
||||
else { // root[TYPE] === 'array'
|
||||
return times(maxIndex - minIndex, i => rootPath.concat(String(i + minIndex)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the index for `arr/-`, replace it with an index value equal to the
|
||||
* length of the array
|
||||
* @param {ESON} eson
|
||||
* @param {Path} path
|
||||
* @return {Path}
|
||||
* Convert the value of a JSON Patch action into a ESON object
|
||||
* @param {JSONPatchAction} action
|
||||
* @returns {ESONPatchAction}
|
||||
*/
|
||||
export function resolvePathIndex (eson, path) {
|
||||
if (path[path.length - 1] === '-') {
|
||||
const parentPath = initial(path)
|
||||
const parent = getIn(eson, parentPath)
|
||||
export function toEsonPatchAction (action) {
|
||||
return ('value' in action)
|
||||
? setIn(action, ['value'], syncEson(action.value))
|
||||
: action
|
||||
}
|
||||
|
||||
if (Array.isArray(parent)) {
|
||||
const index = parent.length
|
||||
return parentPath.concat(String(index))
|
||||
}
|
||||
// TODO: comment
|
||||
export function getType (any) {
|
||||
if (any === undefined) {
|
||||
return 'undefined'
|
||||
}
|
||||
|
||||
return path
|
||||
if (Array.isArray(any)) {
|
||||
return 'array'
|
||||
}
|
||||
|
||||
if (any && typeof any === 'object') {
|
||||
return 'object'
|
||||
}
|
||||
|
||||
return 'value'
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the property after provided property
|
||||
* @param {ESON} parent
|
||||
* @param {string} prop
|
||||
* @return {string | null} Returns the name of the next property,
|
||||
* or null if there is none
|
||||
*/
|
||||
export function findNextProp (parent, prop) {
|
||||
const index = parent[META].props.indexOf(prop)
|
||||
return parent[META].props[index + 1] || null
|
||||
}
|
||||
|
||||
// TODO: move parseJSONPointer and compileJSONPointer to a separate file
|
||||
|
||||
/**
|
||||
* Parse a JSON Pointer
|
||||
* WARNING: this is not a complete implementation
|
||||
* @param {string} pointer
|
||||
* @return {Path}
|
||||
*/
|
||||
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 implementation
|
||||
* @param {Path} path
|
||||
* @return {string}
|
||||
*/
|
||||
export function compileJSONPointer (path) {
|
||||
return path
|
||||
.map(p => '/' + String(p).replace(/~/g, '~0').replace(/\//g, '~1'))
|
||||
.join('')
|
||||
}
|
||||
|
||||
// TODO: move createId to a separate file
|
||||
|
||||
/**
|
||||
* Do a case insensitive search for a search text in a text
|
||||
* @param {String} text
|
||||
|
@ -706,8 +570,8 @@ export function containsCaseInsensitive (text, search) {
|
|||
* Get a new "unique" id. Id's are created from an incremental counter.
|
||||
* @return {number}
|
||||
*/
|
||||
export function createId () {
|
||||
function createId () {
|
||||
_id++
|
||||
return _id
|
||||
return 'node-' + _id
|
||||
}
|
||||
let _id = 0
|
||||
|
|
|
@ -1,57 +1,67 @@
|
|||
'use strict'
|
||||
|
||||
import { readFileSync } from 'fs'
|
||||
import { setIn, getIn, deleteIn } from './utils/immutabilityHelpers'
|
||||
import {
|
||||
META,
|
||||
esonToJson, pathExists, transform,
|
||||
parseJSONPointer, compileJSONPointer,
|
||||
jsonToEson,
|
||||
expand, expandOne, expandPath, applyErrors, search, nextSearchResult,
|
||||
applyErrors,
|
||||
applySelection, ERROR,
|
||||
expand,
|
||||
EXPANDED,
|
||||
expandOne,
|
||||
expandPath,
|
||||
ID,
|
||||
nextSearchResult,
|
||||
pathsFromSelection,
|
||||
previousSearchResult,
|
||||
applySelection, pathsFromSelection,
|
||||
getEsonState,
|
||||
SELECTED, SELECTED_START, SELECTED_END, SELECTED_FIRST, SELECTED_LAST
|
||||
search, SEARCH_PROPERTY, SEARCH_VALUE,
|
||||
SELECTED,
|
||||
SELECTED_END,
|
||||
SELECTED_FIRST,
|
||||
SELECTED_LAST,
|
||||
SELECTED_START, SELECTION,
|
||||
syncEson,
|
||||
TYPE
|
||||
} from './eson'
|
||||
import 'console.table'
|
||||
import repeat from 'lodash/repeat'
|
||||
import { assertDeepEqualEson } from './utils/assertDeepEqualEson'
|
||||
import { getIn, setIn } from './utils/immutabilityHelpers'
|
||||
import { createAssertEqualEson } from './utils/assertEqualEson'
|
||||
|
||||
test('jsonToEson', () => {
|
||||
assertDeepEqualEson(jsonToEson(1), {[META]: {id: '[ID]', path: [], type: 'value', value: 1}})
|
||||
assertDeepEqualEson(jsonToEson("foo"), {[META]: {id: '[ID]', path: [], type: 'value', value: "foo"}})
|
||||
assertDeepEqualEson(jsonToEson(null), {[META]: {id: '[ID]', path: [], type: 'value', value: null}})
|
||||
assertDeepEqualEson(jsonToEson(false), {[META]: {id: '[ID]', path: [], type: 'value', value: false}})
|
||||
assertDeepEqualEson(jsonToEson({a:1, b: 2}), {
|
||||
[META]: {id: '[ID]', path: [], type: 'Object', props: ['a', 'b']},
|
||||
a: {[META]: {id: '[ID]', path: ['a'], type: 'value', value: 1}},
|
||||
b: {[META]: {id: '[ID]', path: ['b'], type: 'value', value: 2}}
|
||||
const assertEqualEson = createAssertEqualEson(expect)
|
||||
|
||||
test('syncEson', () => {
|
||||
const json1 = {
|
||||
arr: [1,2,3],
|
||||
obj: {a : 2}
|
||||
}
|
||||
|
||||
const nodeState1 = syncEson(json1, undefined)
|
||||
|
||||
expect(nodeState1).toEqual({
|
||||
arr: [{}, {}, {}],
|
||||
obj: {a: {}}
|
||||
})
|
||||
expect(nodeState1.arr[0][ID]).toBeDefined()
|
||||
expect(nodeState1.arr[0][TYPE]).toEqual('value')
|
||||
expect(nodeState1.arr[TYPE]).toEqual('array')
|
||||
expect(nodeState1[TYPE]).toEqual('object')
|
||||
|
||||
const json2 = {
|
||||
arr: [1, 2],
|
||||
obj: {a : 2, b : 4}
|
||||
}
|
||||
const nodeState2 = syncEson(json2, nodeState1)
|
||||
|
||||
expect(nodeState2).toEqual({
|
||||
arr: [{}, {}],
|
||||
obj: {a: {}, b: {}}
|
||||
})
|
||||
|
||||
const actual = jsonToEson([1,2])
|
||||
const expected = [
|
||||
{[META]: {id: '[ID]', path: ['0'], type: 'value', value: 1}},
|
||||
{[META]: {id: '[ID]', path: ['1'], type: 'value', value: 2}}
|
||||
]
|
||||
expected[META] = {id: '[ID]', path: [], type: 'Array'}
|
||||
assertDeepEqualEson(actual, expected)
|
||||
})
|
||||
|
||||
test('esonToJson', () => {
|
||||
const json = {
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
"str": "hello world",
|
||||
"nill": null,
|
||||
"bool": false
|
||||
}
|
||||
const eson = jsonToEson(json)
|
||||
expect(esonToJson(eson)).toEqual(json)
|
||||
// ID's should be the same for unchanged contents
|
||||
expect(nodeState2[ID]).toEqual(nodeState1[ID])
|
||||
expect(nodeState2.arr[ID]).toEqual(nodeState1.arr[ID])
|
||||
expect(nodeState2.arr[0][ID]).toEqual(nodeState1.arr[0][ID])
|
||||
expect(nodeState2.arr[1][ID]).toEqual(nodeState1.arr[1][ID])
|
||||
expect(nodeState2.obj[ID]).toEqual(nodeState1.obj[ID])
|
||||
expect(nodeState2.obj.a[ID]).toEqual(nodeState1.obj.a[ID])
|
||||
})
|
||||
|
||||
test('expand a single path', () => {
|
||||
const eson = jsonToEson({
|
||||
const eson = syncEson({
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
|
@ -62,16 +72,14 @@ test('expand a single path', () => {
|
|||
|
||||
const path = ['obj', 'arr', 2]
|
||||
const collapsed = expandOne(eson, path, false)
|
||||
expect(collapsed.obj.arr[2][META].expanded).toEqual(false)
|
||||
assertDeepEqualEson(deleteIn(collapsed, path.concat([META, 'expanded'])), eson)
|
||||
expect(collapsed.obj.arr[2][EXPANDED]).toEqual(false)
|
||||
|
||||
const expanded = expandOne(eson, path, true)
|
||||
expect(expanded.obj.arr[2][META].expanded).toEqual(true)
|
||||
assertDeepEqualEson(deleteIn(expanded, path.concat([META, 'expanded'])), eson)
|
||||
expect(expanded.obj.arr[2][EXPANDED]).toEqual(true)
|
||||
})
|
||||
|
||||
test('expand all objects/arrays on a path', () => {
|
||||
const eson = jsonToEson({
|
||||
const eson = syncEson({
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
|
@ -83,28 +91,20 @@ test('expand all objects/arrays on a path', () => {
|
|||
const path = ['obj', 'arr', 2]
|
||||
|
||||
const collapsed = expandPath(eson, path, false)
|
||||
expect(collapsed[META].expanded).toEqual(false)
|
||||
expect(collapsed.obj[META].expanded).toEqual(false)
|
||||
expect(collapsed.obj.arr[META].expanded).toEqual(false)
|
||||
expect(collapsed.obj.arr[2][META].expanded).toEqual(false)
|
||||
expect(collapsed[EXPANDED]).toEqual(false)
|
||||
expect(collapsed.obj[EXPANDED]).toEqual(false)
|
||||
expect(collapsed.obj.arr[EXPANDED]).toEqual(false)
|
||||
expect(collapsed.obj.arr[2][EXPANDED]).toEqual(false)
|
||||
|
||||
const expanded = expandPath(eson, path, true)
|
||||
expect(expanded[META].expanded).toEqual(true)
|
||||
expect(expanded.obj[META].expanded).toEqual(true)
|
||||
expect(expanded.obj.arr[META].expanded).toEqual(true)
|
||||
expect(expanded.obj.arr[2][META].expanded).toEqual(true)
|
||||
|
||||
let orig = expanded
|
||||
orig = deleteIn(orig, [].concat([META, 'expanded']))
|
||||
orig = deleteIn(orig, ['obj'].concat([META, 'expanded']))
|
||||
orig = deleteIn(orig, ['obj', 'arr'].concat([META, 'expanded']))
|
||||
orig = deleteIn(orig, ['obj', 'arr', 2].concat([META, 'expanded']))
|
||||
|
||||
assertDeepEqualEson(orig, eson)
|
||||
expect(expanded[EXPANDED]).toEqual(true)
|
||||
expect(expanded.obj[EXPANDED]).toEqual(true)
|
||||
expect(expanded.obj.arr[EXPANDED]).toEqual(true)
|
||||
expect(expanded.obj.arr[2][EXPANDED]).toEqual(true)
|
||||
})
|
||||
|
||||
test('expand a callback', () => {
|
||||
const eson = jsonToEson({
|
||||
const eson = syncEson({
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
|
@ -114,25 +114,20 @@ test('expand a callback', () => {
|
|||
})
|
||||
|
||||
function callback (path) {
|
||||
console.log('callback')
|
||||
return (path.length >= 1)
|
||||
? false // collapse
|
||||
? true // expand
|
||||
: undefined // leave untouched
|
||||
}
|
||||
const collapsed = expand(eson, callback)
|
||||
expect(collapsed[META].expanded).toEqual(undefined)
|
||||
expect(collapsed.obj[META].expanded).toEqual(false)
|
||||
expect(collapsed.obj.arr[META].expanded).toEqual(false)
|
||||
expect(collapsed.obj.arr[2][META].expanded).toEqual(false)
|
||||
|
||||
let orig = collapsed
|
||||
orig = deleteIn(orig, ['obj'].concat([META, 'expanded']))
|
||||
orig = deleteIn(orig, ['obj', 'arr'].concat([META, 'expanded']))
|
||||
orig = deleteIn(orig, ['obj', 'arr', 2].concat([META, 'expanded']))
|
||||
assertDeepEqualEson(orig, eson)
|
||||
const expanded = expand(eson, callback)
|
||||
expect(expanded[EXPANDED]).toEqual(false)
|
||||
expect(expanded.obj[EXPANDED]).toEqual(true)
|
||||
expect(expanded.obj.arr[EXPANDED]).toEqual(true)
|
||||
expect(expanded.obj.arr[2][EXPANDED]).toEqual(true)
|
||||
})
|
||||
|
||||
test('expand a callback should not change the object when nothing happens', () => {
|
||||
const eson = jsonToEson({a: [1,2,3], b: {c: 4}})
|
||||
const eson = syncEson({a: [1,2,3], b: {c: 4}})
|
||||
function callback (path) {
|
||||
return undefined
|
||||
}
|
||||
|
@ -141,69 +136,8 @@ test('expand a callback should not change the object when nothing happens', () =
|
|||
expect(collapsed).toBe(eson)
|
||||
})
|
||||
|
||||
test('transform (no change)', () => {
|
||||
const eson = jsonToEson({a: [1,2,3], b: {c: 4}})
|
||||
const updated = transform(eson, (value, path) => value)
|
||||
assertDeepEqualEson(updated, eson)
|
||||
expect(updated).toBe(eson)
|
||||
})
|
||||
|
||||
test('transform (change based on value)', () => {
|
||||
const eson = jsonToEson({a: [1,2,3], b: {c: 4}})
|
||||
|
||||
const updated = transform(eson,
|
||||
(value, path) => value[META].value === 2 ? jsonToEson(20, path) : value)
|
||||
const expected = jsonToEson({a: [1,20,3], b: {c: 4}})
|
||||
|
||||
assertDeepEqualEson(updated, expected)
|
||||
expect(updated.b).toBe(eson.b) // should not have replaced b
|
||||
})
|
||||
|
||||
test('transform (change based on path)', () => {
|
||||
const eson = jsonToEson({a: [1,2,3], b: {c: 4}})
|
||||
|
||||
const updated = transform(eson,
|
||||
(value, path) => path.join('.') === 'a.1' ? jsonToEson(20, path) : value)
|
||||
const expected = jsonToEson({a: [1,20,3], b: {c: 4}})
|
||||
|
||||
assertDeepEqualEson(updated, expected)
|
||||
expect(updated.b).toBe(eson.b) // should not have replaced b
|
||||
})
|
||||
|
||||
test('pathExists', () => {
|
||||
const eson = jsonToEson({
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
"str": "hello world",
|
||||
"nill": null,
|
||||
"bool": false
|
||||
})
|
||||
|
||||
expect(pathExists(eson, ['obj', 'arr', 2, 'first'])).toEqual(true)
|
||||
expect(pathExists(eson, ['obj', 'foo'])).toEqual(false)
|
||||
expect(pathExists(eson, ['obj', 'foo', 'bar'])).toEqual(false)
|
||||
expect(pathExists(eson, [])).toEqual(true)
|
||||
})
|
||||
|
||||
test('parseJSONPointer', () => {
|
||||
expect(parseJSONPointer('/obj/a')).toEqual(['obj', 'a'])
|
||||
expect(parseJSONPointer('/arr/-')).toEqual(['arr', '-'])
|
||||
expect(parseJSONPointer('/foo/~1~0 ~0~1')).toEqual(['foo', '/~ ~/'])
|
||||
expect(parseJSONPointer('/obj')).toEqual(['obj'])
|
||||
expect(parseJSONPointer('/')).toEqual([''])
|
||||
expect(parseJSONPointer('')).toEqual([])
|
||||
})
|
||||
|
||||
test('compileJSONPointer', () => {
|
||||
expect(compileJSONPointer(['foo', 'bar'])).toEqual('/foo/bar')
|
||||
expect(compileJSONPointer(['foo', '/~ ~/'])).toEqual('/foo/~1~0 ~0~1')
|
||||
expect(compileJSONPointer([''])).toEqual('/')
|
||||
expect(compileJSONPointer([])).toEqual('')
|
||||
})
|
||||
|
||||
test('add and remove errors', () => {
|
||||
const eson = jsonToEson({
|
||||
const eson = syncEson({
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
|
@ -220,9 +154,13 @@ test('add and remove errors', () => {
|
|||
const actual1 = applyErrors(eson, jsonSchemaErrors)
|
||||
|
||||
let expected = eson
|
||||
expected = setIn(expected, ['obj', 'arr', '2', 'last', META, 'error'], jsonSchemaErrors[0])
|
||||
expected = setIn(expected, ['nill', META, 'error'], jsonSchemaErrors[1])
|
||||
assertDeepEqualEson(actual1, expected)
|
||||
expected = setIn(expected, ['obj', 'arr', '2', 'last', ERROR], jsonSchemaErrors[0])
|
||||
expected = setIn(expected, ['nill', ERROR], jsonSchemaErrors[1])
|
||||
|
||||
console.log(actual1)
|
||||
console.log(expected)
|
||||
|
||||
assertEqualEson(actual1, expected)
|
||||
|
||||
// re-applying the same errors should not change eson
|
||||
const actual2 = applyErrors(actual1, jsonSchemaErrors)
|
||||
|
@ -230,12 +168,12 @@ test('add and remove errors', () => {
|
|||
|
||||
// clear errors
|
||||
const actual3 = applyErrors(actual2, [])
|
||||
assertDeepEqualEson(actual3, eson)
|
||||
assertEqualEson(actual3, eson)
|
||||
expect(actual3.str).toEqual(eson.str) // shouldn't have touched values not affected by the errors
|
||||
})
|
||||
|
||||
test('search', () => {
|
||||
const eson = jsonToEson({
|
||||
const eson = syncEson({
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
|
@ -259,18 +197,18 @@ test('search', () => {
|
|||
expect(active).toEqual({path: ['obj', 'arr', '2', 'last'], area: 'property'})
|
||||
|
||||
let expected = esonWithSearch
|
||||
expected = setIn(expected, ['obj', 'arr', '2', 'last', META, 'searchProperty'], 'active')
|
||||
expected = setIn(expected, ['str', META, 'searchValue'], 'normal')
|
||||
expected = setIn(expected, ['nill', META, 'searchProperty'], 'normal')
|
||||
expected = setIn(expected, ['nill', META, 'searchValue'], 'normal')
|
||||
expected = setIn(expected, ['bool', META, 'searchProperty'], 'normal')
|
||||
expected = setIn(expected, ['bool', META, 'searchValue'], 'normal')
|
||||
expected = setIn(expected, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY], 'active')
|
||||
expected = setIn(expected, ['str', SEARCH_VALUE], 'normal')
|
||||
expected = setIn(expected, ['nill', SEARCH_PROPERTY], 'normal')
|
||||
expected = setIn(expected, ['nill', SEARCH_VALUE], 'normal')
|
||||
expected = setIn(expected, ['bool', SEARCH_PROPERTY], 'normal')
|
||||
expected = setIn(expected, ['bool', SEARCH_VALUE], 'normal')
|
||||
|
||||
assertDeepEqualEson(esonWithSearch, expected)
|
||||
assertEqualEson(esonWithSearch, expected)
|
||||
})
|
||||
|
||||
test('search number', () => {
|
||||
const eson = jsonToEson({
|
||||
const eson = syncEson({
|
||||
"2": "two",
|
||||
"arr": ["a", "b", "c", "2"]
|
||||
})
|
||||
|
@ -285,7 +223,7 @@ test('search number', () => {
|
|||
})
|
||||
|
||||
test('nextSearchResult', () => {
|
||||
const eson = jsonToEson({
|
||||
const eson = syncEson({
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
|
@ -302,31 +240,31 @@ test('nextSearchResult', () => {
|
|||
])
|
||||
|
||||
expect(first.searchResult.active).toEqual({path: ['obj', 'arr'], area: 'property'})
|
||||
expect(getIn(first.eson, ['obj', 'arr', META, 'searchProperty'])).toEqual('active')
|
||||
expect(getIn(first.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty'])).toEqual('normal')
|
||||
expect(getIn(first.eson, ['bool', META, 'searchValue'])).toEqual('normal')
|
||||
expect(getIn(first.eson, ['obj', 'arr', SEARCH_PROPERTY])).toEqual('active')
|
||||
expect(getIn(first.eson, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY])).toEqual('normal')
|
||||
expect(getIn(first.eson, ['bool', SEARCH_VALUE])).toEqual('normal')
|
||||
|
||||
const second = nextSearchResult(first.eson, first.searchResult)
|
||||
expect(second.searchResult.active).toEqual({path: ['obj', 'arr', '2', 'last'], area: 'property'})
|
||||
expect(getIn(second.eson, ['obj', 'arr', META, 'searchProperty'])).toEqual('normal')
|
||||
expect(getIn(second.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty'])).toEqual('active')
|
||||
expect(getIn(second.eson, ['bool', META, 'searchValue'])).toEqual('normal')
|
||||
expect(getIn(second.eson, ['obj', 'arr', SEARCH_PROPERTY])).toEqual('normal')
|
||||
expect(getIn(second.eson, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY])).toEqual('active')
|
||||
expect(getIn(second.eson, ['bool', SEARCH_VALUE])).toEqual('normal')
|
||||
|
||||
const third = nextSearchResult(second.eson, second.searchResult)
|
||||
expect(third.searchResult.active).toEqual({path: ['bool'], area: 'value'})
|
||||
expect(getIn(third.eson, ['obj', 'arr', META, 'searchProperty'])).toEqual('normal')
|
||||
expect(getIn(third.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty'])).toEqual('normal')
|
||||
expect(getIn(third.eson, ['bool', META, 'searchValue'])).toEqual('active')
|
||||
expect(getIn(third.eson, ['obj', 'arr', SEARCH_PROPERTY])).toEqual('normal')
|
||||
expect(getIn(third.eson, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY])).toEqual('normal')
|
||||
expect(getIn(third.eson, ['bool', SEARCH_VALUE])).toEqual('active')
|
||||
|
||||
const wrappedAround = nextSearchResult(third.eson, third.searchResult)
|
||||
expect(wrappedAround.searchResult.active).toEqual({path: ['obj', 'arr'], area: 'property'})
|
||||
expect(getIn(wrappedAround.eson, ['obj', 'arr', META, 'searchProperty'])).toEqual('active')
|
||||
expect(getIn(wrappedAround.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty'])).toEqual('normal')
|
||||
expect(getIn(wrappedAround.eson, ['bool', META, 'searchValue'])).toEqual('normal')
|
||||
expect(getIn(wrappedAround.eson, ['obj', 'arr', SEARCH_PROPERTY])).toEqual('active')
|
||||
expect(getIn(wrappedAround.eson, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY])).toEqual('normal')
|
||||
expect(getIn(wrappedAround.eson, ['bool', SEARCH_VALUE])).toEqual('normal')
|
||||
})
|
||||
|
||||
test('previousSearchResult', () => {
|
||||
const eson = jsonToEson({
|
||||
const eson = syncEson({
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
|
@ -343,31 +281,31 @@ test('previousSearchResult', () => {
|
|||
])
|
||||
|
||||
expect(init.searchResult.active).toEqual({path: ['obj', 'arr'], area: 'property'})
|
||||
expect(getIn(init.eson, ['obj', 'arr', META, 'searchProperty'])).toEqual('active')
|
||||
expect(getIn(init.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty'])).toEqual('normal')
|
||||
expect(getIn(init.eson, ['bool', META, 'searchValue'])).toEqual('normal')
|
||||
expect(getIn(init.eson, ['obj', 'arr', SEARCH_PROPERTY])).toEqual('active')
|
||||
expect(getIn(init.eson, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY])).toEqual('normal')
|
||||
expect(getIn(init.eson, ['bool', SEARCH_VALUE])).toEqual('normal')
|
||||
|
||||
const third = previousSearchResult(init.eson, init.searchResult)
|
||||
expect(third.searchResult.active).toEqual({path: ['bool'], area: 'value'})
|
||||
expect(getIn(third.eson, ['obj', 'arr', META, 'searchProperty'])).toEqual('normal')
|
||||
expect(getIn(third.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty'])).toEqual('normal')
|
||||
expect(getIn(third.eson, ['bool', META, 'searchValue'])).toEqual('active')
|
||||
expect(getIn(third.eson, ['obj', 'arr', SEARCH_PROPERTY])).toEqual('normal')
|
||||
expect(getIn(third.eson, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY])).toEqual('normal')
|
||||
expect(getIn(third.eson, ['bool', SEARCH_VALUE])).toEqual('active')
|
||||
|
||||
const second = previousSearchResult(third.eson, third.searchResult)
|
||||
expect(second.searchResult.active).toEqual({path: ['obj', 'arr', '2', 'last'], area: 'property'})
|
||||
expect(getIn(second.eson, ['obj', 'arr', META, 'searchProperty'])).toEqual('normal')
|
||||
expect(getIn(second.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty'])).toEqual('active')
|
||||
expect(getIn(second.eson, ['bool', META, 'searchValue'])).toEqual('normal')
|
||||
expect(getIn(second.eson, ['obj', 'arr', SEARCH_PROPERTY])).toEqual('normal')
|
||||
expect(getIn(second.eson, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY])).toEqual('active')
|
||||
expect(getIn(second.eson, ['bool', SEARCH_VALUE])).toEqual('normal')
|
||||
|
||||
const first = previousSearchResult(second.eson, second.searchResult)
|
||||
expect(first.searchResult.active).toEqual({path: ['obj', 'arr'], area: 'property'})
|
||||
expect(getIn(first.eson, ['obj', 'arr', META, 'searchProperty'])).toEqual('active')
|
||||
expect(getIn(first.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty'])).toEqual('normal')
|
||||
expect(getIn(first.eson, ['bool', META, 'searchValue'])).toEqual('normal')
|
||||
expect(getIn(first.eson, ['obj', 'arr', SEARCH_PROPERTY])).toEqual('active')
|
||||
expect(getIn(first.eson, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY])).toEqual('normal')
|
||||
expect(getIn(first.eson, ['bool', SEARCH_VALUE])).toEqual('normal')
|
||||
})
|
||||
|
||||
test('selection (object)', () => {
|
||||
const eson = jsonToEson({
|
||||
const eson = syncEson({
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
|
@ -383,10 +321,10 @@ test('selection (object)', () => {
|
|||
const actual = applySelection(eson, selection)
|
||||
|
||||
let expected = eson
|
||||
expected = setIn(expected, ['obj', META, 'selected'], SELECTED + SELECTED_START + SELECTED_FIRST)
|
||||
expected = setIn(expected, ['str', META, 'selected'], SELECTED)
|
||||
expected = setIn(expected, ['nill', META, 'selected'], SELECTED + SELECTED_END + SELECTED_LAST)
|
||||
assertDeepEqualEson(actual, expected)
|
||||
expected = setIn(expected, ['obj', SELECTION], SELECTED + SELECTED_START + SELECTED_FIRST)
|
||||
expected = setIn(expected, ['str', SELECTION], SELECTED)
|
||||
expected = setIn(expected, ['nill', SELECTION], SELECTED + SELECTED_END + SELECTED_LAST)
|
||||
assertEqualEson(actual, expected)
|
||||
|
||||
// test whether old selection results are cleaned up
|
||||
const selection2 = {
|
||||
|
@ -395,13 +333,13 @@ test('selection (object)', () => {
|
|||
}
|
||||
const actual2 = applySelection(actual, selection2)
|
||||
let expected2 = eson
|
||||
expected2 = setIn(expected2, ['nill', META, 'selected'], SELECTED + SELECTED_START + SELECTED_FIRST)
|
||||
expected2 = setIn(expected2, ['bool', META, 'selected'], SELECTED + SELECTED_END + SELECTED_LAST)
|
||||
assertDeepEqualEson(actual2, expected2)
|
||||
expected2 = setIn(expected2, ['nill', SELECTION], SELECTED + SELECTED_START + SELECTED_FIRST)
|
||||
expected2 = setIn(expected2, ['bool', SELECTION], SELECTED + SELECTED_END + SELECTED_LAST)
|
||||
assertEqualEson(actual2, expected2)
|
||||
})
|
||||
|
||||
test('selection (array)', () => {
|
||||
const eson = jsonToEson({
|
||||
const eson = syncEson({
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
|
@ -417,16 +355,16 @@ test('selection (array)', () => {
|
|||
const actual = applySelection(eson, selection)
|
||||
|
||||
let expected = eson
|
||||
expected = setIn(expected, ['obj', 'arr', '0', META, 'selected'],
|
||||
expected = setIn(expected, ['obj', 'arr', '0', SELECTION],
|
||||
SELECTED + SELECTED_END + SELECTED_FIRST)
|
||||
expected = setIn(expected, ['obj', 'arr', '1', META, 'selected'],
|
||||
expected = setIn(expected, ['obj', 'arr', '1', SELECTION],
|
||||
SELECTED + SELECTED_START + SELECTED_LAST)
|
||||
|
||||
assertDeepEqualEson(actual, expected)
|
||||
assertEqualEson(actual, expected)
|
||||
})
|
||||
|
||||
test('selection (value)', () => {
|
||||
const eson = jsonToEson({
|
||||
const eson = syncEson({
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
|
@ -440,13 +378,13 @@ test('selection (value)', () => {
|
|||
}
|
||||
|
||||
const actual = applySelection(eson, selection)
|
||||
const expected = setIn(eson, ['obj', 'arr', '2', 'first', META, 'selected'],
|
||||
const expected = setIn(eson, ['obj', 'arr', '2', 'first', SELECTION],
|
||||
SELECTED + SELECTED_START + SELECTED_END + SELECTED_FIRST + SELECTED_LAST)
|
||||
assertDeepEqualEson(actual, expected)
|
||||
assertEqualEson(actual, expected)
|
||||
})
|
||||
|
||||
test('selection (node)', () => {
|
||||
const eson = jsonToEson({
|
||||
const eson = syncEson({
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
|
@ -460,26 +398,26 @@ test('selection (node)', () => {
|
|||
}
|
||||
|
||||
const actual = applySelection(eson, selection)
|
||||
const expected = setIn(eson, ['obj', 'arr', META, 'selected'],
|
||||
const expected = setIn(eson, ['obj', 'arr', SELECTION],
|
||||
SELECTED + SELECTED_START + SELECTED_END + SELECTED_FIRST + SELECTED_LAST)
|
||||
assertDeepEqualEson(actual, expected)
|
||||
assertEqualEson(actual, expected)
|
||||
})
|
||||
|
||||
test('pathsFromSelection (object)', () => {
|
||||
const eson = jsonToEson({
|
||||
const json = {
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
"str": "hello world",
|
||||
"nill": null,
|
||||
"bool": false
|
||||
})
|
||||
}
|
||||
const selection = {
|
||||
start: ['obj', 'arr', '2', 'last'],
|
||||
end: ['nill']
|
||||
}
|
||||
|
||||
expect(pathsFromSelection(eson, selection)).toEqual([
|
||||
expect(pathsFromSelection(json, selection)).toEqual([
|
||||
['obj'],
|
||||
['str'],
|
||||
['nill']
|
||||
|
@ -487,138 +425,56 @@ test('pathsFromSelection (object)', () => {
|
|||
})
|
||||
|
||||
test('pathsFromSelection (array)', () => {
|
||||
const eson = jsonToEson({
|
||||
const json = {
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
"str": "hello world",
|
||||
"nill": null,
|
||||
"bool": false
|
||||
})
|
||||
}
|
||||
const selection = {
|
||||
start: ['obj', 'arr', '1'],
|
||||
end: ['obj', 'arr', '0'] // note the "wrong" order of start and end
|
||||
end: ['obj', 'arr', '0'] // note the "backward" order of start and end
|
||||
}
|
||||
|
||||
expect(pathsFromSelection(eson, selection)).toEqual([
|
||||
expect(pathsFromSelection(json, selection)).toEqual([
|
||||
['obj', 'arr', '0'],
|
||||
['obj', 'arr', '1']
|
||||
])
|
||||
})
|
||||
|
||||
test('pathsFromSelection (value)', () => {
|
||||
const eson = jsonToEson({
|
||||
const json = {
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
"str": "hello world",
|
||||
"nill": null,
|
||||
"bool": false
|
||||
})
|
||||
}
|
||||
const selection = {
|
||||
start: ['obj', 'arr', '2', 'first'],
|
||||
end: ['obj', 'arr', '2', 'first']
|
||||
}
|
||||
|
||||
expect(pathsFromSelection(eson, selection)).toEqual([
|
||||
expect(pathsFromSelection(json, selection)).toEqual([
|
||||
['obj', 'arr', '2', 'first'],
|
||||
])
|
||||
})
|
||||
|
||||
test('pathsFromSelection (before)', () => {
|
||||
const eson = jsonToEson({
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
"str": "hello world",
|
||||
"nill": null,
|
||||
"bool": false
|
||||
})
|
||||
const selection = {
|
||||
after: ['obj', 'arr', '2', 'first']
|
||||
}
|
||||
|
||||
expect(pathsFromSelection(eson, selection)).toEqual([])
|
||||
})
|
||||
|
||||
test('pathsFromSelection (after)', () => {
|
||||
const eson = jsonToEson({
|
||||
const json = {
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
"str": "hello world",
|
||||
"nill": null,
|
||||
"bool": false
|
||||
})
|
||||
}
|
||||
const selection = {
|
||||
after: ['obj', 'arr', '2', 'first']
|
||||
}
|
||||
|
||||
expect(pathsFromSelection(eson, selection)).toEqual([])
|
||||
expect(pathsFromSelection(json, selection)).toEqual([])
|
||||
})
|
||||
|
||||
test('getEsonState', () => {
|
||||
const eson = jsonToEson({
|
||||
"obj": {
|
||||
"arr": ["1",2, {"first":3,"last":4}]
|
||||
},
|
||||
"str": "hello world",
|
||||
"nill": null,
|
||||
"bool": false
|
||||
})
|
||||
|
||||
eson.obj[META].expanded = true
|
||||
eson.obj.arr[META].expanded = false
|
||||
eson.obj.arr[0][META].type = 'string'
|
||||
eson.obj.arr[2][META].expanded = true
|
||||
|
||||
const state = getEsonState(eson)
|
||||
|
||||
expect(state).toEqual({
|
||||
'/obj': { expanded: true },
|
||||
'/obj/arr/0': { type: 'string' },
|
||||
'/obj/arr/2': { expanded: true },
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: test applyEsonState
|
||||
|
||||
// helper function to print JSON in the console
|
||||
function printJSON (json, message = null) {
|
||||
if (message) {
|
||||
console.log(message)
|
||||
}
|
||||
console.log(JSON.stringify(json, null, 2))
|
||||
}
|
||||
|
||||
function printESON (eson, message = null) {
|
||||
if (message) {
|
||||
console.log(message)
|
||||
}
|
||||
|
||||
let data = []
|
||||
|
||||
transform(eson, function (value, path) {
|
||||
// const strPath = padEnd(, 20)
|
||||
// console.log(`${strPath} ${'value' in value[META] ? value[META].value : ''} ${JSON.stringify(value[META])}`)
|
||||
|
||||
data.push({
|
||||
path: '[' + path.join(', ') + ']',
|
||||
value: repeat(' ', path.length) + (value[META].type === 'Object'
|
||||
? '{...}'
|
||||
: value[META].type === 'Array'
|
||||
? '[...]'
|
||||
: JSON.stringify(value[META].value)),
|
||||
meta: JSON.stringify(value[META])
|
||||
})
|
||||
|
||||
return value
|
||||
})
|
||||
|
||||
console.table(data)
|
||||
}
|
||||
|
||||
// helper function to load a JSON file
|
||||
function loadJSON (filename) {
|
||||
return JSON.parse(readFileSync(__dirname + '/' + filename, 'utf-8'))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,287 @@
|
|||
import isEqual from 'lodash/isEqual'
|
||||
import initial from 'lodash/initial'
|
||||
|
||||
import { setIn, getIn, deleteIn, insertAt, existsIn } from './utils/immutabilityHelpers'
|
||||
import { parseJSONPointer, compileJSONPointer } from './jsonPointer'
|
||||
|
||||
/**
|
||||
* Apply a patch to a JSON object
|
||||
* The original JSON object will not be changed,
|
||||
* instead, the patch is applied in an immutable way
|
||||
* @param {JSON} json
|
||||
* @param {JSONPatch} patch Array with JSON patch actions
|
||||
* @return {{json: JSON, revert: JSONPatch, error: Error | null}}
|
||||
*/
|
||||
export function immutableJsonPatch (json, patch) {
|
||||
let updatedJson = json
|
||||
let revert = []
|
||||
|
||||
for (let i = 0; i < patch.length; i++) {
|
||||
const action = patch[i]
|
||||
const path = action.path ? parseJSONPointer(action.path) : null
|
||||
const from = action.from ? parseJSONPointer(action.from) : null
|
||||
|
||||
switch (action.op) {
|
||||
case 'add': {
|
||||
const result = add(updatedJson, path, action.value)
|
||||
updatedJson = result.json
|
||||
revert = result.revert.concat(revert)
|
||||
break
|
||||
}
|
||||
|
||||
case 'remove': {
|
||||
const result = remove(updatedJson, path)
|
||||
updatedJson = result.json
|
||||
revert = result.revert.concat(revert)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'replace': {
|
||||
const result = replace(updatedJson, path, action.value)
|
||||
updatedJson = result.json
|
||||
revert = result.revert.concat(revert)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'copy': {
|
||||
if (!action.from) {
|
||||
return {
|
||||
json: updatedJson,
|
||||
revert: [],
|
||||
error: new Error('Property "from" expected in copy action ' + JSON.stringify(action))
|
||||
}
|
||||
}
|
||||
|
||||
const result = copy(updatedJson, path, from)
|
||||
updatedJson = result.json
|
||||
revert = result.revert.concat(revert)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'move': {
|
||||
if (!action.from) {
|
||||
return {
|
||||
json: updatedJson,
|
||||
revert: [],
|
||||
error: new Error('Property "from" expected in move action ' + JSON.stringify(action))
|
||||
}
|
||||
}
|
||||
|
||||
const result = move(updatedJson, path, from)
|
||||
updatedJson = result.json
|
||||
revert = result.revert.concat(revert)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'test': {
|
||||
// when a test fails, cancel the whole patch and return the error
|
||||
const error = test(updatedJson, path, action.value)
|
||||
if (error) {
|
||||
return { json, revert: [], error}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
// unknown patch operation. Cancel the whole patch and return an error
|
||||
return {
|
||||
json,
|
||||
revert: [],
|
||||
error: new Error('Unknown JSONPatch op ' + JSON.stringify(action.op))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
json: updatedJson,
|
||||
revert,
|
||||
error: null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace an existing item
|
||||
* @param {JSON} json
|
||||
* @param {Path} path
|
||||
* @param {JSON} value
|
||||
* @return {{json: JSON, revert: JSONPatch}}
|
||||
*/
|
||||
export function replace (json, path, value) {
|
||||
const oldValue = getIn(json, path)
|
||||
|
||||
return {
|
||||
json: setIn(json, path, value),
|
||||
revert: [{
|
||||
op: 'replace',
|
||||
path: compileJSONPointer(path),
|
||||
value: oldValue
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item or property
|
||||
* @param {JSON} json
|
||||
* @param {Path} path
|
||||
* @return {{json: JSON, revert: JSONPatch}}
|
||||
*/
|
||||
export function remove (json, path) {
|
||||
const oldValue = getIn(json, path)
|
||||
|
||||
return {
|
||||
json: deleteIn(json, path),
|
||||
revert: [{
|
||||
op: 'add',
|
||||
path: compileJSONPointer(path),
|
||||
value: oldValue
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {JSON} json
|
||||
* @param {Path} path
|
||||
* @param {JSON} value
|
||||
* @return {{json: JSON, revert: JSONPatch}}
|
||||
* @private
|
||||
*/
|
||||
export function add (json, path, value) {
|
||||
const resolvedPath = resolvePathIndex(json, path)
|
||||
const parent = getIn(json, initial(path))
|
||||
const parentIsArray = Array.isArray(parent)
|
||||
|
||||
const updatedJson = parentIsArray
|
||||
? insertAt(json, resolvedPath, value)
|
||||
: setIn(json, resolvedPath, value)
|
||||
|
||||
if (!parentIsArray && existsIn(json, resolvedPath)) {
|
||||
const oldValue = getIn(json, resolvedPath)
|
||||
|
||||
return {
|
||||
json: updatedJson,
|
||||
revert: [{
|
||||
op: 'replace',
|
||||
path: compileJSONPointer(resolvedPath),
|
||||
value: oldValue
|
||||
}]
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
json: updatedJson,
|
||||
revert: [{
|
||||
op: 'remove',
|
||||
path: compileJSONPointer(resolvedPath)
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a value
|
||||
* @param {JSON} json
|
||||
* @param {Path} path
|
||||
* @param {Path} from
|
||||
* @return {{json: JSON, revert: ESONPatch}}
|
||||
* @private
|
||||
*/
|
||||
export function copy (json, path, from) {
|
||||
const value = getIn(json, from)
|
||||
|
||||
return add(json, path, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a value
|
||||
* @param {JSON} json
|
||||
* @param {Path} path
|
||||
* @param {Path} from
|
||||
* @return {{json: JSON, revert: ESONPatch}}
|
||||
* @private
|
||||
*/
|
||||
export function move (json, path, from) {
|
||||
const resolvedPath = resolvePathIndex(json, path)
|
||||
const parent = getIn(json, initial(path))
|
||||
const parentIsArray = Array.isArray(parent)
|
||||
const oldValue = getIn(json, path)
|
||||
const value = getIn(json, from)
|
||||
|
||||
const removedJson = remove(json, from).json
|
||||
const updatedJson = parentIsArray
|
||||
? insertAt(removedJson, resolvedPath, value)
|
||||
: setIn(removedJson, resolvedPath, value)
|
||||
|
||||
if (oldValue !== undefined && !parentIsArray) {
|
||||
// replaces an existing value in an object
|
||||
return {
|
||||
json: updatedJson,
|
||||
revert: [
|
||||
{
|
||||
op: 'move',
|
||||
from: compileJSONPointer(resolvedPath),
|
||||
path: compileJSONPointer(from)
|
||||
},
|
||||
{
|
||||
op: 'add',
|
||||
path: compileJSONPointer(resolvedPath),
|
||||
value: oldValue
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
json: updatedJson,
|
||||
revert: [
|
||||
{
|
||||
op: 'move',
|
||||
from: compileJSONPointer(resolvedPath),
|
||||
path: compileJSONPointer(from)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the data contains the provided value at the specified path.
|
||||
* Throws an error when the test fails.
|
||||
* @param {JSON} json
|
||||
* @param {Path} path
|
||||
* @param {*} value
|
||||
* @return {null | Error} Returns an error when the tests, returns null otherwise
|
||||
*/
|
||||
export function test (json, path, value) {
|
||||
if (value === undefined) {
|
||||
return new Error('Test failed, no value provided')
|
||||
}
|
||||
|
||||
if (!existsIn(json, path)) {
|
||||
return new Error('Test failed, path not found')
|
||||
}
|
||||
|
||||
const actualValue = getIn(json, path)
|
||||
if (!isEqual(actualValue, value)) {
|
||||
return new Error('Test failed, value differs')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the path of an index like '''
|
||||
* @param {JSON} json
|
||||
* @param {Path} path
|
||||
* @returns {Path} Returns the resolved path
|
||||
*/
|
||||
export function resolvePathIndex (json, path) {
|
||||
const parent = getIn(json, initial(path))
|
||||
|
||||
return (path[path.length - 1] === '-')
|
||||
? path.slice(0, path.length - 1).concat(parent.length)
|
||||
: path
|
||||
}
|
|
@ -0,0 +1,334 @@
|
|||
'use strict'
|
||||
|
||||
import { immutableJsonPatch } from './immutableJsonPatch'
|
||||
|
||||
test('test toBe', () => {
|
||||
const a = { x: 2 }
|
||||
const b = { x: 2 }
|
||||
|
||||
// just to be sure toBe does what I think it does...
|
||||
expect(a).toBe(a)
|
||||
expect(b).not.toBe(a)
|
||||
expect(b).toEqual(a)
|
||||
})
|
||||
|
||||
test('jsonpatch add', () => {
|
||||
const json = {
|
||||
arr: [1,2,3],
|
||||
obj: {a : 2}
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{op: 'add', path: '/obj/b', value: {foo: 'bar'}}
|
||||
]
|
||||
|
||||
const result = immutableJsonPatch(json, patch)
|
||||
|
||||
expect(result.json).toEqual({
|
||||
arr: [1,2,3],
|
||||
obj: {a : 2, b: {foo: 'bar'}}
|
||||
})
|
||||
expect(result.revert).toEqual([
|
||||
{op: 'remove', path: '/obj/b'}
|
||||
])
|
||||
expect(result.json.arr).toBe(json.arr)
|
||||
})
|
||||
|
||||
test('jsonpatch add: insert in matrix', () => {
|
||||
const json = {
|
||||
arr: [1,2,3],
|
||||
obj: {a : 2}
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{op: 'add', path: '/arr/1', value: 4}
|
||||
]
|
||||
|
||||
const result = immutableJsonPatch(json, patch)
|
||||
|
||||
expect(result.json).toEqual({
|
||||
arr: [1,4,2,3],
|
||||
obj: {a : 2}
|
||||
})
|
||||
expect(result.revert).toEqual([
|
||||
{op: 'remove', path: '/arr/1'}
|
||||
])
|
||||
expect(result.json.obj).toBe(json.obj)
|
||||
})
|
||||
|
||||
test('jsonpatch add: append to matrix', () => {
|
||||
const json = {
|
||||
arr: [1,2,3],
|
||||
obj: {a : 2}
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{op: 'add', path: '/arr/-', value: 4}
|
||||
]
|
||||
|
||||
const result = immutableJsonPatch(json, patch)
|
||||
|
||||
expect(result.json).toEqual({
|
||||
arr: [1,2,3,4],
|
||||
obj: {a : 2}
|
||||
})
|
||||
expect(result.revert).toEqual([
|
||||
{op: 'remove', path: '/arr/3'}
|
||||
])
|
||||
expect(result.json.obj).toBe(json.obj)
|
||||
})
|
||||
|
||||
test('jsonpatch remove', () => {
|
||||
const json = {
|
||||
arr: [1,2,3],
|
||||
obj: {a : 4},
|
||||
unchanged: {}
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{op: 'remove', path: '/obj/a'},
|
||||
{op: 'remove', path: '/arr/1'},
|
||||
]
|
||||
|
||||
const result = immutableJsonPatch(json, patch)
|
||||
|
||||
expect(result.json).toEqual({
|
||||
arr: [1,3],
|
||||
obj: {},
|
||||
unchanged: {}
|
||||
})
|
||||
expect(result.revert).toEqual([
|
||||
{op: 'add', path: '/arr/1', value: 2},
|
||||
{op: 'add', path: '/obj/a', value: 4}
|
||||
])
|
||||
|
||||
// test revert
|
||||
const result2 = immutableJsonPatch(result.json, result.revert)
|
||||
|
||||
expect(result2.json).toEqual(json)
|
||||
expect(result2.revert).toEqual(patch)
|
||||
expect(result.json.unchanged).toBe(json.unchanged)
|
||||
})
|
||||
|
||||
test('jsonpatch replace', () => {
|
||||
const json = {
|
||||
arr: [1,2,3],
|
||||
obj: {a : 4},
|
||||
unchanged: {}
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{op: 'replace', path: '/obj/a', value: 400},
|
||||
{op: 'replace', path: '/arr/1', value: 200},
|
||||
]
|
||||
|
||||
const result = immutableJsonPatch(json, patch)
|
||||
|
||||
expect(result.json).toEqual({
|
||||
arr: [1,200,3],
|
||||
obj: {a: 400},
|
||||
unchanged: {}
|
||||
})
|
||||
expect(result.revert).toEqual([
|
||||
{op: 'replace', path: '/arr/1', value: 2},
|
||||
{op: 'replace', path: '/obj/a', value: 4}
|
||||
])
|
||||
|
||||
// test revert
|
||||
const result2 = immutableJsonPatch(result.json, result.revert)
|
||||
|
||||
expect(result2.json).toEqual(json)
|
||||
expect(result2.revert).toEqual([
|
||||
{op: 'replace', path: '/obj/a', value: 400},
|
||||
{op: 'replace', path: '/arr/1', value: 200}
|
||||
])
|
||||
expect(result.json.unchanged).toBe(json.unchanged)
|
||||
})
|
||||
|
||||
test('jsonpatch copy', () => {
|
||||
const json = {
|
||||
arr: [1,2,3],
|
||||
obj: {a : 4}
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{op: 'copy', from: '/obj', path: '/arr/2'},
|
||||
]
|
||||
|
||||
const result = immutableJsonPatch(json, patch)
|
||||
|
||||
expect(result.json).toEqual({
|
||||
arr: [1, 2, {a:4}, 3],
|
||||
obj: {a: 4}
|
||||
})
|
||||
expect(result.revert).toEqual([
|
||||
{op: 'remove', path: '/arr/2'}
|
||||
])
|
||||
|
||||
// test revert
|
||||
const result2 = immutableJsonPatch(result.json, result.revert)
|
||||
|
||||
expect(result2.json).toEqual(json)
|
||||
expect(result2.revert).toEqual([
|
||||
{op: 'add', path: '/arr/2', value: {a: 4}}
|
||||
])
|
||||
expect(result.json.obj).toBe(json.obj)
|
||||
expect(result.json.arr[2]).toBe(json.obj)
|
||||
})
|
||||
|
||||
test('jsonpatch move', () => {
|
||||
const json = {
|
||||
arr: [1,2,3],
|
||||
obj: {a : 4},
|
||||
unchanged: {}
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{op: 'move', from: '/obj', path: '/arr/2'},
|
||||
]
|
||||
|
||||
const result = immutableJsonPatch(json, patch)
|
||||
|
||||
expect(result.error).toEqual(null)
|
||||
expect(result.json).toEqual({
|
||||
arr: [1, 2, {a:4}, 3],
|
||||
unchanged: {}
|
||||
})
|
||||
expect(result.revert).toEqual([
|
||||
{op: 'move', from: '/arr/2', path: '/obj'}
|
||||
])
|
||||
|
||||
// test revert
|
||||
const result2 = immutableJsonPatch(result.json, result.revert)
|
||||
|
||||
expect(result2.json).toEqual(json)
|
||||
expect(result2.revert).toEqual(patch)
|
||||
expect(result.json.arr[2]).toBe(json.obj)
|
||||
expect(result.json.unchanged).toBe(json.unchanged)
|
||||
})
|
||||
|
||||
test('jsonpatch move and replace', () => {
|
||||
const json = { a: 2, b: 3 }
|
||||
|
||||
const patch = [
|
||||
{op: 'move', from: '/a', path: '/b'},
|
||||
]
|
||||
|
||||
const result = immutableJsonPatch(json, patch)
|
||||
|
||||
expect(result.json).toEqual({ b : 2 })
|
||||
expect(result.revert).toEqual([
|
||||
{op:'move', from: '/b', path: '/a'},
|
||||
{op:'add', path:'/b', value: 3}
|
||||
])
|
||||
|
||||
// test revert
|
||||
const result2 = immutableJsonPatch(result.json, result.revert)
|
||||
|
||||
expect(result2.json).toEqual(json)
|
||||
expect(result2.revert).toEqual([
|
||||
{op: 'remove', path: '/b'},
|
||||
{op: 'move', from: '/a', path: '/b'}
|
||||
])
|
||||
})
|
||||
|
||||
test('jsonpatch move and replace (nested)', () => {
|
||||
const json = {
|
||||
arr: [1,2,3],
|
||||
obj: {a : 4},
|
||||
unchanged: {}
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{op: 'move', from: '/obj', path: '/arr'},
|
||||
]
|
||||
|
||||
const result = immutableJsonPatch(json, patch)
|
||||
|
||||
expect(result.json).toEqual({
|
||||
arr: {a:4},
|
||||
unchanged: {}
|
||||
})
|
||||
expect(result.revert).toEqual([
|
||||
{op:'move', from: '/arr', path: '/obj'},
|
||||
{op:'add', path:'/arr', value: [1,2,3]}
|
||||
])
|
||||
|
||||
// test revert
|
||||
const result2 = immutableJsonPatch(result.json, result.revert)
|
||||
|
||||
expect(result2.json).toEqual(json)
|
||||
expect(result2.revert).toEqual([
|
||||
{op: 'remove', path: '/arr'},
|
||||
{op: 'move', from: '/obj', path: '/arr'}
|
||||
])
|
||||
expect(result.json.unchanged).toBe(json.unchanged)
|
||||
expect(result2.json.unchanged).toBe(json.unchanged)
|
||||
})
|
||||
|
||||
test('jsonpatch test (ok)', () => {
|
||||
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 result = immutableJsonPatch(json, patch)
|
||||
|
||||
expect(result.json).toEqual({
|
||||
arr: [1,2,3],
|
||||
obj: {a : 4},
|
||||
added: 'ok'
|
||||
})
|
||||
expect(result.revert).toEqual([
|
||||
{op: 'remove', path: '/added'}
|
||||
])
|
||||
})
|
||||
|
||||
test('jsonpatch test (fail: path not found)', () => {
|
||||
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 result = immutableJsonPatch(json, patch)
|
||||
|
||||
// patch shouldn't be applied
|
||||
expect(result.json).toEqual({
|
||||
arr: [1,2,3],
|
||||
obj: {a : 4}
|
||||
})
|
||||
expect(result.revert).toEqual([])
|
||||
expect(result.error.toString()).toEqual('Error: Test failed, path not found')
|
||||
})
|
||||
|
||||
test('jsonpatch test (fail: value not equal)', () => {
|
||||
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 result = immutableJsonPatch(json, patch)
|
||||
|
||||
// patch shouldn't be applied
|
||||
expect(result.json).toEqual({
|
||||
arr: [1,2,3],
|
||||
obj: {a : 4}
|
||||
})
|
||||
expect(result.revert).toEqual([])
|
||||
expect(result.error.toString()).toEqual('Error: Test failed, value differs')
|
||||
})
|
|
@ -4,7 +4,7 @@ import JSONEditor from './components/JSONEditor'
|
|||
import CodeMode from './components/CodeMode'
|
||||
import TextMode from './components/TextMode'
|
||||
import TreeMode from './components/TreeMode'
|
||||
import { compileJSONPointer, parseJSONPointer } from './eson'
|
||||
import { compileJSONPointer, parseJSONPointer } from './jsonPointer'
|
||||
|
||||
const modes = {
|
||||
code: CodeMode,
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* Parse a JSON Pointer
|
||||
* WARNING: this is not a complete implementation
|
||||
* @param {string} pointer
|
||||
* @return {Path}
|
||||
*/
|
||||
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 implementation
|
||||
* @param {Path} path
|
||||
* @return {string}
|
||||
*/
|
||||
export function compileJSONPointer (path) {
|
||||
return path
|
||||
.map(p => '/' + String(p).replace(/~/g, '~0').replace(/\//g, '~1'))
|
||||
.join('')
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import {compileJSONPointer, parseJSONPointer} from './jsonPointer'
|
||||
|
||||
test('parseJSONPointer', () => {
|
||||
expect(parseJSONPointer('/obj/a')).toEqual(['obj', 'a'])
|
||||
expect(parseJSONPointer('/arr/-')).toEqual(['arr', '-'])
|
||||
expect(parseJSONPointer('/foo/~1~0 ~0~1')).toEqual(['foo', '/~ ~/'])
|
||||
expect(parseJSONPointer('/obj')).toEqual(['obj'])
|
||||
expect(parseJSONPointer('/')).toEqual([''])
|
||||
expect(parseJSONPointer('')).toEqual([])
|
||||
})
|
||||
|
||||
test('compileJSONPointer', () => {
|
||||
expect(compileJSONPointer(['foo', 'bar'])).toEqual('/foo/bar')
|
||||
expect(compileJSONPointer(['foo', '/~ ~/'])).toEqual('/foo/~1~0 ~0~1')
|
||||
expect(compileJSONPointer([''])).toEqual('/')
|
||||
expect(compileJSONPointer([])).toEqual('')
|
||||
})
|
|
@ -1,356 +0,0 @@
|
|||
import isEqual from 'lodash/isEqual'
|
||||
import initial from 'lodash/initial'
|
||||
import last from 'lodash/last'
|
||||
|
||||
import {
|
||||
setIn, updateIn, getIn, deleteIn, insertAt,
|
||||
cloneWithSymbols
|
||||
} from './utils/immutabilityHelpers'
|
||||
import {
|
||||
META,
|
||||
jsonToEson, esonToJson, updatePaths,
|
||||
parseJSONPointer, compileJSONPointer,
|
||||
expandAll, pathExists, resolvePathIndex, createId, applyEsonState
|
||||
} from './eson'
|
||||
|
||||
/**
|
||||
* Apply a patch to a ESON object
|
||||
* @param {ESON} eson
|
||||
* @param {Array} patch A JSON patch
|
||||
* @param {function(path: Path)} [expand] Optional function to determine
|
||||
* what nodes must be expanded
|
||||
* @return {{data: ESON, revert: Object[], error: Error | null}}
|
||||
*/
|
||||
export function patchEson (eson, patch, expand = expandAll) {
|
||||
let updatedEson = eson
|
||||
let revert = []
|
||||
|
||||
for (let i = 0; i < patch.length; i++) {
|
||||
const action = patch[i]
|
||||
const path = action.path ? parseJSONPointer(action.path) : null
|
||||
const from = action.from ? parseJSONPointer(action.from) : null
|
||||
const options = action.meta
|
||||
|
||||
// TODO: check whether action.op and action.path exist
|
||||
|
||||
switch (action.op) {
|
||||
case 'add': {
|
||||
const newValue = jsonToEson(action.value, path)
|
||||
const newValueWithState = (options && options.state)
|
||||
? applyEsonState(newValue, options.state)
|
||||
: newValue
|
||||
const result = add(updatedEson, path, newValueWithState, options)
|
||||
updatedEson = result.data
|
||||
revert = result.revert.concat(revert)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'remove': {
|
||||
const result = remove(updatedEson, path)
|
||||
updatedEson = result.data
|
||||
revert = result.revert.concat(revert)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'replace': {
|
||||
const newValue = jsonToEson(action.value, path)
|
||||
const newValueWithState = (options && options.state)
|
||||
? applyEsonState(newValue, options.state)
|
||||
: newValue
|
||||
const result = replace(updatedEson, path, newValueWithState)
|
||||
updatedEson = result.data
|
||||
revert = result.revert.concat(revert)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'copy': {
|
||||
if (!action.from) {
|
||||
return {
|
||||
data: eson,
|
||||
revert: [],
|
||||
error: new Error('Property "from" expected in copy action ' + JSON.stringify(action))
|
||||
}
|
||||
}
|
||||
|
||||
const result = copy(updatedEson, path, from, options)
|
||||
updatedEson = result.data
|
||||
revert = result.revert.concat(revert)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case 'move': {
|
||||
if (!action.from) {
|
||||
return {
|
||||
data: eson,
|
||||
revert: [],
|
||||
error: new Error('Property "from" expected in move action ' + JSON.stringify(action))
|
||||
}
|
||||
}
|
||||
|
||||
const result = move(updatedEson, path, from, options)
|
||||
updatedEson = 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(updatedEson, path, action.value)
|
||||
if (error) {
|
||||
return { data: eson, revert: [], error}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
// unknown ESONPatch operation. Cancel the whole patch and return an error
|
||||
return {
|
||||
data: eson,
|
||||
revert: [],
|
||||
error: new Error('Unknown ESONPatch op ' + JSON.stringify(action.op))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: updatedEson,
|
||||
revert,
|
||||
error: null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace an existing item
|
||||
* @param {ESON} data
|
||||
* @param {Path} path
|
||||
* @param {ESON} value
|
||||
* @return {{data: ESON, revert: ESONPatch}}
|
||||
*/
|
||||
export function replace (data, path, value) {
|
||||
const oldValue = getIn(data, path)
|
||||
|
||||
// keep the original id
|
||||
let newValue = setIn(value, [META, 'id'], oldValue[META].id)
|
||||
|
||||
return {
|
||||
data: setIn(data, path, newValue),
|
||||
revert: [{
|
||||
op: 'replace',
|
||||
path: compileJSONPointer(path),
|
||||
value: esonToJson(oldValue),
|
||||
meta: {
|
||||
type: oldValue[META].type
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item or property
|
||||
* @param {ESON} data
|
||||
* @param {Path} path
|
||||
* @return {{data: ESON, revert: ESONPatch}}
|
||||
*/
|
||||
export function remove (data, path) {
|
||||
// console.log('remove', path)
|
||||
|
||||
const parentPath = initial(path)
|
||||
const parent = getIn(data, parentPath)
|
||||
const dataValue = getIn(data, path)
|
||||
const value = esonToJson(dataValue)
|
||||
|
||||
if (parent[META].type === 'Array') {
|
||||
return {
|
||||
data: updatePaths(deleteIn(data, path)),
|
||||
revert: [{
|
||||
op: 'add',
|
||||
path: compileJSONPointer(path),
|
||||
value,
|
||||
meta: {
|
||||
type: dataValue[META].type
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
else { // parent[META].type === 'Object'
|
||||
const prop = last(path)
|
||||
const index = parent[META].props.indexOf(prop)
|
||||
const nextProp = parent[META].props[index + 1] || null
|
||||
|
||||
let updatedParent = deleteIn(parent, [prop]) // delete property itself
|
||||
updatedParent = deleteIn(updatedParent, [META, 'props', index]) // delete property from the props list
|
||||
|
||||
return {
|
||||
data: setIn(data, parentPath, updatePaths(updatedParent, parentPath)),
|
||||
revert: [{
|
||||
op: 'add',
|
||||
path: compileJSONPointer(path),
|
||||
value,
|
||||
meta: {
|
||||
type: dataValue[META].type,
|
||||
before: nextProp
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ESON} data
|
||||
* @param {Path} path
|
||||
* @param {ESON} value
|
||||
* @param {{before?: string}} [options]
|
||||
* @return {{data: ESON, revert: ESONPatch}}
|
||||
* @private
|
||||
*/
|
||||
export function add (data, path, value, options) {
|
||||
const parentPath = initial(path)
|
||||
const parent = getIn(data, parentPath)
|
||||
const resolvedPath = resolvePathIndex(data, path)
|
||||
const prop = last(resolvedPath)
|
||||
|
||||
let updatedEson
|
||||
if (parent[META].type === 'Array') {
|
||||
updatedEson = updatePaths(insertAt(data, resolvedPath, value))
|
||||
}
|
||||
else { // parent[META].type === 'Object'
|
||||
updatedEson = updateIn(data, parentPath, (parent) => {
|
||||
const oldValue = getIn(data, path)
|
||||
const props = parent[META].props
|
||||
const existingIndex = props.indexOf(prop)
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// replace existing item, keep existing id
|
||||
const newValue = setIn(value, [META, 'id'], oldValue[META].id)
|
||||
return setIn(parent, [prop], updatePaths(newValue, path))
|
||||
}
|
||||
else {
|
||||
// insert new item
|
||||
const index = (options && typeof options.before === 'string')
|
||||
? props.indexOf(options.before) // insert before
|
||||
: props.length // append
|
||||
|
||||
let updatedKeys = props.slice()
|
||||
updatedKeys.splice(index, 0, prop)
|
||||
const updatedParent = setIn(parent, [prop], updatePaths(value, parentPath.concat(prop)))
|
||||
return setIn(updatedParent, [META, 'props'], updatedKeys)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (parent[META].type === 'Object' && pathExists(data, resolvedPath)) {
|
||||
const oldValue = getIn(data, resolvedPath)
|
||||
|
||||
return {
|
||||
data: updatedEson,
|
||||
revert: [{
|
||||
op: 'replace',
|
||||
path: compileJSONPointer(resolvedPath),
|
||||
value: esonToJson(oldValue),
|
||||
meta: { type: oldValue[META].type }
|
||||
}]
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
data: updatedEson,
|
||||
revert: [{
|
||||
op: 'remove',
|
||||
path: compileJSONPointer(resolvedPath)
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a value
|
||||
* @param {ESON} data
|
||||
* @param {Path} path
|
||||
* @param {Path} from
|
||||
* @param {{before?: string}} [options]
|
||||
* @return {{data: ESON, revert: ESONPatch}}
|
||||
* @private
|
||||
*/
|
||||
export function copy (data, path, from, options) {
|
||||
const value = getIn(data, from)
|
||||
|
||||
// create new id for the copied item
|
||||
let updatedValue = cloneWithSymbols(value)
|
||||
updatedValue[META] = setIn(updatedValue[META], ['id'], createId())
|
||||
|
||||
return add(data, path, updatedValue, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a value
|
||||
* @param {ESON} data
|
||||
* @param {Path} path
|
||||
* @param {Path} from
|
||||
* @param {{before?: string}} [options]
|
||||
* @return {{data: ESON, revert: ESONPatch}}
|
||||
* @private
|
||||
*/
|
||||
export function move (data, path, from, options) {
|
||||
const dataValue = getIn(data, from)
|
||||
|
||||
const parentPathFrom = initial(from)
|
||||
const parent = getIn(data, parentPathFrom)
|
||||
|
||||
const result1 = remove(data, from)
|
||||
const result2 = add(result1.data, path, dataValue, options)
|
||||
|
||||
const before = result1.revert[0].meta.before
|
||||
const beforeNeeded = (parent[META].type === 'Object' && before)
|
||||
|
||||
if (result2.revert[0].op === 'replace') {
|
||||
const value = result2.revert[0].value
|
||||
const type = result2.revert[0].meta.type
|
||||
const options = beforeNeeded ? { type, before } : { type }
|
||||
|
||||
return {
|
||||
data: result2.data,
|
||||
revert: [
|
||||
{ op: 'move', from: compileJSONPointer(path), path: compileJSONPointer(from) },
|
||||
{ op: 'add', path: compileJSONPointer(path), value, meta: options}
|
||||
]
|
||||
}
|
||||
}
|
||||
else { // result2.revert[0].op === 'remove'
|
||||
return {
|
||||
data: result2.data,
|
||||
revert: beforeNeeded
|
||||
? [{ op: 'move', from: compileJSONPointer(path), path: compileJSONPointer(from), meta: { before } }]
|
||||
: [{ op: 'move', from: compileJSONPointer(path), path: compileJSONPointer(from) }]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether the data contains the provided value at the specified path.
|
||||
* Throws an error when the test fails.
|
||||
* @param {ESON} data
|
||||
* @param {Path} path
|
||||
* @param {*} value
|
||||
* @return {null | Error} Returns an error when the tests, returns null otherwise
|
||||
*/
|
||||
export function test (data, path, value) {
|
||||
if (value === undefined) {
|
||||
return new Error('Test failed, no value provided')
|
||||
}
|
||||
|
||||
if (!pathExists(data, path)) {
|
||||
return new Error('Test failed, path not found')
|
||||
}
|
||||
|
||||
const actualValue = getIn(data, path)
|
||||
if (!isEqual(esonToJson(actualValue), value)) {
|
||||
return new Error('Test failed, value differs')
|
||||
}
|
||||
}
|
|
@ -1,572 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
import { readFileSync } from 'fs'
|
||||
import { META, jsonToEson, esonToJson, expandOne } from './eson'
|
||||
import { patchEson } from './patchEson'
|
||||
import { assertDeepEqualEson } from './utils/assertDeepEqualEson'
|
||||
|
||||
test('jsonpatch add', () => {
|
||||
const json = {
|
||||
arr: [1,2,3],
|
||||
obj: {a : 2}
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{op: 'add', path: '/obj/b', value: {foo: 'bar'}}
|
||||
]
|
||||
|
||||
const data = jsonToEson(json)
|
||||
const result = patchEson(data, patch)
|
||||
const patchedData = result.data
|
||||
const revert = result.revert
|
||||
|
||||
assertDeepEqualEson(patchedData, jsonToEson({
|
||||
arr: [1,2,3],
|
||||
obj: {a : 2, b: {foo: 'bar'}}
|
||||
}))
|
||||
expect(revert).toEqual([
|
||||
{op: 'remove', path: '/obj/b'}
|
||||
])
|
||||
})
|
||||
|
||||
test('jsonpatch add: insert in matrix', () => {
|
||||
const json = {
|
||||
arr: [1,2,3],
|
||||
obj: {a : 2}
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{op: 'add', path: '/arr/1', value: 4}
|
||||
]
|
||||
|
||||
const data = jsonToEson(json)
|
||||
const result = patchEson(data, patch)
|
||||
const patchedData = result.data
|
||||
const revert = result.revert
|
||||
|
||||
assertDeepEqualEson(patchedData, jsonToEson({
|
||||
arr: [1,4,2,3],
|
||||
obj: {a : 2}
|
||||
}))
|
||||
expect(revert).toEqual([
|
||||
{op: 'remove', path: '/arr/1'}
|
||||
])
|
||||
})
|
||||
|
||||
test('jsonpatch add: append to matrix', () => {
|
||||
const json = {
|
||||
arr: [1,2,3],
|
||||
obj: {a : 2}
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{op: 'add', path: '/arr/-', value: 4}
|
||||
]
|
||||
|
||||
const data = jsonToEson(json)
|
||||
const result = patchEson(data, patch)
|
||||
const patchedData = result.data
|
||||
const revert = result.revert
|
||||
|
||||
assertDeepEqualEson(patchedData, jsonToEson({
|
||||
arr: [1,2,3,4],
|
||||
obj: {a : 2}
|
||||
}))
|
||||
expect(revert).toEqual([
|
||||
{op: 'remove', path: '/arr/3'}
|
||||
])
|
||||
})
|
||||
|
||||
test('jsonpatch add: apply eson state', () => {
|
||||
const json = {
|
||||
a: 2
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: 'add',
|
||||
path: '/b',
|
||||
value: {c: {d: 3}},
|
||||
meta: {
|
||||
state: {
|
||||
'': { expanded: true },
|
||||
'/c/d': { expanded: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const data = jsonToEson(json)
|
||||
const result = patchEson(data, patch)
|
||||
const patchedData = result.data
|
||||
|
||||
let expected = jsonToEson({
|
||||
a: 2,
|
||||
b: {c: {d: 3}}
|
||||
})
|
||||
expected = expandOne(expected, ['b'], true)
|
||||
expected = expandOne(expected, ['b', 'c', 'd'], true)
|
||||
|
||||
assertDeepEqualEson(patchedData, expected)
|
||||
})
|
||||
|
||||
test('jsonpatch remove', () => {
|
||||
const json = {
|
||||
arr: [1,2,3],
|
||||
obj: {a : 4}
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{op: 'remove', path: '/obj/a'},
|
||||
{op: 'remove', path: '/arr/1'},
|
||||
]
|
||||
|
||||
const data = jsonToEson(json)
|
||||
const result = patchEson(data, patch)
|
||||
const patchedData = result.data
|
||||
const revert = result.revert
|
||||
const patchedJson = esonToJson(patchedData)
|
||||
|
||||
assertDeepEqualEson(patchedData, jsonToEson({
|
||||
arr: [1,3],
|
||||
obj: {}
|
||||
}))
|
||||
expect(revert).toEqual([
|
||||
{op: 'add', path: '/arr/1', value: 2, meta: {type: 'value'}},
|
||||
{op: 'add', path: '/obj/a', value: 4, meta: {type: 'value', before: null}}
|
||||
])
|
||||
|
||||
// test revert
|
||||
const data2 = jsonToEson(patchedJson)
|
||||
const result2 = patchEson(data2, revert)
|
||||
const patchedData2 = result2.data
|
||||
const revert2 = result2.revert
|
||||
const patchedJson2 = esonToJson(patchedData2)
|
||||
|
||||
expect(patchedJson2).toEqual(json)
|
||||
expect(revert2).toEqual(patch)
|
||||
})
|
||||
|
||||
test('jsonpatch replace', () => {
|
||||
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 = jsonToEson(json)
|
||||
const result = patchEson(data, patch)
|
||||
const patchedData = result.data
|
||||
const revert = result.revert
|
||||
const patchedJson = esonToJson(patchedData)
|
||||
|
||||
assertDeepEqualEson(patchedData, jsonToEson({
|
||||
arr: [1,200,3],
|
||||
obj: {a: 400}
|
||||
}))
|
||||
expect(revert).toEqual([
|
||||
{op: 'replace', path: '/arr/1', value: 2, meta: {type: 'value'}},
|
||||
{op: 'replace', path: '/obj/a', value: 4, meta: {type: 'value'}}
|
||||
])
|
||||
|
||||
// test revert
|
||||
const data2 = jsonToEson(patchedJson)
|
||||
const result2 = patchEson(data2, revert)
|
||||
const patchedData2 = result2.data
|
||||
const revert2 = result2.revert
|
||||
const patchedJson2 = esonToJson(patchedData2)
|
||||
|
||||
expect(patchedJson2).toEqual(json)
|
||||
expect(revert2).toEqual([
|
||||
{op: 'replace', path: '/obj/a', value: 400, meta: {type: 'value'}},
|
||||
{op: 'replace', path: '/arr/1', value: 200, meta: {type: 'value'}}
|
||||
])
|
||||
})
|
||||
|
||||
test('jsonpatch replace (keep ids intact)', () => {
|
||||
const json = { value: 42 }
|
||||
const patch = [
|
||||
{op: 'replace', path: '/value', value: 100}
|
||||
]
|
||||
|
||||
const data = jsonToEson(json)
|
||||
const valueId = data.value[META].id
|
||||
|
||||
const patchedData = patchEson(data, patch).data
|
||||
const patchedValueId = patchedData.value[META].id
|
||||
|
||||
expect(patchedValueId).toEqual(valueId)
|
||||
})
|
||||
|
||||
test('jsonpatch replace: apply eson state', () => {
|
||||
const json = {
|
||||
a: 2,
|
||||
b: 4
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: 'replace',
|
||||
path: '/b',
|
||||
value: {c: {d: 3}},
|
||||
meta: {
|
||||
state: {
|
||||
'': { expanded: true },
|
||||
'/c/d': { expanded: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const data = jsonToEson(json)
|
||||
const result = patchEson(data, patch)
|
||||
const patchedData = result.data
|
||||
|
||||
let expected = jsonToEson({
|
||||
a: 2,
|
||||
b: {c: {d: 3}}
|
||||
})
|
||||
expected = expandOne(expected, ['b'], true)
|
||||
expected = expandOne(expected, ['b', 'c', 'd'], true)
|
||||
|
||||
assertDeepEqualEson(patchedData, expected)
|
||||
})
|
||||
|
||||
test('jsonpatch copy', () => {
|
||||
const json = {
|
||||
arr: [1,2,3],
|
||||
obj: {a : 4}
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{op: 'copy', from: '/obj', path: '/arr/2'},
|
||||
]
|
||||
|
||||
const data = jsonToEson(json)
|
||||
const result = patchEson(data, patch)
|
||||
const patchedData = result.data
|
||||
const revert = result.revert
|
||||
const patchedJson = esonToJson(patchedData)
|
||||
|
||||
expect(patchedJson).toEqual({
|
||||
arr: [1, 2, {a:4}, 3],
|
||||
obj: {a: 4}
|
||||
})
|
||||
expect(revert).toEqual([
|
||||
{op: 'remove', path: '/arr/2'}
|
||||
])
|
||||
|
||||
// test revert
|
||||
const data2 = jsonToEson(patchedJson)
|
||||
const result2 = patchEson(data2, revert)
|
||||
const patchedData2 = result2.data
|
||||
const revert2 = result2.revert
|
||||
const patchedJson2 = esonToJson(patchedData2)
|
||||
|
||||
expect(patchedJson2).toEqual(json)
|
||||
expect(revert2).toEqual([
|
||||
{op: 'add', path: '/arr/2', value: {a: 4}, meta: {type: 'Object'}}
|
||||
])
|
||||
})
|
||||
|
||||
test('jsonpatch copy (keeps the same ids)', () => {
|
||||
const json = { foo: { bar: 42 } }
|
||||
const patch = [
|
||||
{op: 'copy', from: '/foo', path: '/copied'}
|
||||
]
|
||||
|
||||
const data = jsonToEson(json)
|
||||
const fooId = data.foo[META].id
|
||||
const barId = data.foo.bar[META].id
|
||||
|
||||
const patchedData = patchEson(data, patch).data
|
||||
const patchedFooId = patchedData.foo[META].id
|
||||
const patchedBarId = patchedData.foo.bar[META].id
|
||||
const copiedId = patchedData.copied[META].id
|
||||
const patchedCopiedBarId = patchedData.copied.bar[META].id
|
||||
|
||||
expect(patchedFooId).toEqual(fooId)
|
||||
expect(patchedBarId).toEqual(barId)
|
||||
|
||||
expect(copiedId).not.toEqual(fooId)
|
||||
|
||||
// The id's of the copied childs are the same, that's okish, they will not bite each other
|
||||
expect(patchedCopiedBarId).toEqual(patchedBarId)
|
||||
})
|
||||
|
||||
test('jsonpatch move', () => {
|
||||
const json = {
|
||||
arr: [1,2,3],
|
||||
obj: {a : 4}
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{op: 'move', from: '/obj', path: '/arr/2'},
|
||||
]
|
||||
|
||||
const data = jsonToEson(json)
|
||||
const result = patchEson(data, patch)
|
||||
const patchedData = result.data
|
||||
const revert = result.revert
|
||||
const patchedJson = esonToJson(patchedData)
|
||||
|
||||
expect(result.error).toEqual(null)
|
||||
expect(patchedJson).toEqual({
|
||||
arr: [1, 2, {a:4}, 3]
|
||||
})
|
||||
expect(revert).toEqual([
|
||||
{op: 'move', from: '/arr/2', path: '/obj'}
|
||||
])
|
||||
|
||||
// test revert
|
||||
const data2 = jsonToEson(patchedJson)
|
||||
const result2 = patchEson(data2, revert)
|
||||
const patchedData2 = result2.data
|
||||
const revert2 = result2.revert
|
||||
const patchedJson2 = esonToJson(patchedData2)
|
||||
|
||||
expect(patchedJson2).toEqual(json)
|
||||
expect(revert2).toEqual(patch)
|
||||
})
|
||||
|
||||
test('jsonpatch move before', () => {
|
||||
const json = {
|
||||
arr: [1,2,3],
|
||||
obj: {a : 4},
|
||||
zzz: 'zzz'
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{op: 'move', from: '/obj', path: '/arr/2'},
|
||||
]
|
||||
|
||||
const data = jsonToEson(json)
|
||||
const result = patchEson(data, patch)
|
||||
const patchedData = result.data
|
||||
const revert = result.revert
|
||||
const patchedJson = esonToJson(patchedData)
|
||||
|
||||
expect(result.error).toEqual(null)
|
||||
expect(patchedJson).toEqual({
|
||||
arr: [1, 2, {a:4}, 3],
|
||||
zzz: 'zzz'
|
||||
})
|
||||
expect(revert).toEqual([
|
||||
{op: 'move', from: '/arr/2', path: '/obj', meta: {before: 'zzz'}}
|
||||
])
|
||||
|
||||
// test revert
|
||||
const data2 = jsonToEson(patchedJson)
|
||||
const result2 = patchEson(data2, revert)
|
||||
const patchedData2 = result2.data
|
||||
const revert2 = result2.revert
|
||||
const patchedJson2 = esonToJson(patchedData2)
|
||||
|
||||
expect(patchedJson2).toEqual(json)
|
||||
expect(revert2).toEqual(patch)
|
||||
})
|
||||
|
||||
test('jsonpatch move and replace', () => {
|
||||
const json = { a: 2, b: 3 }
|
||||
|
||||
const patch = [
|
||||
{op: 'move', from: '/a', path: '/b'},
|
||||
]
|
||||
|
||||
const data = jsonToEson(json)
|
||||
|
||||
const result = patchEson(data, patch)
|
||||
const patchedData = result.data
|
||||
const revert = result.revert
|
||||
const patchedJson = esonToJson(patchedData)
|
||||
|
||||
// id of the replaced B must be kept intact
|
||||
expect(patchedData.b[META].id).toEqual(data.b[META].id)
|
||||
|
||||
assertDeepEqualEson(patchedData, jsonToEson({b: 2}))
|
||||
expect(patchedJson).toEqual({ b : 2 })
|
||||
expect(revert).toEqual([
|
||||
{op:'move', from: '/b', path: '/a'},
|
||||
{op:'add', path:'/b', value: 3, meta: {type: 'value', before: 'b'}}
|
||||
])
|
||||
|
||||
// test revert
|
||||
const data2 = jsonToEson(patchedJson)
|
||||
const result2 = patchEson(data2, revert)
|
||||
const patchedData2 = result2.data
|
||||
const revert2 = result2.revert
|
||||
const patchedJson2 = esonToJson(patchedData2)
|
||||
|
||||
expect(patchedJson2).toEqual(json)
|
||||
expect(revert2).toEqual([
|
||||
{op: 'remove', path: '/b'},
|
||||
{op: 'move', from: '/a', path: '/b'}
|
||||
])
|
||||
})
|
||||
|
||||
test('jsonpatch move and replace (nested)', () => {
|
||||
const json = {
|
||||
arr: [1,2,3],
|
||||
obj: {a : 4}
|
||||
}
|
||||
|
||||
const patch = [
|
||||
{op: 'move', from: '/obj', path: '/arr'},
|
||||
]
|
||||
|
||||
const data = jsonToEson(json)
|
||||
const result = patchEson(data, patch)
|
||||
const patchedData = result.data
|
||||
const revert = result.revert
|
||||
const patchedJson = esonToJson(patchedData)
|
||||
|
||||
expect(patchedJson).toEqual({
|
||||
arr: {a:4}
|
||||
})
|
||||
expect(revert).toEqual([
|
||||
{op:'move', from: '/arr', path: '/obj'},
|
||||
{op:'add', path:'/arr', value: [1,2,3], meta: {type: 'Array'}}
|
||||
])
|
||||
|
||||
// test revert
|
||||
const data2 = jsonToEson(patchedJson)
|
||||
const result2 = patchEson(data2, revert)
|
||||
const patchedData2 = result2.data
|
||||
const revert2 = result2.revert
|
||||
const patchedJson2 = esonToJson(patchedData2)
|
||||
|
||||
expect(patchedJson2).toEqual(json)
|
||||
expect(revert2).toEqual([
|
||||
{op: 'remove', path: '/arr'},
|
||||
{op: 'move', from: '/obj', path: '/arr'}
|
||||
])
|
||||
})
|
||||
|
||||
test('jsonpatch move (keep id intact)', () => {
|
||||
const json = { value: 42 }
|
||||
const patch = [
|
||||
{op: 'move', from: '/value', path: '/moved'}
|
||||
]
|
||||
|
||||
const data = jsonToEson(json)
|
||||
const valueId = data.value[META].id
|
||||
|
||||
const patchedData = patchEson(data, patch).data
|
||||
const patchedValueId = patchedData.moved[META].id
|
||||
|
||||
expect(patchedValueId).toEqual(valueId)
|
||||
})
|
||||
|
||||
test('jsonpatch move and replace (keep ids intact)', () => {
|
||||
const json = { a: 2, b: 3 }
|
||||
const patch = [
|
||||
{op: 'move', from: '/a', path: '/b'}
|
||||
]
|
||||
|
||||
const data = jsonToEson(json)
|
||||
const bId = data.b[META].id
|
||||
|
||||
expect(data[META].props).toEqual(['a', 'b'])
|
||||
|
||||
const patchedData = patchEson(data, patch).data
|
||||
|
||||
expect(patchedData.b[META].id).toEqual(bId)
|
||||
expect(patchedData[META].props).toEqual(['b'])
|
||||
})
|
||||
|
||||
test('jsonpatch test (ok)', () => {
|
||||
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 = jsonToEson(json)
|
||||
const result = patchEson(data, patch)
|
||||
const patchedData = result.data
|
||||
const revert = result.revert
|
||||
const patchedJson = esonToJson(patchedData)
|
||||
|
||||
expect(patchedJson).toEqual({
|
||||
arr: [1,2,3],
|
||||
obj: {a : 4},
|
||||
added: 'ok'
|
||||
})
|
||||
expect(revert).toEqual([
|
||||
{op: 'remove', path: '/added'}
|
||||
])
|
||||
|
||||
})
|
||||
|
||||
test('jsonpatch test (fail: path not found)', () => {
|
||||
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 = jsonToEson(json)
|
||||
const result = patchEson(data, patch)
|
||||
const patchedData = result.data
|
||||
const revert = result.revert
|
||||
const patchedJson = esonToJson(patchedData)
|
||||
|
||||
// patch shouldn't be applied
|
||||
expect(patchedJson).toEqual({
|
||||
arr: [1,2,3],
|
||||
obj: {a : 4}
|
||||
})
|
||||
expect(revert).toEqual([])
|
||||
expect(result.error.toString()).toEqual('Error: Test failed, path not found')
|
||||
})
|
||||
|
||||
test('jsonpatch test (fail: value not equal)', () => {
|
||||
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 = jsonToEson(json)
|
||||
const result = patchEson(data, patch)
|
||||
const patchedData = result.data
|
||||
const revert = result.revert
|
||||
const patchedJson = esonToJson(patchedData)
|
||||
|
||||
// patch shouldn't be applied
|
||||
expect(patchedJson).toEqual({
|
||||
arr: [1,2,3],
|
||||
obj: {a : 4}
|
||||
})
|
||||
expect(revert).toEqual([])
|
||||
expect(result.error.toString()).toEqual('Error: Test failed, value differs')
|
||||
})
|
||||
|
||||
// helper function to print JSON in the console
|
||||
function printJSON (json, message = null) {
|
||||
if (message) {
|
||||
console.log(message)
|
||||
}
|
||||
console.log(JSON.stringify(json, null, 2))
|
||||
}
|
||||
|
||||
// helper function to load a JSON file
|
||||
function loadJSON (filename) {
|
||||
return JSON.parse(readFileSync(__dirname + '/' + filename, 'utf-8'))
|
||||
}
|
|
@ -57,7 +57,20 @@
|
|||
*/
|
||||
|
||||
/**
|
||||
* @typedef {'Object' | 'Array' | 'value' | 'string'} ESONType
|
||||
* @typedef {'object' | 'array' | 'value' | 'string'} ESONType
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* op: 'add' | 'remove' | 'replace' | 'copy' | 'move' | 'test',
|
||||
* path: string,
|
||||
* from?: string,
|
||||
* value?: *
|
||||
* }} JSONPatchAction
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {JSONPatchAction[]} JSONPatch
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
import {META} from "../eson"
|
||||
import lodashTransform from "lodash/transform"
|
||||
|
||||
export function assertDeepEqualEson (actual, expected, path = [], ignoreIds = true) {
|
||||
if (expected === undefined) {
|
||||
throw new Error('Argument "expected" is undefined')
|
||||
}
|
||||
|
||||
// console.log('assertDeepEqualEson', actual, expected)
|
||||
|
||||
const actualMeta = ignoreIds ? normalizeMetaIds(actual[META]) : actual[META]
|
||||
const expectedMeta = ignoreIds ? normalizeMetaIds(expected[META]) : expected[META]
|
||||
|
||||
expect(actualMeta).toEqual(expectedMeta) // `Meta data not equal, path=[${path.join(', ')}], actual[META]=${JSON.stringify(actualMeta)}, expected[META]=${JSON.stringify(expectedMeta)}`
|
||||
|
||||
if (actualMeta.type === 'Array') {
|
||||
expect(actual.length).toEqual(expected.length) // 'Actual lengths of arrays should be equal, path=[${path.join(\', \')}]'
|
||||
actual.forEach((item, index) => assertDeepEqualEson(actual[index], expected[index], path.concat(index)), ignoreIds)
|
||||
}
|
||||
else if (actualMeta.type === 'Object') {
|
||||
expect(Object.keys(actual).sort()).toEqual(Object.keys(expected).sort()) // 'Actual properties should be equal, path=[${path.join(\', \')}]'
|
||||
actualMeta.props.forEach(key => assertDeepEqualEson(actual[key], expected[key], path.concat(key)), ignoreIds)
|
||||
}
|
||||
else { // actual[META].type === 'value'
|
||||
expect(Object.keys(actual)).toEqual([]) // 'Value should not contain additional properties, path=[${path.join(\', \')}]'
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMetaIds (meta) {
|
||||
return lodashTransform(meta, (result, value, key) => {
|
||||
if (key === 'id') {
|
||||
result[key] = '[ID]'
|
||||
}
|
||||
else {
|
||||
result[key] = value
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { ID, TYPE, VALUE } from '../eson'
|
||||
import uniq from 'lodash/uniq'
|
||||
import each from 'lodash/each'
|
||||
|
||||
export function createAssertEqualEson(expect) {
|
||||
|
||||
function assertEqualEson (actual, expected, ignoreIds = true) {
|
||||
if (expected === undefined) {
|
||||
throw new Error('Argument "expected" is undefined')
|
||||
}
|
||||
|
||||
// regular deep equal
|
||||
expect(actual).toEqual(expected)
|
||||
|
||||
assertEqualEsonKeys(actual, expected, ignoreIds)
|
||||
}
|
||||
|
||||
function assertEqualEsonKeys (actual, expected, ignoreIds = true) {
|
||||
// collect all symbols
|
||||
const symbols = uniq(Object.getOwnPropertySymbols(actual)
|
||||
.concat(Object.getOwnPropertySymbols(expected)))
|
||||
|
||||
// test whether all meta data is the same
|
||||
symbols
|
||||
.filter(symbol => symbol !== ID || ignoreIds)
|
||||
.forEach(symbol => expect(actual[symbol]).toEqual(expected[symbol]))
|
||||
|
||||
if (actual[TYPE] === 'array') {
|
||||
each(expected, (item, index) => assertEqualEsonKeys(actual[index], expected[index], ignoreIds))
|
||||
}
|
||||
else if (actual[TYPE] === 'object') {
|
||||
each(actual, (value, key) => assertEqualEsonKeys(actual[key], expected[key]), ignoreIds)
|
||||
}
|
||||
else { // actual[TYPE] === 'value'
|
||||
expect(actual[VALUE]).toEqual(expected[VALUE])
|
||||
}
|
||||
}
|
||||
|
||||
return assertEqualEson
|
||||
}
|
|
@ -17,7 +17,7 @@ import { isObjectOrArray } from './typeUtils'
|
|||
* @param {*} value
|
||||
* @return {*}
|
||||
*/
|
||||
export function cloneWithSymbols (value) {
|
||||
export function shallowCloneWithSymbols (value) {
|
||||
if (Array.isArray(value)) {
|
||||
// copy array items
|
||||
let arr = value.slice()
|
||||
|
@ -97,7 +97,7 @@ export function setIn (object, path, value) {
|
|||
return object
|
||||
}
|
||||
else {
|
||||
const updatedObject = cloneWithSymbols(object)
|
||||
const updatedObject = shallowCloneWithSymbols(object)
|
||||
updatedObject[key] = updatedValue
|
||||
return updatedObject
|
||||
}
|
||||
|
@ -129,7 +129,7 @@ export function updateIn (object, path, callback) {
|
|||
return object
|
||||
}
|
||||
else {
|
||||
const updatedObject = cloneWithSymbols(object)
|
||||
const updatedObject = shallowCloneWithSymbols(object)
|
||||
updatedObject[key] = updatedValue
|
||||
return updatedObject
|
||||
}
|
||||
|
@ -159,7 +159,7 @@ export function deleteIn (object, path) {
|
|||
return object
|
||||
}
|
||||
else {
|
||||
const updatedObject = cloneWithSymbols(object)
|
||||
const updatedObject = shallowCloneWithSymbols(object)
|
||||
|
||||
if (Array.isArray(updatedObject)) {
|
||||
updatedObject.splice(key, 1)
|
||||
|
@ -179,7 +179,7 @@ export function deleteIn (object, path) {
|
|||
return object
|
||||
}
|
||||
else {
|
||||
const updatedObject = cloneWithSymbols(object)
|
||||
const updatedObject = shallowCloneWithSymbols(object)
|
||||
updatedObject[key] = updatedValue
|
||||
return updatedObject
|
||||
}
|
||||
|
@ -205,9 +205,89 @@ export function insertAt (object, path, value) {
|
|||
throw new TypeError('Array expected at path ' + JSON.stringify(parentPath))
|
||||
}
|
||||
|
||||
const updatedItems = cloneWithSymbols(items)
|
||||
const updatedItems = shallowCloneWithSymbols(items)
|
||||
updatedItems.splice(index, 0, value)
|
||||
|
||||
return updatedItems
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a JSON object, traverse over the whole object,
|
||||
* and allow replacing Objects/Arrays/values.
|
||||
* Does not iterate over symbols.
|
||||
* @param {JSON} json
|
||||
* @param {function (json: JSON, path: Path) : JSON} callback
|
||||
* @param {Path} [path]
|
||||
* @return {JSON}
|
||||
*/
|
||||
export function transform (json, callback, path = []) {
|
||||
const updated1 = callback(json, path)
|
||||
|
||||
if (Array.isArray(json)) { // array
|
||||
let updated2 = undefined
|
||||
|
||||
for (let i = 0; i < updated1.length; i++) {
|
||||
const before = updated1[i]
|
||||
// we stringify the index here, so the Path only contains strings and can be safely
|
||||
// stringified/parsed to JSONPointer without loosing information.
|
||||
// We do not want to rely on path keys being numeric/string.
|
||||
const after = transform(before, callback, path.concat(i + ''))
|
||||
if (after !== before) {
|
||||
if (!updated2) {
|
||||
updated2 = shallowCloneWithSymbols(updated1)
|
||||
}
|
||||
updated2[i] = after
|
||||
}
|
||||
}
|
||||
|
||||
return updated2 ? updated2 : updated1
|
||||
}
|
||||
else if (json && typeof json === 'object') { // object
|
||||
let updated2 = undefined
|
||||
|
||||
for (let key in updated1) {
|
||||
if (updated1.hasOwnProperty(key)) {
|
||||
const before = updated1[key]
|
||||
const after = transform(before, callback, path.concat(key))
|
||||
if (after !== before) {
|
||||
if (!updated2) {
|
||||
updated2 = shallowCloneWithSymbols(updated1)
|
||||
}
|
||||
updated2[key] = after
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updated2 ? updated2 : updated1
|
||||
}
|
||||
else { // number, string, boolean, null
|
||||
return updated1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a path exists in a JSON object
|
||||
* @param {ESON} json
|
||||
* @param {Path} path
|
||||
* @return {boolean} Returns true if the path exists, else returns false
|
||||
* @private
|
||||
*/
|
||||
export function existsIn (json, path) {
|
||||
if (json === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (path.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (Array.isArray(json)) {
|
||||
// index of an array
|
||||
return existsIn(json[parseInt(path[0], 10)], path.slice(1))
|
||||
}
|
||||
else { // Object
|
||||
// object property. find the index of this property
|
||||
return existsIn(json[path[0]], path.slice(1))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { getIn, setIn, updateIn, deleteIn, insertAt } from './immutabilityHelpers'
|
||||
import { deleteIn, existsIn, getIn, insertAt, setIn, transform, updateIn } from './immutabilityHelpers'
|
||||
|
||||
test('getIn', () => {
|
||||
const obj = {
|
||||
|
@ -274,3 +274,47 @@ test('insertAt', () => {
|
|||
const updated = insertAt(obj, ['a', '2'], 8)
|
||||
expect(updated).toEqual({a: [1,2,8,3]})
|
||||
})
|
||||
|
||||
test('transform (no change)', () => {
|
||||
const eson = {a: [1,2,3], b: {c: 4}}
|
||||
const updated = transform(eson, (value, path) => value)
|
||||
expect(updated).toBe(eson)
|
||||
})
|
||||
|
||||
test('transform (change based on value)', () => {
|
||||
const eson = {a: [1,2,3], b: {c: 4}}
|
||||
|
||||
const updated = transform(eson,
|
||||
(value, path) => value === 2 ? 20 : value)
|
||||
const expected = {a: [1,20,3], b: {c: 4}}
|
||||
|
||||
expect(updated).toEqual(expected)
|
||||
expect(updated.b).toBe(eson.b) // should not have replaced b
|
||||
})
|
||||
|
||||
test('transform (change based on path)', () => {
|
||||
const eson = {a: [1,2,3], b: {c: 4}}
|
||||
|
||||
const updated = transform(eson,
|
||||
(value, path) => path.join('.') === 'a.1' ? 20 : value)
|
||||
const expected = {a: [1,20,3], b: {c: 4}}
|
||||
|
||||
expect(updated).toEqual(expected)
|
||||
expect(updated.b).toBe(eson.b) // should not have replaced b
|
||||
})
|
||||
|
||||
test('existsIn', () => {
|
||||
const json = {
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
"str": "hello world",
|
||||
"nill": null,
|
||||
"bool": false
|
||||
}
|
||||
|
||||
expect(existsIn(json, ['obj', 'arr', 2, 'first'])).toEqual(true)
|
||||
expect(existsIn(json, ['obj', 'foo'])).toEqual(false)
|
||||
expect(existsIn(json, ['obj', 'foo', 'bar'])).toEqual(false)
|
||||
expect(existsIn(json, [])).toEqual(true)
|
||||
})
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
const map = new WeakMap()
|
||||
let counter = 0;
|
||||
|
||||
/**
|
||||
* Generate a unique key for an object or array (by object reference)
|
||||
* @param {Object | Array} item
|
||||
* @returns {string | null}
|
||||
* Returns the generated key.
|
||||
* If the item is no Object or Array, null is returned
|
||||
*/
|
||||
export function weakKey(item) {
|
||||
if (!item || (!Array.isArray(item) && typeof item !== 'object')) {
|
||||
return null
|
||||
}
|
||||
|
||||
let k = map.get(item)
|
||||
|
||||
if (!k) {
|
||||
k = 'key-' + counter
|
||||
counter++
|
||||
map.set(item, k)
|
||||
}
|
||||
|
||||
return k
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { weakKey } from './reactUtils'
|
||||
|
||||
test('weakKey should keep the key the same for objects and arrays', () => {
|
||||
const a = {x: 1}
|
||||
const b = {x: 1}
|
||||
const c = b
|
||||
const d = [1, 2, 3]
|
||||
const e = [1, 2, 3]
|
||||
|
||||
expect(weakKey(a)).toEqual(weakKey(a))
|
||||
expect(weakKey(b)).toEqual(weakKey(b))
|
||||
expect(weakKey(a)).not.toEqual(weakKey(b))
|
||||
expect(weakKey(b)).toEqual(weakKey(c))
|
||||
expect(weakKey(d)).toEqual(weakKey(d))
|
||||
expect(weakKey(e)).toEqual(weakKey(e))
|
||||
expect(weakKey(d)).not.toEqual(weakKey(e))
|
||||
expect(weakKey(d)).not.toEqual(weakKey(e))
|
||||
})
|
||||
|
||||
test('weakKey should return null for non-object and non-array items', () => {
|
||||
expect(weakKey('foo')).toBeNull()
|
||||
expect(weakKey(123)).toBeNull()
|
||||
expect(weakKey(null)).toBeNull()
|
||||
expect(weakKey(undefined)).toBeNull()
|
||||
expect(weakKey(true)).toBeNull()
|
||||
})
|
|
@ -98,13 +98,13 @@ export function escapeJSON (text) {
|
|||
* Find a unique name. Suffix the name with ' (copy)', '(copy 2)', etc
|
||||
* until a unique name is found
|
||||
* @param {string} name
|
||||
* @param {Array.<string>} invalidNames
|
||||
* @param {Object} existingProps Object with existing props
|
||||
*/
|
||||
export function findUniqueName (name, invalidNames) {
|
||||
export function findUniqueName (name, existingProps) {
|
||||
let validName = name
|
||||
let i = 1
|
||||
|
||||
while (invalidNames.includes(validName)) {
|
||||
while (validName in existingProps) {
|
||||
const copy = 'copy' + (i > 1 ? (' ' + i) : '')
|
||||
validName = `${name} (${copy})`
|
||||
i++
|
||||
|
|
|
@ -18,8 +18,8 @@ test('unescapeHTML', () => {
|
|||
})
|
||||
|
||||
test('findUniqueName', () => {
|
||||
expect(findUniqueName('other', ['a', 'b', 'c'])).toEqual('other')
|
||||
expect(findUniqueName('b', ['a', 'b', 'c'])).toEqual('b (copy)')
|
||||
expect(findUniqueName('b', ['a', 'b', 'c', 'b (copy)'])).toEqual('b (copy 2)')
|
||||
expect(findUniqueName('b', ['a', 'b', 'c', 'b (copy)', 'b (copy 2)'])).toEqual('b (copy 3)')
|
||||
expect(findUniqueName('other', {'a': true, 'b': true, 'c': true})).toEqual('other')
|
||||
expect(findUniqueName('b', {'a': true, 'b': true, 'c': true})).toEqual('b (copy)')
|
||||
expect(findUniqueName('b', {'a': true, 'b': true, 'c': true, 'b (copy)': true})).toEqual('b (copy 2)')
|
||||
expect(findUniqueName('b', {'a': true, 'b': true, 'c': true, 'b (copy)': true, 'b (copy 2)': true})).toEqual('b (copy 3)')
|
||||
})
|
||||
|
|
|
@ -62,10 +62,10 @@ export function valueType(value) {
|
|||
return 'regexp'
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return 'Array'
|
||||
return 'array'
|
||||
}
|
||||
|
||||
return 'Object'
|
||||
return 'object'
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue