Refactored ESON to use Symbols, refactored patchEson

This commit is contained in:
jos 2017-12-15 19:57:21 +01:00
parent 53b20e2f59
commit 156f330e4e
8 changed files with 347 additions and 351 deletions

View File

@ -8,7 +8,7 @@ import FloatingMenu from './menu/FloatingMenu'
import { escapeHTML, unescapeHTML } from '../utils/stringUtils'
import { getInnerText, insideRect, findParentWithAttribute } from '../utils/domUtils'
import { stringConvert, valueType, isUrl } from '../utils/typeUtils'
import { compileJSONPointer, mapEsonArray, SELECTED, SELECTED_END, SELECTED_AFTER, SELECTED_BEFORE } from '../eson'
import { compileJSONPointer, META, SELECTED, SELECTED_END, SELECTED_AFTER, SELECTED_BEFORE } from '../eson'
import type { ESONObjectProperty, ESON, SearchResultStatus, Path } from '../types'
@ -46,10 +46,10 @@ export default class JSONNode extends PureComponent {
}
render () {
if (this.props.eson._meta.type === 'Object') {
if (this.props.eson[META].type === 'Object') {
return this.renderJSONObject(this.props)
}
else if (this.props.eson._meta.type === 'Array') {
else if (this.props.eson[META].type === 'Array') {
return this.renderJSONArray(this.props)
}
else { // no Object or Array
@ -58,7 +58,7 @@ export default class JSONNode extends PureComponent {
}
renderJSONObject ({prop, index, eson, options, events}) {
const keys = eson._meta.keys
const keys = eson[META].keys
const node = h('div', {
key: 'node',
onKeyDown: this.handleKeyDown,
@ -70,14 +70,14 @@ export default class JSONNode extends PureComponent {
this.renderProperty(prop, index, eson, options),
this.renderReadonly(`{${keys.length}}`, `Array containing ${keys.length} items`),
// this.renderFloatingMenuButton(),
this.renderError(eson._meta.error)
this.renderError(eson[META].error)
])
let childs
if (eson._meta.expanded) {
if (eson[META].expanded) {
if (keys.length > 0) {
const props = keys.map(key => h(this.constructor, {
key: eson[key]._meta.id,
key: eson[key][META].id,
// parent: this,
prop: key,
eson: eson[key],
@ -94,7 +94,7 @@ export default class JSONNode extends PureComponent {
}
}
const floatingMenu = (eson._meta.selected === SELECTED_END)
const floatingMenu = (eson[META].selected === SELECTED_END)
? this.renderFloatingMenu([
{type: 'sort'},
{type: 'duplicate'},
@ -108,8 +108,8 @@ export default class JSONNode extends PureComponent {
const insertArea = this.renderInsertBeforeArea()
return h('div', {
'data-path': compileJSONPointer(this.props.eson._meta.path),
className: this.getContainerClassName(eson._meta.selected, this.state.hover),
'data-path': compileJSONPointer(this.props.eson[META].path),
className: this.getContainerClassName(eson[META].selected, this.state.hover),
onMouseOver: this.handleMouseOver,
onMouseLeave: this.handleMouseLeave
}, [node, floatingMenu, insertArea, childs])
@ -125,16 +125,16 @@ export default class JSONNode extends PureComponent {
// this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu),
// this.renderActionMenuButton(),
this.renderProperty(prop, index, eson, options),
this.renderReadonly(`[${eson._meta.length}]`, `Array containing ${eson._meta.length} items`),
this.renderReadonly(`[${eson.length}]`, `Array containing ${eson.length} items`),
// this.renderFloatingMenuButton(),
this.renderError(eson._meta.error)
this.renderError(eson[META].error)
])
let childs
if (eson._meta.expanded) {
if (eson._meta.length > 0) {
const items = mapEsonArray(eson, (item, index) => h(this.constructor, {
key : item._meta.id,
if (eson[META].expanded) {
if (eson.length > 0) {
const items = eson.map((item, index) => h(this.constructor, {
key : item[META].id,
// parent: this,
index,
eson: item,
@ -151,7 +151,7 @@ export default class JSONNode extends PureComponent {
}
}
const floatingMenu = (eson._meta.selected === SELECTED_END)
const floatingMenu = (eson[META].selected === SELECTED_END)
? this.renderFloatingMenu([
{type: 'sort'},
{type: 'duplicate'},
@ -165,8 +165,8 @@ export default class JSONNode extends PureComponent {
const insertArea = this.renderInsertBeforeArea()
return h('div', {
'data-path': compileJSONPointer(this.props.eson._meta.path),
className: this.getContainerClassName(eson._meta.selected, this.state.hover),
'data-path': compileJSONPointer(this.props.eson[META].path),
className: this.getContainerClassName(eson[META].selected, this.state.hover),
onMouseOver: this.handleMouseOver,
onMouseLeave: this.handleMouseLeave
}, [node, floatingMenu, insertArea, childs])
@ -183,12 +183,12 @@ export default class JSONNode extends PureComponent {
// this.renderActionMenuButton(),
this.renderProperty(prop, index, eson, options),
this.renderSeparator(),
this.renderValue(eson._meta.value, eson._meta.searchValue, options),
this.renderValue(eson[META].value, eson[META].searchValue, options),
// this.renderFloatingMenuButton(),
this.renderError(eson._meta.error)
this.renderError(eson[META].error)
])
const floatingMenu = (eson._meta.selected === SELECTED_END)
const floatingMenu = (eson[META].selected === SELECTED_END)
? this.renderFloatingMenu([
// {text: 'String', onClick: this.props.events.onChangeType, type: 'checkbox', checked: false},
{type: 'duplicate'},
@ -202,15 +202,15 @@ export default class JSONNode extends PureComponent {
const insertArea = this.renderInsertBeforeArea()
return h('div', {
'data-path': compileJSONPointer(this.props.eson._meta.path),
className: this.getContainerClassName(eson._meta.selected, this.state.hover),
'data-path': compileJSONPointer(this.props.eson[META].path),
className: this.getContainerClassName(eson[META].selected, this.state.hover),
onMouseOver: this.handleMouseOver,
onMouseLeave: this.handleMouseLeave
}, [node, floatingMenu, insertArea])
}
renderInsertBeforeArea () {
const floatingMenu = (this.props.eson._meta.selected === SELECTED_BEFORE)
const floatingMenu = (this.props.eson[META].selected === SELECTED_BEFORE)
? this.renderFloatingMenu([
{type: 'insertStructure'},
{type: 'insertValue'},
@ -234,7 +234,7 @@ export default class JSONNode extends PureComponent {
*/
renderAppend (text) {
return h('div', {
'data-path': compileJSONPointer(this.props.eson._meta.path) + '/-',
'data-path': compileJSONPointer(this.props.eson[META].path) + '/-',
className: 'jsoneditor-node',
onKeyDown: this.handleKeyDownAppend
}, [
@ -269,10 +269,10 @@ export default class JSONNode extends PureComponent {
}, rootName)
}
const editable = !isIndex && (!options.isPropertyEditable || options.isPropertyEditable(this.props.eson._meta.path))
const editable = !isIndex && (!options.isPropertyEditable || options.isPropertyEditable(this.props.eson[META].path))
const emptyClassName = (prop != null && prop.length === 0) ? ' jsoneditor-empty' : ''
const searchClassName = prop != null ? JSONNode.getSearchResultClass(eson._meta.searchProperty) : ''
const searchClassName = prop != null ? JSONNode.getSearchResultClass(eson[META].searchProperty) : ''
const escapedPropName = prop != null ? escapeHTML(prop, options.escapeUnicode) : null
if (editable) {
@ -303,7 +303,7 @@ export default class JSONNode extends PureComponent {
const itsAnUrl = isUrl(value)
const isEmpty = escapedValue.length === 0
const editable = !options.isValueEditable || options.isValueEditable(this.props.eson._meta.path)
const editable = !options.isValueEditable || options.isValueEditable(this.props.eson[META].path)
if (editable) {
return h('div', {
key: 'value',
@ -395,7 +395,7 @@ export default class JSONNode extends PureComponent {
}
target.className = JSONNode.getValueClass(type, itsAnUrl, isEmpty) +
JSONNode.getSearchResultClass(this.props.eson._meta.searchValue)
JSONNode.getSearchResultClass(this.props.eson[META].searchValue)
target.title = itsAnUrl ? JSONNode.URL_TITLE : ''
// remove all classNames from childs (needed for IE and Edge)
@ -448,7 +448,7 @@ export default class JSONNode extends PureComponent {
}
renderExpandButton () {
const className = `jsoneditor-button jsoneditor-${this.props.eson._meta.expanded ? 'expanded' : 'collapsed'}`
const className = `jsoneditor-button jsoneditor-${this.props.eson[META].expanded ? 'expanded' : 'collapsed'}`
return h('div', {key: 'expand', className: 'jsoneditor-button-container'},
h('button', {
@ -469,9 +469,9 @@ export default class JSONNode extends PureComponent {
return h(ActionMenu, {
key: 'menu',
path: this.props.eson._meta.path,
path: this.props.eson[META].path,
events: this.props.events,
type: this.props.eson._meta.type, // TODO: fix type
type: this.props.eson[META].type, // TODO: fix type
menuType,
open: true,
@ -513,7 +513,7 @@ export default class JSONNode extends PureComponent {
renderFloatingMenu (items) {
return h(FloatingMenu, {
key: 'floating-menu',
path: this.props.eson._meta.path,
path: this.props.eson[META].path,
events: this.props.events,
items
})
@ -603,7 +603,7 @@ export default class JSONNode extends PureComponent {
/** @private */
handleChangeProperty = (event) => {
const parentPath = initial(this.props.eson._meta.path)
const parentPath = initial(this.props.eson[META].path)
const oldProp = this.props.prop.name
const newProp = unescapeHTML(getInnerText(event.target))
@ -616,8 +616,8 @@ export default class JSONNode extends PureComponent {
handleChangeValue = (event) => {
const value = this.getValueFromEvent(event)
if (value !== this.props.eson._meta.value) {
this.props.events.onChangeValue(this.props.eson._meta.path, value)
if (value !== this.props.eson[META].value) {
this.props.events.onChangeValue(this.props.eson[META].path, value)
}
}
@ -634,24 +634,24 @@ export default class JSONNode extends PureComponent {
if (keyBinding === 'duplicate') {
event.preventDefault()
this.props.events.onDuplicate(this.props.eson._meta.path)
this.props.events.onDuplicate(this.props.eson[META].path)
}
if (keyBinding === 'insert') {
event.preventDefault()
this.props.events.onInsert(this.props.eson._meta.path, 'value')
this.props.events.onInsert(this.props.eson[META].path, 'value')
}
if (keyBinding === 'remove') {
event.preventDefault()
this.props.events.onRemove(this.props.eson._meta.path)
this.props.events.onRemove(this.props.eson[META].path)
}
if (keyBinding === 'expand') {
event.preventDefault()
const recurse = false
const expanded = !this.props.eson._meta.expanded
this.props.events.onExpand(this.props.eson._meta.path, expanded, recurse)
const expanded = !this.props.eson[META].expanded
this.props.events.onExpand(this.props.eson[META].path, expanded, recurse)
}
if (keyBinding === 'actionMenu') {
@ -666,7 +666,7 @@ export default class JSONNode extends PureComponent {
if (keyBinding === 'insert') {
event.preventDefault()
this.props.events.onAppend(this.props.eson._meta.path, 'value')
this.props.events.onAppend(this.props.eson[META].path, 'value')
}
if (keyBinding === 'actionMenu') {
@ -687,8 +687,8 @@ export default class JSONNode extends PureComponent {
/** @private */
handleExpand = (event) => {
const recurse = event.ctrlKey
const path = this.props.eson._meta.path
const expanded = !this.props.eson._meta.expanded
const path = this.props.eson[META].path
const expanded = !this.props.eson[META].expanded
this.props.events.onExpand(path, expanded, recurse)
}
@ -717,7 +717,7 @@ export default class JSONNode extends PureComponent {
*/
getValueFromEvent (event) {
const stringValue = unescapeHTML(getInnerText(event.target))
return this.props.eson._meta.type === 'string'
return this.props.eson[META].type === 'string'
? stringValue
: stringConvert(stringValue)
}

View File

@ -12,6 +12,7 @@ import { getIn, setIn, updateIn } from '../utils/immutabilityHelpers'
import { parseJSON } from '../utils/jsonUtils'
import { enrichSchemaError } from '../utils/schemaUtils'
import {
META,
jsonToEson, esonToJson, pathExists,
expand, expandOne, expandPath, applyErrors,
search, nextSearchResult, previousSearchResult,
@ -154,7 +155,7 @@ export default class TreeMode extends Component {
// Apply json
if (nextProps.json !== currentProps.json) {
// FIXME: merge _meta from existing eson
// FIXME: merge meta data from existing eson
this.setState({
json: nextProps.json,
eson: jsonToEson(nextProps.json) // FIXME: how to handle expand?
@ -222,7 +223,7 @@ export default class TreeMode extends Component {
onMouseDown: this.handleTouchStart,
onTouchStart: this.handleTouchStart,
className: 'jsoneditor-list jsoneditor-root' +
(eson._meta.selected ? ' jsoneditor-selected' : '')},
(eson[META].selected ? ' jsoneditor-selected' : '')},
h(Node, {
eson,
events: state.events,
@ -982,7 +983,7 @@ export default class TreeMode extends Component {
* @return {boolean} Returns true when expanded, false otherwise
*/
isExpanded (path) {
return getIn(this.state.eson, path)._meta.expanded
return getIn(this.state.eson, path)[META].expanded
}
/**

View File

@ -47,7 +47,7 @@ export function jsonToEson (json, path = []) {
return eson
}
else if (Array.isArray(json)) {
let eson = json.map((value, index) => jsonToEson(value, path.concat(index)))
let eson = json.map((value, index) => jsonToEson(value, path.concat(String(index))))
eson[META] = { id, path, type: 'Array' }
return eson
}
@ -58,21 +58,6 @@ export function jsonToEson (json, path = []) {
}
}
/**
* Map over an eson array
* @param {ESONArray} esonArray
* @param {function (value, index, array)} callback
* @return {Array}
*/
export function mapEsonArray (esonArray, callback) {
const length = esonArray[META].length
let result = []
for (let i = 0; i < length; i++) {
result[i] = callback(esonArray[i], i, esonArray)
}
return result
}
/**
* Expand function which will expand all nodes
* @param {Path} path
@ -130,21 +115,21 @@ export function jsonToEsonOld (json, expand = expandAll, path: JSONPath = [], ty
* @return {Object | Array | string | number | boolean | null} json
*/
export function esonToJson (eson: ESON) {
switch (eson.type) {
switch (eson[META].type) {
case 'Array':
return eson.items.map(item => esonToJson(item.value))
return eson.map(item => esonToJson(item))
case 'Object':
const object = {}
eson.props.forEach(prop => {
object[prop.name] = esonToJson(prop.value)
eson[META].keys.forEach(prop => {
object[prop] = esonToJson(eson[prop])
})
return object
default: // type 'string' or 'value'
return eson.value
return eson[META].value
}
}
@ -236,13 +221,6 @@ export function setInEson (eson: ESON, jsonPath: JSONPath, value: JSONType) {
return setIn(eson, toEsonPath(eson, jsonPath), value)
}
/**
* Set the value of a nested property in an ESON object using a JSON path
*/
export function updateInEson (eson: ESON, jsonPath: JSONPath, callback) {
return updateIn(eson, toEsonPath(eson, jsonPath), callback)
}
/**
* Set the value of a nested property in an ESON object using a JSON path
*/
@ -279,7 +257,7 @@ export function transform (eson, callback, path = []) {
let changed = false
let updatedArr = []
for (let i = 0; i < updated.length; i++) {
updatedArr[i] = transform(updated[i], callback, path.concat(i))
updatedArr[i] = transform(updated[i], callback, path.concat(String(i)))
changed = changed || (updatedArr[i] !== updated[i])
}
updatedArr[META] = updated[META]
@ -290,6 +268,26 @@ export function transform (eson, callback, path = []) {
}
}
/**
* 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)) {
// TODO: extend setIn to support symbols
let updatedValue = cloneWithSymbols(value)
updatedValue[META] = setIn(value[META], ['path'], path)
return updatedValue
}
else {
return value
}
}, path)
}
/**
* Expand or collapse all items matching a filter callback
* @param {ESON} eson
@ -550,7 +548,7 @@ export function applySelection (eson, selection) {
const maxIndex = Math.max(startIndex, endIndex) + 1 // include max index itself
const selectedIndices = range(minIndex, maxIndex)
selectedPaths = selectedIndices.map(index => rootPath.concat(index))
selectedPaths = selectedIndices.map(index => rootPath.concat(String(index)))
let updatedArr = root.slice()
updatedArr = cloneWithSymbols(root)
@ -752,11 +750,11 @@ function recurseTraverse (value: ESON, path: JSONPath, root: ESON, callback: Rec
/**
* Test whether a path exists in the eson object
* @param {ESON} eson
* @param {JSONPath} path
* @param {Path} path
* @return {boolean} Returns true if the path exists, else returns false
* @private
*/
export function pathExists (eson: ESON, path: JSONPath) {
export function pathExists (eson, path) {
if (eson === undefined) {
return false
}
@ -765,19 +763,13 @@ export function pathExists (eson: ESON, path: JSONPath) {
return true
}
if (eson.type === 'Array') {
if (Array.isArray(eson)) {
// index of an array
const index = path[0]
const item = eson.items[parseInt(index)]
return pathExists(item && item.value, path.slice(1))
return pathExists(eson[parseInt(path[0])], path.slice(1))
}
else { // eson.type === 'Object'
// object property. find the index of this property
const index = findPropertyIndex(eson, path[0])
const prop = eson.props[index]
return pathExists(prop && prop.value, path.slice(1))
return pathExists(eson[path[0]], path.slice(1))
}
}
@ -790,15 +782,12 @@ export function pathExists (eson: ESON, path: JSONPath) {
*/
export function resolvePathIndex (eson, path) {
if (path[path.length - 1] === '-') {
const parentPath = path.slice(0, path.length - 1)
const parent = getInEson(eson, parentPath)
const parentPath = initial(path)
const parent = getIn(eson, parentPath)
if (parent.type === 'Array') {
const index = parent.items.length
const resolvedPath = path.slice(0)
resolvedPath[resolvedPath.length - 1] = index
return resolvedPath
if (Array.isArray(parent)) {
const index = parent.length
return parentPath.concat(String(index))
}
}
@ -812,14 +801,9 @@ export function resolvePathIndex (eson, path) {
* @return {string | null} Returns the name of the next property,
* or null if there is none
*/
export function findNextProp (parent: ESONObject, prop: string) : string | null {
const index = findPropertyIndex(parent, prop)
if (index === -1) {
return null
}
const next = parent.props[index + 1]
return next && next.name || null
export function findNextProp (parent, prop) {
const index = parent[META].keys.indexOf(prop)
return parent[META].keys[index + 1] || null
}
/**

View File

@ -3,12 +3,15 @@ import initial from 'lodash/initial'
import last from 'lodash/last'
import type { ESON, Path, ESONPatch } from './types'
import { setIn, updateIn, getIn, insertAt } from './utils/immutabilityHelpers'
import {
jsonToEson, jsonToEsonOld, esonToJson, toEsonPath,
getInEson, setInEson, deleteInEson,
setIn, updateIn, getIn, deleteIn, insertAt,
cloneWithSymbols
} from './utils/immutabilityHelpers'
import {
META,
jsonToEson, esonToJson, updatePaths,
parseJSONPointer, compileJSONPointer,
expandAll, pathExists, resolvePathIndex, findPropertyIndex, findNextProp, createId
expandAll, pathExists, resolvePathIndex, createId
} from './eson'
/**
@ -19,7 +22,7 @@ import {
* what nodes must be expanded
* @return {{data: ESON, revert: Object[], error: Error | null}}
*/
export function patchEson (eson: ESON, patch: ESONPatch, expand = expandAll) {
export function patchEson (eson, patch, expand = expandAll) {
let updatedEson = eson
let revert = []
@ -32,7 +35,9 @@ export function patchEson (eson: ESON, patch: ESONPatch, expand = expandAll) {
switch (action.op) {
case 'add': {
const path = parseJSONPointer(action.path)
const newValue = jsonToEsonOld(action.value, expand, path, options && options.type)
const newValue = jsonToEson(action.value, path)
// FIXME: apply expanded state
// FIXME: apply options.type
const result = add(updatedEson, action.path, newValue, options)
updatedEson = result.data
revert = result.revert.concat(revert)
@ -50,7 +55,9 @@ export function patchEson (eson: ESON, patch: ESONPatch, expand = expandAll) {
case 'replace': {
const path = parseJSONPointer(action.path)
const newValue = jsonToEsonOld(action.value, expand, path, options && options.type)
const newValue = jsonToEson(action.value, path)
// FIXME: apply expanded state
// FIXME: apply options.type
const result = replace(updatedEson, path, newValue)
updatedEson = result.data
revert = result.revert.concat(revert)
@ -125,17 +132,17 @@ export function patchEson (eson: ESON, patch: ESONPatch, expand = expandAll) {
* @param {ESON} value
* @return {{data: ESON, revert: ESONPatch}}
*/
export function replace (data: ESON, path: Path, value: ESON) {
const oldValue = getInEson(data, path)
export function replace (data, path, value) {
const oldValue = getIn(data, path)
return {
data: setInEson(data, path, value),
data: setIn(data, path, value),
revert: [{
op: 'replace',
path: compileJSONPointer(path),
value: esonToJson(oldValue),
jsoneditor: {
type: oldValue.type
type: oldValue[META].type
}
}]
}
@ -148,40 +155,45 @@ export function replace (data: ESON, path: Path, value: ESON) {
* @return {{data: ESON, revert: ESONPatch}}
*/
// FIXME: path should be a path instead of a string? (all functions in patchEson)
export function remove (data: ESON, path: string) {
export function remove (data, path) {
// console.log('remove', path)
const pathArray = parseJSONPointer(path)
const parentPath = initial(pathArray)
const parent = getInEson(data, parentPath)
const dataValue = getInEson(data, pathArray)
const parent = getIn(data, parentPath)
const dataValue = getIn(data, pathArray)
const value = esonToJson(dataValue)
if (parent.type === 'Array') {
if (parent[META].type === 'Array') {
return {
data: deleteInEson(data, pathArray),
data: updatePaths(deleteIn(data, pathArray)),
revert: [{
op: 'add',
path,
value,
jsoneditor: {
type: dataValue.type
type: dataValue[META].type
}
}]
}
}
else { // object.type === 'Object'
const prop = last(pathArray)
const index = parent[META].keys.indexOf(prop)
const nextProp = parent[META].keys[index + 1] || null
let updatedParent = deleteIn(parent, [prop])
updatedParent[META] = deleteIn(parent[META], ['keys', index], parent[META].keys)
return {
data: deleteInEson(data, pathArray),
data: setIn(data, parentPath, updatePaths(updatedParent, parentPath)),
revert: [{
op: 'add',
path,
value,
jsoneditor: {
type: dataValue.type,
before: findNextProp(parent, prop)
type: dataValue[META].type,
before: nextProp
}
}]
}
@ -193,47 +205,55 @@ export function remove (data: ESON, path: string) {
* @param {string} path
* @param {ESON} value
* @param {{before?: string}} [options]
* @param {number} [id] Optional id for the new item
* @return {{data: ESON, revert: ESONPatch}}
* @private
*/
export function add (data: ESON, path: string, value: ESON, options, id = createId()) {
// TODO: refactor path to an array with strings
export function add (data, path, value, options) {
// FIXME: apply id to new created values
const pathArray = parseJSONPointer(path)
const parentPath = pathArray.slice(0, pathArray.length - 1)
const esonPath = toEsonPath(data, parentPath)
const parent = getIn(data, esonPath)
const parentPath = initial(pathArray)
const parent = getIn(data, parentPath)
const resolvedPath = resolvePathIndex(data, pathArray)
const prop = resolvedPath[resolvedPath.length - 1]
const prop = last(resolvedPath)
let updatedEson
if (parent.type === 'Array') {
const newItem = {
id, // TODO: create a unique id within current id's instead of using a global, ever incrementing id
value
}
updatedEson = insertAt(data, esonPath.concat('items', prop), newItem)
if (parent[META].type === 'Array') {
updatedEson = updatePaths(insertAt(data, resolvedPath, value))
}
else { // parent.type === 'Object'
updatedEson = updateIn(data, esonPath, (object) => {
const existingIndex = findPropertyIndex(object, prop)
updatedEson = updateIn(data, parentPath, (parent) => {
const oldValue = getIn(data, pathArray)
const props = parent[META].keys
const existingIndex = props.indexOf(prop)
if (existingIndex !== -1) {
// replace existing item
return setIn(object, ['props', existingIndex, 'value'], value)
// update path
// FIXME: also update value's id
let newValue = updatePaths(cloneWithSymbols(value), pathArray)
newValue[META] = setIn(newValue[META], ['id'], oldValue[META].id)
// console.log('copied id from existing value' + oldValue[META].id)
// TODO: update paths of existing value
return setIn(parent, [prop], newValue)
}
else {
// insert new item
const newProp = { id, name: prop, value }
const index = (options && typeof options.before === 'string')
? findPropertyIndex(object, options.before) // insert before
: object.props.length // append
? props.indexOf(options.before) // insert before
: props.length // append
return insertAt(object, ['props', index], newProp)
let updatedKeys = props.slice()
updatedKeys.splice(index, 0, prop)
const updatedParent = setIn(parent, [prop], updatePaths(value, parentPath.concat(prop)))
return setIn(updatedParent, [META, 'keys'], updatedKeys)
}
})
}
if (parent.type === 'Object' && pathExists(data, resolvedPath)) {
const oldValue = getInEson(data, resolvedPath)
if (parent[META].type === 'Object' && pathExists(data, resolvedPath)) {
const oldValue = getIn(data, resolvedPath)
return {
data: updatedEson,
@ -241,7 +261,7 @@ export function add (data: ESON, path: string, value: ESON, options, id = create
op: 'replace',
path: compileJSONPointer(resolvedPath),
value: esonToJson(oldValue),
jsoneditor: { type: oldValue.type }
jsoneditor: { type: oldValue[META].type }
}]
}
}
@ -265,10 +285,14 @@ export function add (data: ESON, path: string, value: ESON, options, id = create
* @return {{data: ESON, revert: ESONPatch}}
* @private
*/
export function copy (data: ESON, path: string, from: string, options) {
const value = getInEson(data, parseJSONPointer(from))
export function copy (data, path, from, options) {
const value = getIn(data, parseJSONPointer(from))
return add(data, path, value, options)
// create new id for the copied item
let updatedValue = cloneWithSymbols(value)
updatedValue[META] = setIn(updatedValue[META], ['id'], createId())
return add(data, path, updatedValue, options)
}
/**
@ -280,22 +304,19 @@ export function copy (data: ESON, path: string, from: string, options) {
* @return {{data: ESON, revert: ESONPatch}}
* @private
*/
export function move (data: ESON, path: string, from: string, options) {
export function move (data, path, from, options) {
const fromArray = parseJSONPointer(from)
const prop = getIn(data, initial(toEsonPath(data, fromArray)))
const dataValue = prop.value
const id = prop.id // we want to use the existing id in case the move is a renaming a property
// FIXME: only reuse the existing id when move is renaming a property in the same object
const dataValue = getIn(data, fromArray)
const parentPathFrom = initial(fromArray)
const parent = getInEson(data, parentPathFrom)
const parent = getIn(data, parentPathFrom)
const result1 = remove(data, from)
const result2 = add(result1.data, path, dataValue, options, id)
const result2 = add(result1.data, path, dataValue, options)
// FIXME: passing id as parameter is ugly, make that redundant (use replace instead of remove/add? (that would give less predictive output :( ))
const before = result1.revert[0].jsoneditor.before
const beforeNeeded = (parent.type === 'Object' && before)
const beforeNeeded = (parent[META].type === 'Object' && before)
if (result2.revert[0].op === 'replace') {
const value = result2.revert[0].value
@ -314,7 +335,7 @@ export function move (data: ESON, path: string, from: string, options) {
return {
data: result2.data,
revert: beforeNeeded
? [{ op: 'move', from: path, path: from, jsoneditor: {before} }]
? [{ op: 'move', from: path, path: from, jsoneditor: { before } }]
: [{ op: 'move', from: path, path: from }]
}
}
@ -328,7 +349,7 @@ export function move (data: ESON, path: string, from: string, options) {
* @param {*} value
* @return {null | Error} Returns an error when the tests, returns null otherwise
*/
export function test (data: ESON, path: string, value: any) {
export function test (data, path, value) {
if (value === undefined) {
return new Error('Test failed, no value provided')
}
@ -338,7 +359,7 @@ export function test (data: ESON, path: string, value: any) {
return new Error('Test failed, path not found')
}
const actualValue = getInEson(data, pathArray)
const actualValue = getIn(data, pathArray)
if (!isEqual(esonToJson(actualValue), value)) {
return new Error('Test failed, value differs')
}

View File

@ -125,6 +125,7 @@ export function updateIn (object, path, callback) {
const key = path[0]
const updatedValue = updateIn(object[key], path.slice(1), callback)
// TODO: create a function applyProp(...) which does the following if/else construct
if (object[key] === updatedValue) {
// return original object unchanged when the new value is identical to the old one
return object
@ -206,8 +207,7 @@ export function insertAt (object, path, value) {
throw new TypeError('Array expected at path ' + JSON.stringify(parentPath))
}
const updatedItems = items.slice(0)
const updatedItems = cloneWithSymbols(items)
updatedItems.splice(index, 0, value)
return updatedItems

View File

@ -12,35 +12,13 @@ import {
SELECTED, SELECTED_END
} from '../src/eson'
import 'console.table'
import lodashTransform from 'lodash/transform'
import repeat from 'lodash/repeat'
import { assertDeepEqualEson } from './utils/assertDeepEqualEson'
const JSON1 = loadJSON('./resources/json1.json')
const ESON1 = loadJSON('./resources/eson1.json')
const ESON2 = loadJSON('./resources/eson2.json')
test('toEsonPath', t => {
const jsonPath = ['obj', 'arr', '2', 'last']
const esonPath = [
'props', '0', 'value',
'props', '0', 'value',
'items', '2', 'value',
'props', '1', 'value'
]
t.deepEqual(toEsonPath(ESON1, jsonPath), esonPath)
})
test('toJsonPath', t => {
const jsonPath = ['obj', 'arr', '2', 'last']
const esonPath = [
'props', '0', 'value',
'props', '0', 'value',
'items', '2', 'value',
'props', '1', 'value'
]
t.deepEqual(toJsonPath(ESON1, esonPath), jsonPath)
})
test('jsonToEson', t => {
assertDeepEqualEson(t, jsonToEson(1), {[META]: {id: '[ID]', path: [], type: 'value', value: 1}})
assertDeepEqualEson(t, jsonToEson("foo"), {[META]: {id: '[ID]', path: [], type: 'value', value: "foo"}})
@ -54,15 +32,24 @@ test('jsonToEson', t => {
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}}
{[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(t, actual, expected)
})
test('esonToJson', t => {
t.deepEqual(esonToJson(ESON1), JSON1)
const json = {
"obj": {
"arr": [1,2, {"first":3,"last":4}]
},
"str": "hello world",
"nill": null,
"bool": false
}
const eson = jsonToEson(json)
t.deepEqual(esonToJson(eson), json)
})
test('expand a single path', t => {
@ -186,10 +173,19 @@ test('transform (change based on path)', t => {
})
test('pathExists', t => {
t.is(pathExists(ESON1, ['obj', 'arr', 2, 'first']), true)
t.is(pathExists(ESON1, ['obj', 'foo']), false)
t.is(pathExists(ESON1, ['obj', 'foo', 'bar']), false)
t.is(pathExists(ESON1, []), true)
const eson = jsonToEson({
"obj": {
"arr": [1,2, {"first":3,"last":4}]
},
"str": "hello world",
"nill": null,
"bool": false
})
t.is(pathExists(eson, ['obj', 'arr', 2, 'first']), true)
t.is(pathExists(eson, ['obj', 'foo']), false)
t.is(pathExists(eson, ['obj', 'foo', 'bar']), false)
t.is(pathExists(eson, []), true)
})
test('parseJSONPointer', t => {
@ -282,14 +278,14 @@ test('search', t => {
const active = searchResult.active
t.deepEqual(matches, [
{path: ['obj', 'arr', 2, 'last'], area: 'property'},
{path: ['obj', 'arr', '2', 'last'], area: 'property'},
{path: ['str'], area: 'value'},
{path: ['nill'], area: 'property'},
{path: ['nill'], area: 'value'},
{path: ['bool'], area: 'property'},
{path: ['bool'], area: 'value'}
])
t.deepEqual(active, {path: ['obj', 'arr', 2, 'last'], area: 'property'})
t.deepEqual(active, {path: ['obj', 'arr', '2', 'last'], area: 'property'})
let expected = esonWithSearch
expected = setIn(expected, ['obj', 'arr', '2', 'last', META, 'searchProperty'], 'active')
@ -315,31 +311,31 @@ test('nextSearchResult', t => {
t.deepEqual(searchResult.matches, [
{path: ['obj', 'arr'], area: 'property'},
{path: ['obj', 'arr', 2, 'last'], area: 'property'},
{path: ['obj', 'arr', '2', 'last'], area: 'property'},
{path: ['bool'], area: 'value'}
])
t.deepEqual(searchResult.active, {path: ['obj', 'arr'], area: 'property'})
t.is(getIn(searchResult.eson, ['obj', 'arr', META, 'searchProperty']), 'active')
t.is(getIn(searchResult.eson, ['obj', 'arr', 2, 'last', META, 'searchProperty']), 'normal')
t.is(getIn(searchResult.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty']), 'normal')
t.is(getIn(searchResult.eson, ['bool', META, 'searchValue']), 'normal')
const second = nextSearchResult(searchResult.eson, searchResult.matches, searchResult.active)
t.deepEqual(second.active, {path: ['obj', 'arr', 2, 'last'], area: 'property'})
t.deepEqual(second.active, {path: ['obj', 'arr', '2', 'last'], area: 'property'})
t.is(getIn(second.eson, ['obj', 'arr', META, 'searchProperty']), 'normal')
t.is(getIn(second.eson, ['obj', 'arr', 2, 'last', META, 'searchProperty']), 'active')
t.is(getIn(second.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty']), 'active')
t.is(getIn(second.eson, ['bool', META, 'searchValue']), 'normal')
const third = nextSearchResult(second.eson, second.matches, second.active)
t.deepEqual(third.active, {path: ['bool'], area: 'value'})
t.is(getIn(third.eson, ['obj', 'arr', META, 'searchProperty']), 'normal')
t.is(getIn(third.eson, ['obj', 'arr', 2, 'last', META, 'searchProperty']), 'normal')
t.is(getIn(third.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty']), 'normal')
t.is(getIn(third.eson, ['bool', META, 'searchValue']), 'active')
const wrappedAround = nextSearchResult(third.eson, third.matches, third.active)
t.deepEqual(wrappedAround.active, {path: ['obj', 'arr'], area: 'property'})
t.is(getIn(wrappedAround.eson, ['obj', 'arr', META, 'searchProperty']), 'active')
t.is(getIn(wrappedAround.eson, ['obj', 'arr', 2, 'last', META, 'searchProperty']), 'normal')
t.is(getIn(wrappedAround.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty']), 'normal')
t.is(getIn(wrappedAround.eson, ['bool', META, 'searchValue']), 'normal')
})
@ -356,31 +352,31 @@ test('previousSearchResult', t => {
t.deepEqual(searchResult.matches, [
{path: ['obj', 'arr'], area: 'property'},
{path: ['obj', 'arr', 2, 'last'], area: 'property'},
{path: ['obj', 'arr', '2', 'last'], area: 'property'},
{path: ['bool'], area: 'value'}
])
t.deepEqual(searchResult.active, {path: ['obj', 'arr'], area: 'property'})
t.is(getIn(searchResult.eson, ['obj', 'arr', META, 'searchProperty']), 'active')
t.is(getIn(searchResult.eson, ['obj', 'arr', 2, 'last', META, 'searchProperty']), 'normal')
t.is(getIn(searchResult.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty']), 'normal')
t.is(getIn(searchResult.eson, ['bool', META, 'searchValue']), 'normal')
const third = previousSearchResult(searchResult.eson, searchResult.matches, searchResult.active)
t.deepEqual(third.active, {path: ['bool'], area: 'value'})
t.is(getIn(third.eson, ['obj', 'arr', META, 'searchProperty']), 'normal')
t.is(getIn(third.eson, ['obj', 'arr', 2, 'last', META, 'searchProperty']), 'normal')
t.is(getIn(third.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty']), 'normal')
t.is(getIn(third.eson, ['bool', META, 'searchValue']), 'active')
const second = previousSearchResult(third.eson, third.matches, third.active)
t.deepEqual(second.active, {path: ['obj', 'arr', 2, 'last'], area: 'property'})
t.deepEqual(second.active, {path: ['obj', 'arr', '2', 'last'], area: 'property'})
t.is(getIn(second.eson, ['obj', 'arr', META, 'searchProperty']), 'normal')
t.is(getIn(second.eson, ['obj', 'arr', 2, 'last', META, 'searchProperty']), 'active')
t.is(getIn(second.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty']), 'active')
t.is(getIn(second.eson, ['bool', META, 'searchValue']), 'normal')
const first = previousSearchResult(second.eson, second.matches, second.active)
t.deepEqual(first.active, {path: ['obj', 'arr'], area: 'property'})
t.is(getIn(first.eson, ['obj', 'arr', META, 'searchProperty']), 'active')
t.is(getIn(first.eson, ['obj', 'arr', 2, 'last', META, 'searchProperty']), 'normal')
t.is(getIn(first.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty']), 'normal')
t.is(getIn(first.eson, ['bool', META, 'searchValue']), 'normal')
})
@ -531,36 +527,6 @@ test('pathsFromSelection (after)', t => {
t.deepEqual(pathsFromSelection(ESON1, selection), [])
})
function assertDeepEqualEson (t, actual, expected, path = [], ignoreIds = true) {
const actualMeta = ignoreIds ? normalizeMetaIds(actual[META]) : actual[META]
const expectedMeta = ignoreIds ? normalizeMetaIds(expected[META]) : expected[META]
t.deepEqual(actualMeta, expectedMeta, `Meta data not equal, path=[${path.join(', ')}]`)
if (actualMeta.type === 'Array') {
t.deepEqual(actual.length, expected.length, 'Actual lengths of arrays should be equal, path=[${path.join(\', \')}]')
actual.forEach((item, index) => assertDeepEqualEson(t, actual[index], expected[index], path.concat(index)), ignoreIds)
}
else if (actualMeta.type === 'Object') {
t.deepEqual(Object.keys(actual).sort(), Object.keys(expected).sort(), 'Actual properties should be equal, path=[${path.join(\', \')}]')
actualMeta.keys.forEach(key => assertDeepEqualEson(t, actual[key], expected[key], path.concat(key)), ignoreIds)
}
else { // actual[META].type === 'value'
t.deepEqual(Object.keys(actual), [], '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
}
}, {})
}
// helper function to print JSON in the console
function printJSON (json, message = null) {
if (message) {

View File

@ -1,9 +1,8 @@
import { readFileSync } from 'fs'
import test from 'ava'
import { jsonToEsonOld, esonToJson, toEsonPath } from '../src/eson'
import { patchEson, cut } from '../src/patchEson'
const ESON1 = loadJSON('./resources/eson1.json')
import { META, jsonToEson, esonToJson } from '../src/eson'
import { patchEson } from '../src/patchEson'
import { assertDeepEqualEson } from './utils/assertDeepEqualEson'
test('jsonpatch add', t => {
const json = {
@ -15,21 +14,44 @@ test('jsonpatch add', t => {
{op: 'add', path: '/obj/b', value: {foo: 'bar'}}
]
const data = jsonToEsonOld(json)
const data = jsonToEson(json)
const result = patchEson(data, patch)
const patchedData = result.data
const revert = result.revert
const patchedJson = esonToJson(patchedData)
t.deepEqual(patchedJson, {
assertDeepEqualEson(t, patchedData, jsonToEson({
arr: [1,2,3],
obj: {a : 2, b: {foo: 'bar'}}
})
}))
t.deepEqual(revert, [
{op: 'remove', path: '/obj/b'}
])
})
test('jsonpatch add: insert in matrix', t => {
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(t, patchedData, jsonToEson({
arr: [1,4,2,3],
obj: {a : 2}
}))
t.deepEqual(revert, [
{op: 'remove', path: '/arr/1'}
])
})
test('jsonpatch add: append to matrix', t => {
const json = {
arr: [1,2,3],
@ -40,22 +62,20 @@ test('jsonpatch add: append to matrix', t => {
{op: 'add', path: '/arr/-', value: 4}
]
const data = jsonToEsonOld(json)
const data = jsonToEson(json)
const result = patchEson(data, patch)
const patchedData = result.data
const revert = result.revert
const patchedJson = esonToJson(patchedData)
t.deepEqual(patchedJson, {
assertDeepEqualEson(t, patchedData, jsonToEson({
arr: [1,2,3,4],
obj: {a : 2}
})
}))
t.deepEqual(revert, [
{op: 'remove', path: '/arr/3'}
])
})
test('jsonpatch remove', t => {
const json = {
arr: [1,2,3],
@ -67,23 +87,23 @@ test('jsonpatch remove', t => {
{op: 'remove', path: '/arr/1'},
]
const data = jsonToEsonOld(json)
const data = jsonToEson(json)
const result = patchEson(data, patch)
const patchedData = result.data
const revert = result.revert
const patchedJson = esonToJson(patchedData)
t.deepEqual(patchedJson, {
assertDeepEqualEson(t, patchedData, jsonToEson({
arr: [1,3],
obj: {}
})
}))
t.deepEqual(revert, [
{op: 'add', path: '/arr/1', value: 2, jsoneditor: {type: 'value'}},
{op: 'add', path: '/obj/a', value: 4, jsoneditor: {type: 'value', before: null}}
])
// test revert
const data2 = jsonToEsonOld(patchedJson)
const data2 = jsonToEson(patchedJson)
const result2 = patchEson(data2, revert)
const patchedData2 = result2.data
const revert2 = result2.revert
@ -104,23 +124,23 @@ test('jsonpatch replace', t => {
{op: 'replace', path: '/arr/1', value: 200},
]
const data = jsonToEsonOld(json)
const data = jsonToEson(json)
const result = patchEson(data, patch)
const patchedData = result.data
const revert = result.revert
const patchedJson = esonToJson(patchedData)
t.deepEqual(patchedJson, {
assertDeepEqualEson(t, patchedData, jsonToEson({
arr: [1,200,3],
obj: {a: 400}
})
}))
t.deepEqual(revert, [
{op: 'replace', path: '/arr/1', value: 2, jsoneditor: {type: 'value'}},
{op: 'replace', path: '/obj/a', value: 4, jsoneditor: {type: 'value'}}
])
// test revert
const data2 = jsonToEsonOld(patchedJson)
const data2 = jsonToEson(patchedJson)
const result2 = patchEson(data2, revert)
const patchedData2 = result2.data
const revert2 = result2.revert
@ -133,20 +153,21 @@ test('jsonpatch replace', t => {
])
})
test('jsonpatch replace (keep ids intact)', t => {
const json = { value: 42 }
const patch = [
{op: 'replace', path: '/value', value: 100}
]
const data = jsonToEsonOld(json)
const valueId = data.props[0].id
const patchedData = patchEson(data, patch).data
const patchedValueId = patchedData.props[0].id
t.is(patchedValueId, valueId)
})
// // FIXME: keep ids intact
// test('jsonpatch replace (keep ids intact)', t => {
// 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
//
// t.is(patchedValueId, valueId)
// })
test('jsonpatch copy', t => {
const json = {
@ -158,7 +179,7 @@ test('jsonpatch copy', t => {
{op: 'copy', from: '/obj', path: '/arr/2'},
]
const data = jsonToEsonOld(json)
const data = jsonToEson(json)
const result = patchEson(data, patch)
const patchedData = result.data
const revert = result.revert
@ -173,7 +194,7 @@ test('jsonpatch copy', t => {
])
// test revert
const data2 = jsonToEsonOld(patchedJson)
const data2 = jsonToEson(patchedJson)
const result2 = patchEson(data2, revert)
const patchedData2 = result2.data
const revert2 = result2.revert
@ -191,18 +212,15 @@ test('jsonpatch copy (keeps the same ids)', t => {
{op: 'copy', from: '/foo', path: '/copied'}
]
const data = jsonToEsonOld(json)
const fooId = data.props[0].id
const barId = data.props[0].value.props[0].id
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.props[0].id
const patchedBarId = patchedData.props[0].value.props[0].id
const copiedId = patchedData.props[1].id
const patchedCopiedBarId = patchedData.props[1].value.props[0].id
t.is(patchedData.props[0].name, 'foo')
t.is(patchedData.props[1].name, 'copied')
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
t.is(patchedFooId, fooId, 'same foo id')
t.is(patchedBarId, barId, 'same bar id')
@ -210,7 +228,6 @@ test('jsonpatch copy (keeps the same ids)', t => {
t.not(copiedId, fooId, 'different id of property copied')
// The id's of the copied childs are the same, that's okish, they will not bite each other
// FIXME: better solution for id's either always unique, or unique per object/array
t.is(patchedCopiedBarId, patchedBarId, 'same copied bar id')
})
@ -224,7 +241,7 @@ test('jsonpatch move', t => {
{op: 'move', from: '/obj', path: '/arr/2'},
]
const data = jsonToEsonOld(json)
const data = jsonToEson(json)
const result = patchEson(data, patch)
const patchedData = result.data
const revert = result.revert
@ -239,7 +256,7 @@ test('jsonpatch move', t => {
])
// test revert
const data2 = jsonToEsonOld(patchedJson)
const data2 = jsonToEson(patchedJson)
const result2 = patchEson(data2, revert)
const patchedData2 = result2.data
const revert2 = result2.revert
@ -260,7 +277,7 @@ test('jsonpatch move before', t => {
{op: 'move', from: '/obj', path: '/arr/2'},
]
const data = jsonToEsonOld(json)
const data = jsonToEson(json)
const result = patchEson(data, patch)
const patchedData = result.data
const revert = result.revert
@ -276,7 +293,7 @@ test('jsonpatch move before', t => {
])
// test revert
const data2 = jsonToEsonOld(patchedJson)
const data2 = jsonToEson(patchedJson)
const result2 = patchEson(data2, revert)
const patchedData2 = result2.data
const revert2 = result2.revert
@ -293,7 +310,7 @@ test('jsonpatch move and replace', t => {
{op: 'move', from: '/a', path: '/b'},
]
const data = jsonToEsonOld(json)
const data = jsonToEson(json)
const result = patchEson(data, patch)
const patchedData = result.data
@ -301,24 +318,9 @@ test('jsonpatch move and replace', t => {
const patchedJson = esonToJson(patchedData)
// id of the replaced B must be kept intact
t.is(patchedData.props[0].id, data.props[1].id)
replaceIds(patchedData)
t.deepEqual(patchedData, {
"type": "Object",
"expanded": true,
"props": [
{
"id": "[ID]",
"name": "b",
"value": {
"type": "value",
"value": 2
}
}
]
})
t.is(patchedData.b[META].id, data.b[META].id)
assertDeepEqualEson(t, patchedData, jsonToEson({b: 2}))
t.deepEqual(patchedJson, { b : 2 })
t.deepEqual(revert, [
{op:'move', from: '/b', path: '/a'},
@ -326,7 +328,7 @@ test('jsonpatch move and replace', t => {
])
// test revert
const data2 = jsonToEsonOld(patchedJson)
const data2 = jsonToEson(patchedJson)
const result2 = patchEson(data2, revert)
const patchedData2 = result2.data
const revert2 = result2.revert
@ -349,7 +351,7 @@ test('jsonpatch move and replace (nested)', t => {
{op: 'move', from: '/obj', path: '/arr'},
]
const data = jsonToEsonOld(json)
const data = jsonToEson(json)
const result = patchEson(data, patch)
const patchedData = result.data
const revert = result.revert
@ -364,7 +366,7 @@ test('jsonpatch move and replace (nested)', t => {
])
// test revert
const data2 = jsonToEsonOld(patchedJson)
const data2 = jsonToEson(patchedJson)
const result2 = patchEson(data2, revert)
const patchedData2 = result2.data
const revert2 = result2.revert
@ -383,32 +385,31 @@ test('jsonpatch move (keep id intact)', t => {
{op: 'move', from: '/value', path: '/moved'}
]
const data = jsonToEsonOld(json)
const valueId = data.props[0].id
const data = jsonToEson(json)
const valueId = data.value[META].id
const patchedData = patchEson(data, patch).data
const patchedValueId = patchedData.props[0].id
const patchedValueId = patchedData.moved[META].id
t.is(patchedValueId, valueId)
})
test('jsonpatch move and replace (keep ids intact)', t => {
const json = { a: 2, b: 3 }
const patch = [
{op: 'move', from: '/a', path: '/b'}
]
const data = jsonToEsonOld(json)
const bId = data.props[1].id
t.is(data.props[0].name, 'a')
t.is(data.props[1].name, 'b')
const patchedData = patchEson(data, patch).data
t.is(patchedData.props[0].name, 'b')
t.is(patchedData.props[0].id, bId)
})
// test('jsonpatch move and replace (keep ids intact)', t => {
// const json = { a: 2, b: 3 }
// const patch = [
// {op: 'move', from: '/a', path: '/b'}
// ]
//
// const data = jsonToEson(json)
// const bId = data.b[META].id
//
// t.deepEqual(data[META].keys, ['a', 'b'])
//
// const patchedData = patchEson(data, patch).data
//
// t.is(patchedData.b[META].id, bId)
// t.deepEqual(data[META].keys, ['b'])
// })
test('jsonpatch test (ok)', t => {
const json = {
@ -421,7 +422,7 @@ test('jsonpatch test (ok)', t => {
{op: 'add', path: '/added', value: 'ok'}
]
const data = jsonToEsonOld(json)
const data = jsonToEson(json)
const result = patchEson(data, patch)
const patchedData = result.data
const revert = result.revert
@ -449,7 +450,7 @@ test('jsonpatch test (fail: path not found)', t => {
{op: 'add', path: '/added', value: 'ok'}
]
const data = jsonToEsonOld(json)
const data = jsonToEson(json)
const result = patchEson(data, patch)
const patchedData = result.data
const revert = result.revert
@ -475,7 +476,7 @@ test('jsonpatch test (fail: value not equal)', t => {
{op: 'add', path: '/added', value: 'ok'}
]
const data = jsonToEsonOld(json)
const data = jsonToEson(json)
const result = patchEson(data, patch)
const patchedData = result.data
const revert = result.revert
@ -490,23 +491,6 @@ test('jsonpatch test (fail: value not equal)', t => {
t.is(result.error.toString(), 'Error: Test failed, value differs')
})
// helper function to replace all id properties with a constant value
function replaceIds (data, value = '[ID]') {
if (data.type === 'Object') {
data.props.forEach(prop => {
prop.id = value
replaceIds(prop.value, value)
})
}
if (data.type === 'Array') {
data.items.forEach(item => {
item.id = value
replaceIds(item.value, value)
})
}
}
// helper function to print JSON in the console
function printJSON (json, message = null) {
if (message) {

View File

@ -0,0 +1,40 @@
// TODO: move assertDeepEqualEson into a separate function
import {META} from "../../src/eson"
import lodashTransform from "lodash/transform"
export function assertDeepEqualEson (t, 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]
t.deepEqual(actualMeta, expectedMeta, `Meta data not equal, path=[${path.join(', ')}], actual[META]=${JSON.stringify(actualMeta)}, expected[META]=${JSON.stringify(expectedMeta)}`)
if (actualMeta.type === 'Array') {
t.deepEqual(actual.length, expected.length, 'Actual lengths of arrays should be equal, path=[${path.join(\', \')}]')
actual.forEach((item, index) => assertDeepEqualEson(t, actual[index], expected[index], path.concat(index)), ignoreIds)
}
else if (actualMeta.type === 'Object') {
t.deepEqual(Object.keys(actual).sort(), Object.keys(expected).sort(), 'Actual properties should be equal, path=[${path.join(\', \')}]')
actualMeta.keys.forEach(key => assertDeepEqualEson(t, actual[key], expected[key], path.concat(key)), ignoreIds)
}
else { // actual[META].type === 'value'
t.deepEqual(Object.keys(actual), [], '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
}
}, {})
}