diff --git a/src/actions.js b/src/actions.js index c893dc5..0571df1 100644 --- a/src/actions.js +++ b/src/actions.js @@ -1,9 +1,11 @@ import last from 'lodash/last' import initial from 'lodash/initial' import { - compileJSONPointer, getInEson, esonToJson, findNextProp, + META, + compileJSONPointer, esonToJson, findNextProp, pathsFromSelection, findRootPath, findSelectionIndices } from './eson' +import { cloneWithSymbols, getIn, setIn } from './utils/immutabilityHelpers' import { findUniqueName } from './utils/stringUtils' import { isObject, stringConvert } from './utils/typeUtils' import { compareAsc, compareDesc, strictShallowEqual } from './utils/arrayUtils' @@ -11,14 +13,14 @@ import { compareAsc, compareDesc, strictShallowEqual } from './utils/arrayUtils' /** * Create a JSONPatch to change the value of a property or item - * @param {ESON} data + * @param {ESON} eson * @param {Path} path * @param {*} value * @return {Array} */ -export function changeValue (data, path, value) { +export function changeValue (eson, path, value) { // console.log('changeValue', data, value) - const oldDataValue = getInEson(data, path) + const oldDataValue = getIn(eson, path) return [{ op: 'replace', @@ -32,18 +34,18 @@ export function changeValue (data, path, value) { /** * Create a JSONPatch to change a property name - * @param {ESON} data + * @param {ESON} eson * @param {Path} parentPath * @param {string} oldProp * @param {string} newProp * @return {Array} */ -export function changeProperty (data, parentPath, oldProp, newProp) { +export function changeProperty (eson, parentPath, oldProp, newProp) { // console.log('changeProperty', parentPath, oldProp, newProp) - const parent = getInEson(data, parentPath) + const parent = getIn(eson, parentPath) // prevent duplicate property names - const uniqueNewProp = findUniqueName(newProp, parent.props.map(p => p.name)) + const uniqueNewProp = findUniqueName(newProp, parent[META].keys) return [{ op: 'move', @@ -57,13 +59,13 @@ export function changeProperty (data, parentPath, oldProp, newProp) { /** * Create a JSONPatch to change the type of a property or item - * @param {ESON} data + * @param {ESON} eson * @param {Path} path * @param {ESONType} type * @return {Array} */ -export function changeType (data, path, type) { - const oldValue = esonToJson(getInEson(data, path)) +export function changeType (eson, path, type) { + const oldValue = esonToJson(getIn(eson, path)) const newValue = convertType(oldValue, type) // console.log('changeType', path, type, oldValue, newValue) @@ -85,20 +87,20 @@ export function changeType (data, path, type) { * a unique property name for the duplicated node in case of duplicating * and object property * - * @param {ESON} data + * @param {ESON} eson * @param {Selection} selection * @return {Array} */ -export function duplicate (data, selection) { +export function duplicate (eson, selection) { // console.log('duplicate', path) if (!selection.start || !selection.end) { return [] } const rootPath = findRootPath(selection) - const root = getInEson(data, rootPath) + const root = getIn(eson, rootPath) const { maxIndex } = findSelectionIndices(root, rootPath, selection) - const paths = pathsFromSelection(data, selection) + const paths = pathsFromSelection(eson, selection) if (root.type === 'Array') { return paths.map((path, offset) => ({ @@ -108,12 +110,11 @@ export function duplicate (data, selection) { })) } else { // object.type === 'Object' - const nextProp = root.props && root.props[maxIndex] - const before = nextProp ? nextProp.name : null + const before = root[META].keys[maxIndex] || null return paths.map(path => { const prop = last(path) - const newProp = findUniqueName(prop, root.props.map(p => p.name)) + const newProp = findUniqueName(prop, root[META].keys) return { op: 'copy', @@ -134,14 +135,14 @@ export function duplicate (data, selection) { * a unique property name for the inserted node in case of duplicating * and object property * - * @param {ESON} data + * @param {ESON} eson * @param {Path} path * @param {Array.<{name?: string, value: JSONType, type?: ESONType}>} values * @return {Array} */ -export function insertBefore (data, path, values) { // TODO: find a better name and define datastructure for values +export function insertBefore (eson, path, values) { // TODO: find a better name and define datastructure for values const parentPath = initial(path) - const parent = getInEson(data, parentPath) + const parent = getIn(eson, parentPath) if (parent.type === 'Array') { const startIndex = parseInt(last(path)) @@ -157,7 +158,7 @@ export function insertBefore (data, path, values) { // TODO: find a better name else { // object.type === 'Object' const before = last(path) return values.map(entry => { - const newProp = findUniqueName(entry.name, parent.props.map(p => p.name)) + const newProp = findUniqueName(entry.name, parent[META].keys) return { op: 'add', path: compileJSONPointer(parentPath.concat(newProp)), @@ -178,18 +179,18 @@ export function insertBefore (data, path, values) { // TODO: find a better name * a unique property name for the inserted node in case of duplicating * and object property * - * @param {ESON} data + * @param {ESON} eson * @param {Selection} selection * @param {Array.<{name?: string, value: JSONType, type?: ESONType}>} values * @return {Array} */ -export function replace (data, selection, values) { // TODO: find a better name and define datastructure for values +export function replace (eson, selection, values) { // TODO: find a better name and define datastructure for values const rootPath = findRootPath(selection) - const root = getInEson(data, rootPath) + const root = getIn(eson, rootPath) const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection) if (root.type === 'Array') { - const removeActions = removeAll(pathsFromSelection(data, selection)) + const removeActions = removeAll(pathsFromSelection(eson, selection)) const insertActions = values.map((entry, offset) => ({ op: 'add', path: compileJSONPointer(rootPath.concat(minIndex + offset)), @@ -202,12 +203,11 @@ export function replace (data, selection, values) { // TODO: find a better name return removeActions.concat(insertActions) } else { // object.type === 'Object' - const nextProp = root.props && root.props[maxIndex] - const before = nextProp ? nextProp.name : null + const before = root[META].keys[maxIndex] || null - const removeActions = removeAll(pathsFromSelection(data, selection)) + const removeActions = removeAll(pathsFromSelection(eson, selection)) const insertActions = values.map(entry => { - const newProp = findUniqueName(entry.name, root.props.map(p => p.name)) + const newProp = findUniqueName(entry.name, root[META].keys) return { op: 'add', path: compileJSONPointer(rootPath.concat(newProp)), @@ -230,15 +230,15 @@ export function replace (data, selection, values) { // TODO: find a better name * a unique property name for the inserted node in case of duplicating * and object property * - * @param {ESON} data + * @param {ESON} eson * @param {Path} parentPath * @param {ESONType} type * @return {Array} */ -export function append (data, parentPath, type) { +export function append (eson, parentPath, type) { // console.log('append', parentPath, value) - const parent = getInEson(data, parentPath) + const parent = getIn(eson, parentPath) const value = createEntry(type) if (parent.type === 'Array') { @@ -252,7 +252,7 @@ export function append (data, parentPath, type) { }] } else { // object.type === 'Object' - const newProp = findUniqueName('', parent.props.map(p => p.name)) + const newProp = findUniqueName('', parent[META].keys) return [{ op: 'add', @@ -295,50 +295,49 @@ 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} data + * @param {ESON} eson * @param {Path} path * @param {'asc' | 'desc' | null} [order=null] If not provided, will toggle current ordering * @return {Array} */ -export function sort (data, path, order = null) { +export function sort (eson, path, order = null) { // console.log('sort', path, order) const compare = order === 'desc' ? compareDesc : compareAsc - const object = getInEson(data, path) + const object = getIn(eson, path) if (object.type === 'Array') { - const orderedItems = object.items.slice(0) + const orderedItems = object.slice() // order the items by value orderedItems.sort((a, b) => compare(a.value, b.value)) // when no order is provided, test whether ordering ascending // changed anything. If not, sort descending - if (!order && strictShallowEqual(object.items, orderedItems)) { + if (!order && strictShallowEqual(object, orderedItems)) { orderedItems.reverse() } return [{ op: 'replace', path: compileJSONPointer(path), - value: esonToJson({ - type: 'Array', - items: orderedItems - }) + value: orderedItems }] } else { // object.type === 'Object' - const orderedProps = object.props.slice(0) // order the properties by key - orderedProps.sort((a, b) => compare(a.name, b.name)) + const orderedKeys = object[META].keys.slice().sort((a, b) => compare(a.name, b.name)) // when no order is provided, test whether ordering ascending // changed anything. If not, sort descending - if (!order && strictShallowEqual(object.props, orderedProps)) { - orderedProps.reverse() + if (!order && strictShallowEqual(object[META].keys, orderedKeys)) { + orderedKeys.reverse() } + const orderedProps = cloneWithSymbols(object) + orderedProps[META] = setIn(object[META], ['keys'], orderedKeys) + return [{ op: 'replace', path: compileJSONPointer(path), diff --git a/src/components/TextMode.js b/src/components/TextMode.js index 83cd819..264a03a 100644 --- a/src/components/TextMode.js +++ b/src/components/TextMode.js @@ -5,7 +5,7 @@ import Ajv from 'ajv' import { parseJSON } from '../utils/jsonUtils' import { escapeUnicodeChars } from '../utils/stringUtils' import { enrichSchemaError, limitErrors } from '../utils/schemaUtils' -import { jsonToEsonOld, esonToJson } from '../eson' +import { jsonToEson, esonToJson } from '../eson' import { patchEson } from '../patchEson' import { createFindKeyBinding } from '../utils/keyBindings' import { KEY_BINDINGS } from '../constants' @@ -334,7 +334,7 @@ export default class TextMode extends Component { patch (actions) { const json = this.get() - const data = jsonToEsonOld(json) + const data = jsonToEson(json) const result = patchEson(data, actions) this.set(esonToJson(result.data)) diff --git a/src/components/TreeMode.js b/src/components/TreeMode.js index b58d836..88338eb 100644 --- a/src/components/TreeMode.js +++ b/src/components/TreeMode.js @@ -336,19 +336,19 @@ export default class TreeMode extends Component { } handleChangeValue = (path, value) => { - this.handlePatch(changeValue(this.state.data, path, value)) + this.handlePatch(changeValue(this.state.eson, path, value)) } handleChangeProperty = (parentPath, oldProp, newProp) => { - this.handlePatch(changeProperty(this.state.data, parentPath, oldProp, newProp)) + this.handlePatch(changeProperty(this.state.eson, parentPath, oldProp, newProp)) } handleChangeType = (path, type) => { - this.handlePatch(changeType(this.state.data, path, type)) + this.handlePatch(changeType(this.state.eson, path, type)) } handleInsert = (path, type) => { - this.handlePatch(insertBefore(this.state.data, path, [{ + this.handlePatch(insertBefore(this.state.eson, path, [{ type, name: '', value: createEntry(type) @@ -367,7 +367,7 @@ export default class TreeMode extends Component { } handleAppend = (parentPath, type) => { - this.handlePatch(append(this.state.data, parentPath, type)) + this.handlePatch(append(this.state.eson, parentPath, type)) // apply focus to new node this.focusToNext(parentPath) @@ -375,7 +375,7 @@ export default class TreeMode extends Component { handleDuplicate = () => { if (this.state.selection) { - this.handlePatch(duplicate(this.state.data, this.state.selection)) + this.handlePatch(duplicate(this.state.eson, this.state.selection)) // TODO: focus to duplicated selection } } @@ -395,7 +395,7 @@ export default class TreeMode extends Component { else if (this.state.selection) { // remove selection // TODO: select next property? (same as when removing a path?) - const paths = pathsFromSelection(this.state.data, this.state.selection) + const paths = pathsFromSelection(this.state.eson, this.state.selection) this.setState({ selection: null }) this.handlePatch(removeAll(paths)) } @@ -446,13 +446,13 @@ export default class TreeMode extends Component { } handleKeyDownPaste = (event) => { - const { clipboard, data } = this.state + const { clipboard, eson } = this.state if (clipboard && clipboard.length > 0) { event.preventDefault() const path = this.findDataPathFromElement(event.target) - this.handlePatch(insertBefore(data, path, clipboard)) + this.handlePatch(insertBefore(eson, path, clipboard)) } } @@ -460,7 +460,7 @@ export default class TreeMode extends Component { const path = this.findDataPathFromElement(event.target) if (path) { const selection = { start: path, end: path } - this.handlePatch(duplicate(this.state.data, selection)) + this.handlePatch(duplicate(this.state.eson, selection)) // apply focus to the duplicated node this.focusToNext(path) @@ -470,9 +470,9 @@ export default class TreeMode extends Component { handleCut = () => { const selection = this.state.selection if (selection && selection.start && selection.end) { - const data = this.state.data - const paths = pathsFromSelection(data, selection) - const clipboard = contentsFromPaths(data, paths) + const eson = this.state.eson + const paths = pathsFromSelection(eson, selection) + const clipboard = contentsFromPaths(eson, paths) this.setState({ clipboard, selection: null }) @@ -490,9 +490,9 @@ export default class TreeMode extends Component { handleCopy = () => { const selection = this.state.selection if (selection && selection.start && selection.end) { - const data = this.state.data - const paths = pathsFromSelection(data, selection) - const clipboard = contentsFromPaths(data, paths) + const eson = this.state.eson + const paths = pathsFromSelection(eson, selection) + const clipboard = contentsFromPaths(eson, paths) this.setState({ clipboard }) } @@ -503,11 +503,11 @@ export default class TreeMode extends Component { } handlePaste = () => { - const { data, selection, clipboard } = this.state + const { eson, selection, clipboard } = this.state if (selection && clipboard && clipboard.length > 0) { this.setState({ selection: null }) - this.handlePatch(replace(data, selection, clipboard)) + this.handlePatch(replace(eson, selection, clipboard)) // TODO: select the pasted contents } } @@ -539,7 +539,7 @@ export default class TreeMode extends Component { } handleSort = (path, order = null) => { - this.handlePatch(sort(this.state.data, path, order)) + this.handlePatch(sort(this.state.eson, path, order)) } handleSelect = (selection: Selection) => { @@ -753,13 +753,13 @@ export default class TreeMode extends Component { * Emit an onChange event when there is a listener for it. * @private */ - emitOnChange (patch: ESONPatch, revert: ESONPatch, data: ESON) { + emitOnChange (patch: ESONPatch, revert: ESONPatch, eson: ESON) { if (this.props.onPatch) { this.props.onPatch(patch, revert) } if (this.props.onChange || this.props.onChangeText) { - const json = esonToJson(data) + const json = esonToJson(eson) if (this.props.onChange) { this.props.onChange(json) @@ -798,10 +798,10 @@ export default class TreeMode extends Component { const historyIndex = this.state.historyIndex const historyItem = history[historyIndex] - const result = patchEson(this.state.data, historyItem.undo) + const result = patchEson(this.state.eson, historyItem.undo) this.setState({ - data: result.data, + eson: result.data, history, historyIndex: historyIndex + 1 }) @@ -816,10 +816,10 @@ export default class TreeMode extends Component { const historyIndex = this.state.historyIndex - 1 const historyItem = history[historyIndex] - const result = patchEson(this.state.data, historyItem.redo) + const result = patchEson(this.state.eson, historyItem.redo) this.setState({ - data: result.data, + eson: result.data, history, historyIndex }) @@ -845,10 +845,10 @@ export default class TreeMode extends Component { } const expand = options.expand || (path => this.expandKeepOrExpandAll(path)) - const result = patchEson(this.state.data, actions, expand) - const data = result.data + const result = patchEson(this.state.eson, actions, expand) + const eson = result.data - if (this.props.history != false) { + if (this.props.history !== false) { // update data and store history const historyItem = { redo: actions, @@ -860,21 +860,21 @@ export default class TreeMode extends Component { .slice(0, MAX_HISTORY_ITEMS) this.setState({ - data, + eson, history, historyIndex: 0 }) } else { // update data and don't store history - this.setState({ data }) + this.setState({ eson }) } return { patch: actions, revert: result.revert, error: result.error, - data // FIXME: shouldn't pass data here + data: eson // FIXME: shouldn't pass data here } } @@ -973,7 +973,7 @@ export default class TreeMode extends Component { * @param {Path} path */ exists (path) { - return pathExists(this.state.data, path) + return pathExists(this.state.eson, path) } /** diff --git a/src/eson.js b/src/eson.js index acd1c1e..510b6f8 100644 --- a/src/eson.js +++ b/src/eson.js @@ -15,7 +15,7 @@ import initial from 'lodash/initial' import last from 'lodash/last' import type { - ESON, ESONObject, ESONArrayItem, ESONPointer, Selection, ESONType, ESONPath, + ESON, ESONObject, ESONArrayItem, ESONPointer, Selection, ESONPath, Path, JSONPath, JSONType } from './types' @@ -29,6 +29,15 @@ export const SELECTED_AFTER = 4 export const META = Symbol('meta') +/** + * Expand function which will expand all nodes + * @param {Path} path + * @return {boolean} + */ +export function expandAll (path) { + return true +} + /** * * @param {JSONType} json @@ -58,57 +67,6 @@ export function jsonToEson (json, path = []) { } } -/** - * Expand function which will expand all nodes - * @param {Path} path - * @return {boolean} - */ -export function expandAll (path) { - return true -} - -/** - * Convert a JSON object into ESON - * @param {Object | Array | string | number | boolean | null} json - * @param {function(path: JSONPath)} [expand] - * @param {JSONPath} [path=[]] - * @param {ESONType} [type='value'] Optional eson type for the created value - * @return {ESON} - */ -export function jsonToEsonOld (json, expand = expandAll, path: JSONPath = [], type: ESONType = 'value') : ESON { - if (Array.isArray(json)) { - return { - type: 'Array', - expanded: expand(path), - items: json.map((child, index) => { - return { - id: createId(), // TODO: use id based on index (only has to be unique within this array) - value: jsonToEsonOld(child, expand, path.concat(index)) - } - }) - } - } - else if (isObject(json)) { - return { - type: 'Object', - expanded: expand(path), - props: Object.keys(json).map((name, index) => { - return { - id: createId(), // TODO: use id based on index (only has to be unique within this array) - name, - value: jsonToEsonOld(json[name], expand, path.concat(name)) - } - }) - } - } - else { // value - return { - type: (type === 'string') ? 'string' : 'value', - value: json - } - } -} - /** * Convert an ESON object to a JSON object * @param {ESON} eson @@ -133,103 +91,6 @@ export function esonToJson (eson: ESON) { } } -/** - * Convert a path of a JSON object into a path in the corresponding ESON object - * @param {ESON} eson - * @param {JSONPath} path - * @return {ESONPath} esonPath - * @private - */ -export function toEsonPath (eson: ESON, path: JSONPath) : ESONPath { - if (path.length === 0) { - return [] - } - - if (eson.type === 'Array') { - // index of an array - const index = path[0] - const item = eson.items[parseInt(index)] - if (!item) { - throw new Error('Array item "' + index + '" not found') - } - - return ['items', String(index), 'value'].concat(toEsonPath(item.value, path.slice(1))) - } - else if (eson.type === 'Object') { - // object property. find the index of this property - const index = findPropertyIndex(eson, path[0]) - const prop = eson.props[index] - if (!prop) { - throw new Error('Object property "' + path[0] + '" not found') - } - - return ['props', String(index), 'value'] - .concat(toEsonPath(prop.value, path.slice(1))) - } - else { - return [] - } -} - -/** - * Convert an ESON object to a JSON object - * @param {ESON} eson - * @param {ESONPath} esonPath - * @return {JSONPath} path - */ -export function toJsonPath (eson: ESON, esonPath: ESONPath) : JSONPath { - if (esonPath.length === 0) { - return [] - } - - if (eson.type === 'Array') { - // index of an array - const index = esonPath[1] - const item = eson.items[parseInt(index)] - if (!item) { - throw new Error('Array item "' + index + '" not found') - } - - return [index].concat(toJsonPath(item.value, esonPath.slice(3))) - } - else if (eson.type === 'Object') { - // object property. find the index of this property - const index = esonPath[1] - const prop = eson.props[parseInt(index)] - if (!prop) { - throw new Error('Object property "' + esonPath[1] + '" not found') - } - - return [prop.name].concat(toJsonPath(prop.value, esonPath.slice(3))) - } - else { - return [] - } -} - -/** - * Get a nested property from an ESON object using a JSON path - */ -export function getInEson (eson: ESON, jsonPath: JSONPath) { - return getIn(eson, toEsonPath(eson, jsonPath)) -} - -/** - * Set the value of a nested property in an ESON object using a JSON path - */ -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 deleteInEson (eson: ESON, jsonPath: JSONPath) : JSONType { - // with initial we remove the 'value' property, - // we want to remove the whole item from the items array - return deleteIn(eson, initial(toEsonPath(eson, jsonPath))) -} - /** * Transform an eson object, traverse over the whole object (excluding the _meta) * objects, and allow replacing Objects/Arrays/values @@ -569,14 +430,19 @@ export function applySelection (eson, selection) { * Find the min and max index of a start and end child. * Start and end can be a property name in case of an Object, * or a matrix index (string with a number) in case of an Array. + * + * @param {ESON} root + * @param {Path} rootPath + * @param {Selection} selection + * @return {{minIndex: number, maxIndex: number}} */ -export function findSelectionIndices (root: ESON, rootPath: JSONPath, selection: Selection) : { minIndex: number, maxIndex: number } { +export function findSelectionIndices (root, rootPath, selection) { const start = (selection.after || selection.before || selection.start)[rootPath.length] const end = (selection.after || selection.before || selection.end)[rootPath.length] // if no object we assume it's an Array - const startIndex = root.type === 'Object' ? findPropertyIndex(root, start) : parseInt(start) - const endIndex = root.type === 'Object' ? findPropertyIndex(root, end) : parseInt(end) + const startIndex = root[META].type === 'Object' ? root[META].keys.indexOf(start) : parseInt(start) + const endIndex = root[META].type === 'Object' ? root[META].keys.indexOf(end) : parseInt(end) const minIndex = Math.min(startIndex, endIndex) const maxIndex = Math.max(startIndex, endIndex) + @@ -588,15 +454,15 @@ export function findSelectionIndices (root: ESON, rootPath: JSONPath, selection: /** * Get the JSON paths from a selection, sorted from first to last */ -export function pathsFromSelection (eson: ESON, selection: Selection): JSONPath[] { +export function pathsFromSelection (eson, selection: Selection): JSONPath[] { // find the parent node shared by both start and end of the selection const rootPath = findRootPath(selection) - const root = getInEson(eson, rootPath) + const root = getIn(eson, rootPath) const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection) - if (root.type === 'Object') { - return times(maxIndex - minIndex, i => rootPath.concat(root.props[i + minIndex].name)) + if (root[META].type === 'Object') { + return times(maxIndex - minIndex, i => rootPath.concat(root[META].keys[i + minIndex])) } else { // root.type === 'Array' return times(maxIndex - minIndex, i => rootPath.concat(String(i + minIndex))) @@ -611,10 +477,9 @@ export function pathsFromSelection (eson: ESON, selection: Selection): JSONPath[ */ export function contentsFromPaths (data: ESON, paths: JSONPath[]) { return paths.map(path => { - const esonPath = toEsonPath(data, path) return { - name: getIn(data, initial(esonPath).concat('name')) || String(esonPath[esonPath.length - 2]), - value: esonToJson(getIn(data, esonPath)) + name: getIn(data, last(path)), + value: esonToJson(getIn(data, path)) // FIXME: also store the type and expanded state } }) @@ -659,93 +524,6 @@ function findSharedPath (path1: JSONPath, path2: JSONPath): JSONPath { return path1.slice(0, i) } -// -// /** -// * Recursively transform ESON: a recursive "map" function -// * @param {ESON} eson -// * @param {function(value: ESON, path: Path, root: ESON)} callback -// * @return {ESON} Returns the transformed eson object -// */ -// export function transform (eson: ESON, callback: RecurseCallback) : ESON { -// return recurseTransform (eson, [], eson, callback) -// } -// -// /** -// * Recursively transform ESON -// * @param {ESON} value -// * @param {JSONPath} path -// * @param {ESON} root The root object, object at path=[] -// * @param {function(value: ESON, path: Path, root: ESON)} callback -// * @return {ESON} Returns the transformed eson object -// */ -// function recurseTransform (value: ESON, path: JSONPath, root: ESON, callback: RecurseCallback) : ESON { -// let updatedValue: ESON = callback(value, path, root) -// -// if (value.type === 'Array') { -// let updatedItems = updatedValue.items -// -// updatedValue.items.forEach((item, index) => { -// const updatedItem = recurseTransform(item.value, path.concat(String(index)), root, callback) -// updatedItems = setIn(updatedItems, [index, 'value'], updatedItem) -// }) -// -// updatedValue = setIn(updatedValue, ['items'], updatedItems) -// } -// -// if (value.type === 'Object') { -// let updatedProps = updatedValue.props -// -// updatedValue.props.forEach((prop, index) => { -// const updatedItem = recurseTransform(prop.value, path.concat(prop.name), root, callback) -// updatedProps = setIn(updatedProps, [index, 'value'], updatedItem) -// }) -// -// updatedValue = setIn(updatedValue, ['props'], updatedProps) -// } -// -// // (for type 'string' or 'value' there are no childs to traverse) -// -// return updatedValue -// } - -/** - * Recursively loop over a ESON object: a recursive "forEach" function. - * @param {ESON} eson - * @param {function(value: ESON, path: JSONPath, root: ESON)} callback - */ -export function traverse (eson: ESON, callback: RecurseCallback) { - return recurseTraverse (eson, [], eson, callback) -} - -/** - * Recursively traverse a ESON object - * @param {ESON} value - * @param {JSONPath} path - * @param {ESON | null} root The root object, object at path=[] - * @param {function(value: ESON, path: Path, root: ESON)} callback - */ -function recurseTraverse (value: ESON, path: JSONPath, root: ESON, callback: RecurseCallback) { - callback(value, path, root) - - switch (value.type) { - case 'Array': { - value.items.forEach((item: ESONArrayItem, index) => { - recurseTraverse(item.value, path.concat(String(index)), root, callback) - }) - break - } - - case 'Object': { - value.props.forEach((prop) => { - recurseTraverse(prop.value, path.concat(prop.name), root, callback) - }) - break - } - - default: // type 'string' or 'value' - // no childs to traverse - } -} /** * Test whether a path exists in the eson object @@ -808,12 +586,12 @@ export function findNextProp (parent, prop) { /** * Find the index of a property - * @param {ESONObject} object + * @param {ESON} object * @param {string} prop * @return {number} Returns the index when found, -1 when not found */ -export function findPropertyIndex (object: ESONObject, prop: string) { - return object.props.findIndex(p => p.name === prop) +export function findPropertyIndex (object, prop) { + return object[META].keys.indexOf(prop) } // TODO: move parseJSONPointer and compileJSONPointer to a separate file diff --git a/test/eson.test.js b/test/eson.test.js index d7bc71a..b14e4ff 100644 --- a/test/eson.test.js +++ b/test/eson.test.js @@ -3,7 +3,7 @@ import test from 'ava' import { setIn, getIn, deleteIn } from '../src/utils/immutabilityHelpers' import { META, - esonToJson, toEsonPath, toJsonPath, pathExists, transform, traverse, + esonToJson, pathExists, transform, parseJSONPointer, compileJSONPointer, jsonToEson, expand, expandOne, expandPath, applyErrors, search, nextSearchResult, @@ -15,8 +15,6 @@ import 'console.table' 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('jsonToEson', t => { @@ -236,33 +234,6 @@ test('add and remove errors', t => { t.is(actual3.str, eson.str) // shouldn't have touched values not affected by the errors }) -test('traverse', t => { - // {obj: {a: 2}, arr: [3]} - - let log = [] - const returnValue = traverse(ESON2, function (value, path, root) { - t.is(root, ESON2) - - log.push([value, path, root]) - }) - - t.is(returnValue, undefined) - - const EXPECTED_LOG = [ - [ESON2, [], ESON2], - [ESON2.props[0].value, ['obj'], ESON2], - [ESON2.props[0].value.props[0].value, ['obj', 'a'], ESON2], - [ESON2.props[1].value, ['arr'], ESON2], - [ESON2.props[1].value.items[0].value, ['arr', '0'], ESON2], - ] - - log.forEach((row, index) => { - t.deepEqual(log[index], EXPECTED_LOG[index], 'should have equal log at index ' + index ) - }) - t.deepEqual(log, EXPECTED_LOG) -}) - - test('search', t => { const eson = jsonToEson({ "obj": { @@ -476,12 +447,20 @@ test('selection (node)', t => { }) test('pathsFromSelection (object)', t => { + const eson = jsonToEson({ + "obj": { + "arr": [1,2, {"first":3,"last":4}] + }, + "str": "hello world", + "nill": null, + "bool": false + }) const selection = { start: ['obj', 'arr', '2', 'last'], end: ['nill'] } - t.deepEqual(pathsFromSelection(ESON1, selection), [ + t.deepEqual(pathsFromSelection(eson, selection), [ ['obj'], ['str'], ['nill'] @@ -489,42 +468,74 @@ test('pathsFromSelection (object)', t => { }) test('pathsFromSelection (array)', t => { + const eson = jsonToEson({ + "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 } - t.deepEqual(pathsFromSelection(ESON1, selection), [ + t.deepEqual(pathsFromSelection(eson, selection), [ ['obj', 'arr', '0'], ['obj', 'arr', '1'] ]) }) test('pathsFromSelection (value)', t => { + const eson = jsonToEson({ + "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'] } - t.deepEqual(pathsFromSelection(ESON1, selection), [ + t.deepEqual(pathsFromSelection(eson, selection), [ ['obj', 'arr', '2', 'first'], ]) }) test('pathsFromSelection (before)', t => { + const eson = jsonToEson({ + "obj": { + "arr": [1,2, {"first":3,"last":4}] + }, + "str": "hello world", + "nill": null, + "bool": false + }) const selection = { before: ['obj', 'arr', '2', 'first'] } - t.deepEqual(pathsFromSelection(ESON1, selection), []) + t.deepEqual(pathsFromSelection(eson, selection), []) }) test('pathsFromSelection (after)', t => { + 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'] } - t.deepEqual(pathsFromSelection(ESON1, selection), []) + t.deepEqual(pathsFromSelection(eson, selection), []) }) // helper function to print JSON in the console