Refactored model to use Symbol

This commit is contained in:
jos 2017-12-15 14:02:42 +01:00
parent a059eb844e
commit 53b20e2f59
5 changed files with 269 additions and 195 deletions

29
package-lock.json generated
View File

@ -1717,8 +1717,7 @@
"clone": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz",
"integrity": "sha1-Jgt6meux7f4kdTgXX3gyQ8sZ0Uk=",
"dev": true
"integrity": "sha1-Jgt6meux7f4kdTgXX3gyQ8sZ0Uk="
},
"clone-stats": {
"version": "0.0.1",
@ -1936,6 +1935,14 @@
"date-now": "0.1.4"
}
},
"console.table": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/console.table/-/console.table-0.9.1.tgz",
"integrity": "sha1-SwH9CmtW//t5CSeD5WqbuQZ4cow=",
"requires": {
"easy-table": "1.1.0"
}
},
"constants-browserify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
@ -2224,7 +2231,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
"integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
"dev": true,
"requires": {
"clone": "1.0.2"
}
@ -2330,6 +2336,14 @@
}
}
},
"easy-table": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.1.0.tgz",
"integrity": "sha1-hvmrTBAvA3G3KXuSplHVgkvIy3M=",
"requires": {
"wcwidth": "1.0.1"
}
},
"eazy-logger": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-3.0.2.tgz",
@ -7891,6 +7905,15 @@
}
}
},
"wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
"integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=",
"optional": true,
"requires": {
"defaults": "1.0.3"
}
},
"webpack": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-1.14.0.tgz",

View File

@ -29,6 +29,7 @@
"dependencies": {
"ajv": "4.10.4",
"brace": "0.9.1",
"console.table": "0.9.1",
"javascript-natural-sort": "0.7.1",
"lodash": "4.17.4",
"prop-types": "15.5.10",

View File

@ -5,7 +5,7 @@
* All functions are pure and don't mutate the ESON.
*/
import { setIn, getIn, updateIn, deleteIn } from './utils/immutabilityHelpers'
import { setIn, getIn, updateIn, deleteIn, cloneWithSymbols } from './utils/immutabilityHelpers'
import { isObject } from './utils/typeUtils'
import isEqual from 'lodash/isEqual'
import isEmpty from 'lodash/isEmpty'
@ -27,6 +27,8 @@ export const SELECTED_END = 2
export const SELECTED_BEFORE = 3
export const SELECTED_AFTER = 4
export const META = Symbol('meta')
/**
*
* @param {JSONType} json
@ -40,19 +42,19 @@ export function jsonToEson (json, path = []) {
let eson = {}
const keys = Object.keys(json)
keys.forEach((key) => eson[key] = jsonToEson(json[key], path.concat(key)))
eson._meta = { id, path, type: 'Object', keys }
// TODO: rename keys to props
eson[META] = { id, path, type: 'Object', keys }
return eson
}
else if (Array.isArray(json)) {
let eson = {}
json.forEach((value, index) => eson[index] = jsonToEson(value, path.concat(index)))
eson._meta = { id, path, type: 'Array', length: json.length }
let eson = json.map((value, index) => jsonToEson(value, path.concat(index)))
eson[META] = { id, path, type: 'Array' }
return eson
}
else { // json is a number, string, boolean, or null
return {
_meta: { id, path, type: 'value', value: json }
}
let eson = {}
eson[META] = { id, path, type: 'value', value: json }
return eson
}
}
@ -63,7 +65,7 @@ export function jsonToEson (json, path = []) {
* @return {Array}
*/
export function mapEsonArray (esonArray, callback) {
const length = esonArray._meta.length
const length = esonArray[META].length
let result = []
for (let i = 0; i < length; i++) {
result[i] = callback(esonArray[i], i, esonArray)
@ -71,33 +73,6 @@ export function mapEsonArray (esonArray, callback) {
return result
}
/**
* Splice an eson Array: delete items and insert items
* @param {ESON} esonArray
* @param {number} start
* @param {number} deleteCount
* @param {Array} [insertItems]
*/
export function spliceEsonArray(esonArray, start, deleteCount, insertItems = []) {
let splicedArray = {}
const originalLength = esonArray._meta.length
for (let i = 0; i < start; i++) {
splicedArray[i] = esonArray[i]
}
for (let i = 0; i < insertItems.length; i++) {
splicedArray[i + start] = insertItems[i]
}
for (let i = start + deleteCount; i < originalLength; i++) {
splicedArray[i - deleteCount] = esonArray[i]
}
const length = originalLength - deleteCount + insertItems.length
splicedArray._meta = setIn(esonArray._meta, ['length'], length)
return splicedArray
}
/**
* Expand function which will expand all nodes
* @param {Path} path
@ -288,20 +263,29 @@ export function deleteInEson (eson: ESON, jsonPath: JSONPath) : JSONType {
export function transform (eson, callback, path = []) {
const updated = callback(eson, path)
if (updated._meta.type === 'Object' || updated._meta.type === 'Array') {
if (updated[META].type === 'Object') {
let changed = false
let updatedProps = {}
let updatedObj = {}
for (let key in updated) {
if (updated.hasOwnProperty(key) && key !== '_meta') { // don't traverse the _meta objects
const childPath = path.concat(updated._meta.type === 'Array' ? parseInt(key) : key)
updatedProps[key] = transform(updated[key], callback, childPath)
changed = changed || (updatedProps[key] !== updated[key])
if (updated.hasOwnProperty(key)) {
updatedObj[key] = transform(updated[key], callback, path.concat(key))
changed = changed || (updatedObj[key] !== updated[key])
}
}
updatedProps._meta = updated._meta
return changed ? updatedProps : updated
updatedObj[META] = updated[META]
return changed ? updatedObj : updated
}
else { // eson._meta.type === 'value'
else if (updated[META].type === 'Array') {
let changed = false
let updatedArr = []
for (let i = 0; i < updated.length; i++) {
updatedArr[i] = transform(updated[i], callback, path.concat(i))
changed = changed || (updatedArr[i] !== updated[i])
}
updatedArr[META] = updated[META]
return changed ? updatedArr : updated
}
else { // eson[META].type === 'value'
return updated
}
}
@ -318,7 +302,7 @@ export function transform (eson, callback, path = []) {
*/
export function expand (eson, filterCallback, expanded = true) {
return transform(eson, function (value, path) {
return ((value._meta.type === 'Array' || value._meta.type === 'Object') && filterCallback(path))
return ((value[META].type === 'Array' || value[META].type === 'Object') && filterCallback(path))
? expandOne(value, [], expanded)
: value
})
@ -332,7 +316,7 @@ export function expand (eson, filterCallback, expanded = true) {
* @return {ESON}
*/
export function expandOne (eson, path, expanded = true) {
return setIn(eson, path.concat(['_meta', 'expanded']), expanded)
return setIn(eson, path.concat([META, 'expanded']), expanded)
}
/**
@ -371,7 +355,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([META, 'error']), error)
}, eson)
// cleanup any old error messages
@ -393,8 +377,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[META][field] && !pathsMap[compileJSONPointer(path)])
? deleteIn(value, [META, field])
: value
})
}
@ -420,20 +404,20 @@ export function search (eson, text) {
if (typeof prop === 'string' && text !== '' && containsCaseInsensitive(prop, text)) {
const searchState = isEmpty(matches) ? 'active' : 'normal'
matches.push({path, area: 'property'})
updatedValue = setIn(updatedValue, ['_meta', 'searchProperty'], searchState)
updatedValue = setIn(updatedValue, [META, 'searchProperty'], searchState)
}
else {
updatedValue = deleteIn(updatedValue, ['_meta', 'searchProperty'])
updatedValue = deleteIn(updatedValue, [META, 'searchProperty'])
}
// check value
if (value._meta.type === 'value' && text !== '' && containsCaseInsensitive(value._meta.value, text)) {
if (value[META].type === 'value' && text !== '' && containsCaseInsensitive(value[META].value, text)) {
const searchState = isEmpty(matches) ? 'active' : 'normal'
matches.push({path, area: 'value'})
updatedValue = setIn(updatedValue, ['_meta', 'searchValue'], searchState)
updatedValue = setIn(updatedValue, [META, 'searchValue'], searchState)
}
else {
updatedValue = deleteIn(updatedValue, ['_meta', 'searchValue'])
updatedValue = deleteIn(updatedValue, [META, 'searchValue'])
}
return updatedValue
@ -509,7 +493,7 @@ export function nextSearchResult (eson, matches, active) {
function setSearchStatus (eson, esonPointer, searchStatus) {
const metaProp = esonPointer.area === 'property' ? 'searchProperty': 'searchValue'
return setIn(eson, esonPointer.path.concat(['_meta', metaProp]), searchStatus)
return setIn(eson, esonPointer.path.concat([META, metaProp]), searchStatus)
}
/**
@ -523,11 +507,11 @@ export function applySelection (eson, selection) {
return cleanupMetaData(eson, 'selected')
}
else if (selection.before) {
const updatedEson = setIn(eson, selection.before.concat(['_meta', 'selected']), SELECTED_BEFORE)
const updatedEson = setIn(eson, selection.before.concat([META, 'selected']), SELECTED_BEFORE)
return cleanupMetaData(updatedEson, 'selected', [selection.before])
}
else if (selection.after) {
const updatedEson = setIn(eson, selection.after.concat(['_meta', 'selected']), SELECTED_AFTER)
const updatedEson = setIn(eson, selection.after.concat([META, 'selected']), SELECTED_AFTER)
return cleanupMetaData(updatedEson, 'selected', [selection.after])
}
else { // selection.start and selection.end
@ -541,23 +525,24 @@ export function applySelection (eson, selection) {
// TODO: simplify the update function. Use pathsFromSelection ?
if (root._meta.type === 'Object') {
const startIndex = root._meta.keys.indexOf(start)
const endIndex = root._meta.keys.indexOf(end)
if (root[META].type === 'Object') {
const startIndex = root[META].keys.indexOf(start)
const endIndex = root[META].keys.indexOf(end)
const minIndex = Math.min(startIndex, endIndex)
const maxIndex = Math.max(startIndex, endIndex) + 1 // include max index itself
const selectedProps = root._meta.keys.slice(minIndex, maxIndex)
const selectedProps = root[META].keys.slice(minIndex, maxIndex)
selectedPaths = selectedProps.map(prop => rootPath.concat(prop))
let updatedRoot = Object.assign({}, root)
let updatedObj = cloneWithSymbols(root)
selectedProps.forEach(prop => {
updatedRoot[prop] = setIn(updatedRoot[prop], ['_meta', 'selected'], prop === end ? SELECTED_END : SELECTED)
updatedObj[prop] = setIn(updatedObj[prop], [META, 'selected'],
prop === end ? SELECTED_END : SELECTED)
})
return updatedRoot
return updatedObj
}
else { // root._meta.type === 'Array'
else { // root[META].type === 'Array'
const startIndex = parseInt(start)
const endIndex = parseInt(end)
@ -567,12 +552,14 @@ export function applySelection (eson, selection) {
const selectedIndices = range(minIndex, maxIndex)
selectedPaths = selectedIndices.map(index => rootPath.concat(index))
let updatedRoot = Object.assign({}, root)
let updatedArr = root.slice()
updatedArr = cloneWithSymbols(root)
selectedIndices.forEach(index => {
updatedRoot[index] = setIn(updatedRoot[index], ['_meta', 'selected'], index === endIndex ? SELECTED_END : SELECTED)
updatedArr[index] = setIn(updatedArr[index], [META, 'selected'],
index === endIndex ? SELECTED_END : SELECTED)
})
return updatedRoot
return updatedArr
}
})

View File

@ -1,6 +1,5 @@
'use strict';
import clone from 'lodash/clone'
import { isObjectOrArray, isObject } from './typeUtils'
/**
@ -14,6 +13,40 @@ import { isObjectOrArray, isObject } from './typeUtils'
* https://github.com/mariocasciaro/object-path-immutable
*/
/**
* Shallow clone of an Object, Array, or value
* Also copies any symbols on the Objects and Arrays
* @param {*} value
* @return {*}
*/
export function cloneWithSymbols (value) {
if (Array.isArray(value)) {
// copy array items
let arr = value.slice()
// copy all symbols
Object.getOwnPropertySymbols(value).forEach(symbol => arr[symbol] = value[symbol])
return arr
}
else if (typeof value === 'object') {
// copy properties
let obj = {}
for (let prop in value) {
if (value.hasOwnProperty(prop)) {
obj[prop] = value[prop]
}
}
// copy all symbols
Object.getOwnPropertySymbols(value).forEach(symbol => obj[symbol] = value[symbol])
return obj
}
else {
return value
}
}
/**
* helper function to get a nested property in an object or array
@ -66,7 +99,7 @@ export function setIn (object, path, value) {
return object
}
else {
const updatedObject = clone(object)
const updatedObject = cloneWithSymbols(object)
updatedObject[key] = updatedValue
return updatedObject
}
@ -97,7 +130,7 @@ export function updateIn (object, path, callback) {
return object
}
else {
const updatedObject = clone(object)
const updatedObject = cloneWithSymbols(object)
updatedObject[key] = updatedValue
return updatedObject
}
@ -127,7 +160,7 @@ export function deleteIn (object, path) {
return object
}
else {
const updatedObject = clone(object)
const updatedObject = cloneWithSymbols(object)
if (Array.isArray(updatedObject)) {
updatedObject.splice(key, 1)
@ -147,7 +180,7 @@ export function deleteIn (object, path) {
return object
}
else {
const updatedObject = clone(object)
const updatedObject = cloneWithSymbols(object)
updatedObject[key] = updatedValue
return updatedObject
}

View File

@ -2,15 +2,18 @@ import { readFileSync } from 'fs'
import test from 'ava'
import { setIn, getIn, deleteIn } from '../src/utils/immutabilityHelpers'
import {
META,
esonToJson, toEsonPath, toJsonPath, pathExists, transform, traverse,
parseJSONPointer, compileJSONPointer,
jsonToEson,
expand, expandOne, expandPath, applyErrors, search, nextSearchResult,
previousSearchResult,
applySelection, pathsFromSelection,
SELECTED, SELECTED_END, spliceEsonArray
SELECTED, SELECTED_END
} from '../src/eson'
import deepMap from "deep-map/lib/index"
import 'console.table'
import lodashTransform from 'lodash/transform'
import repeat from 'lodash/repeat'
const JSON1 = loadJSON('./resources/json1.json')
const ESON1 = loadJSON('./resources/eson1.json')
@ -39,22 +42,23 @@ test('toJsonPath', t => {
})
test('jsonToEson', t => {
t.deepEqual(replaceIds2(jsonToEson(1)), {_meta: {id: '[ID]', path: [], type: 'value', value: 1}})
t.deepEqual(replaceIds2(jsonToEson("foo")), {_meta: {id: '[ID]', path: [], type: 'value', value: "foo"}})
t.deepEqual(replaceIds2(jsonToEson(null)), {_meta: {id: '[ID]', path: [], type: 'value', value: null}})
t.deepEqual(replaceIds2(jsonToEson(false)), {_meta: {id: '[ID]', path: [], type: 'value', value: false}})
t.deepEqual(replaceIds2(jsonToEson({a:1, b: 2})), {
_meta: {id: '[ID]', path: [], type: 'Object', keys: ['a', 'b']},
a: {_meta: {id: '[ID]', path: ['a'], type: 'value', value: 1}},
b: {_meta: {id: '[ID]', path: ['b'], type: 'value', value: 2}}
assertDeepEqualEson(t, jsonToEson(1), {[META]: {id: '[ID]', path: [], type: 'value', value: 1}})
assertDeepEqualEson(t, jsonToEson("foo"), {[META]: {id: '[ID]', path: [], type: 'value', value: "foo"}})
assertDeepEqualEson(t, jsonToEson(null), {[META]: {id: '[ID]', path: [], type: 'value', value: null}})
assertDeepEqualEson(t, jsonToEson(false), {[META]: {id: '[ID]', path: [], type: 'value', value: false}})
assertDeepEqualEson(t, jsonToEson({a:1, b: 2}), {
[META]: {id: '[ID]', path: [], type: 'Object', keys: ['a', 'b']},
a: {[META]: {id: '[ID]', path: ['a'], type: 'value', value: 1}},
b: {[META]: {id: '[ID]', path: ['b'], type: 'value', value: 2}}
})
// printJSON(replaceIds2(jsonToEson([1,2])))
t.deepEqual(replaceIds2(jsonToEson([1,2])), {
_meta: {id: '[ID]', path: [], type: 'Array', length: 2},
0: {_meta: {id: '[ID]', path: [0], type: 'value', value: 1}},
1: {_meta: {id: '[ID]', path: [1], type: 'value', value: 2}}
})
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(t, actual, expected)
})
test('esonToJson', t => {
@ -73,12 +77,12 @@ test('expand a single path', t => {
const path = ['obj', 'arr', 2]
const collapsed = expandOne(eson, path, false)
t.is(collapsed.obj.arr[2]._meta.expanded, false)
t.deepEqual(deleteIn(collapsed, path.concat(['_meta', 'expanded'])), eson)
t.is(collapsed.obj.arr[2][META].expanded, false)
assertDeepEqualEson(t, deleteIn(collapsed, path.concat([META, 'expanded'])), eson)
const expanded = expandOne(eson, path, true)
t.is(expanded.obj.arr[2]._meta.expanded, true)
t.deepEqual(deleteIn(expanded, path.concat(['_meta', 'expanded'])), eson)
t.is(expanded.obj.arr[2][META].expanded, true)
assertDeepEqualEson(t, deleteIn(expanded, path.concat([META, 'expanded'])), eson)
})
test('expand all objects/arrays on a path', t => {
@ -94,24 +98,24 @@ test('expand all objects/arrays on a path', t => {
const path = ['obj', 'arr', 2]
const collapsed = expandPath(eson, path, false)
t.is(collapsed._meta.expanded, false)
t.is(collapsed.obj._meta.expanded, false)
t.is(collapsed.obj.arr._meta.expanded, false)
t.is(collapsed.obj.arr[2]._meta.expanded, false)
t.is(collapsed[META].expanded, false)
t.is(collapsed.obj[META].expanded, false)
t.is(collapsed.obj.arr[META].expanded, false)
t.is(collapsed.obj.arr[2][META].expanded, false)
const expanded = expandPath(eson, path, true)
t.is(expanded._meta.expanded, true)
t.is(expanded.obj._meta.expanded, true)
t.is(expanded.obj.arr._meta.expanded, true)
t.is(expanded.obj.arr[2]._meta.expanded, true)
t.is(expanded[META].expanded, true)
t.is(expanded.obj[META].expanded, true)
t.is(expanded.obj.arr[META].expanded, true)
t.is(expanded.obj.arr[2][META].expanded, 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']))
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']))
t.deepEqual(orig, eson)
assertDeepEqualEson(t, orig, eson)
})
test('expand a callback', t => {
@ -129,15 +133,16 @@ test('expand a callback', t => {
}
const expandedValue = false
const collapsed = expand(eson, filterCallback, expandedValue)
t.is(collapsed.obj.arr._meta.expanded, expandedValue)
t.is(collapsed.obj.arr._meta.expanded, expandedValue)
t.is(collapsed.obj.arr[2]._meta.expanded, expandedValue)
t.is(collapsed[META].expanded, undefined)
t.is(collapsed.obj[META].expanded, expandedValue)
t.is(collapsed.obj.arr[META].expanded, expandedValue)
t.is(collapsed.obj.arr[2][META].expanded, expandedValue)
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']))
t.deepEqual(orig, eson)
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(t, orig, eson)
})
test('expand a callback should not change the object when nothing happens', t => {
@ -154,7 +159,7 @@ test('expand a callback should not change the object when nothing happens', t =>
test('transform (no change)', t => {
const eson = jsonToEson({a: [1,2,3], b: {c: 4}})
const updated = transform(eson, (value, path) => value)
t.deepEqual(updated, eson)
assertDeepEqualEson(t, updated, eson)
t.is(updated, eson)
})
@ -162,13 +167,10 @@ test('transform (change based on value)', t => {
const eson = jsonToEson({a: [1,2,3], b: {c: 4}})
const updated = transform(eson,
(value, path) => value._meta.value === 2 ? jsonToEson(20, path) : value)
(value, path) => value[META].value === 2 ? jsonToEson(20, path) : value)
const expected = jsonToEson({a: [1,20,3], b: {c: 4}})
replaceIds(updated)
replaceIds(expected)
t.deepEqual(updated, expected)
assertDeepEqualEson(t, updated, expected)
t.is(updated.b, eson.b) // should not have replaced b
})
@ -179,9 +181,7 @@ test('transform (change based on path)', t => {
(value, path) => path.join('.') === 'a.1' ? jsonToEson(20, path) : value)
const expected = jsonToEson({a: [1,20,3], b: {c: 4}})
replaceIds(updated)
replaceIds(expected)
t.deepEqual(updated, expected)
assertDeepEqualEson(t, updated, expected)
t.is(updated.b, eson.b) // should not have replaced b
})
@ -226,9 +226,9 @@ test('add and remove errors', t => {
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])
t.deepEqual(actual1, expected)
expected = setIn(expected, ['obj', 'arr', '2', 'last', META, 'error'], jsonSchemaErrors[0])
expected = setIn(expected, ['nill', META, 'error'], jsonSchemaErrors[1])
assertDeepEqualEson(t, actual1, expected)
// re-applying the same errors should not change eson
const actual2 = applyErrors(actual1, jsonSchemaErrors)
@ -236,7 +236,7 @@ test('add and remove errors', t => {
// clear errors
const actual3 = applyErrors(actual2, [])
t.deepEqual(actual3, eson)
assertDeepEqualEson(t, actual3, eson)
t.is(actual3.str, eson.str) // shouldn't have touched values not affected by the errors
})
@ -292,14 +292,14 @@ test('search', t => {
t.deepEqual(active, {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', 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')
t.deepEqual(esonWithSearch, expected)
assertDeepEqualEson(t, esonWithSearch, expected)
})
test('nextSearchResult', t => {
@ -320,27 +320,27 @@ test('nextSearchResult', t => {
])
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, ['bool', '_meta', 'searchValue']), 'normal')
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, ['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.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, ['bool', '_meta', 'searchValue']), 'normal')
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, ['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, ['bool', '_meta', 'searchValue']), 'active')
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, ['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, ['bool', '_meta', 'searchValue']), 'normal')
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, ['bool', META, 'searchValue']), 'normal')
})
test('previousSearchResult', t => {
@ -361,27 +361,27 @@ test('previousSearchResult', t => {
])
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, ['bool', '_meta', 'searchValue']), 'normal')
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, ['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, ['bool', '_meta', 'searchValue']), 'active')
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, ['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.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, ['bool', '_meta', 'searchValue']), 'normal')
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, ['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, ['bool', '_meta', 'searchValue']), 'normal')
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, ['bool', META, 'searchValue']), 'normal')
})
test('selection (object)', t => {
@ -401,10 +401,10 @@ test('selection (object)', t => {
const actual = applySelection(eson, selection)
let expected = eson
expected = setIn(expected, ['obj', '_meta', 'selected'], SELECTED)
expected = setIn(expected, ['str', '_meta', 'selected'], SELECTED)
expected = setIn(expected, ['nill', '_meta', 'selected'], SELECTED_END)
t.deepEqual(actual, expected)
expected = setIn(expected, ['obj', META, 'selected'], SELECTED)
expected = setIn(expected, ['str', META, 'selected'], SELECTED)
expected = setIn(expected, ['nill', META, 'selected'], SELECTED_END)
assertDeepEqualEson(t, actual, expected)
// test whether old selection results are cleaned up
const selection2 = {
@ -413,9 +413,9 @@ test('selection (object)', t => {
}
const actual2 = applySelection(actual, selection2)
let expected2 = eson
expected2 = setIn(expected2, ['nill', '_meta', 'selected'], SELECTED)
expected2 = setIn(expected2, ['bool', '_meta', 'selected'], SELECTED_END)
t.deepEqual(actual2, expected2)
expected2 = setIn(expected2, ['nill', META, 'selected'], SELECTED)
expected2 = setIn(expected2, ['bool', META, 'selected'], SELECTED_END)
assertDeepEqualEson(t, actual2, expected2)
})
test('selection (array)', t => {
@ -435,10 +435,10 @@ test('selection (array)', t => {
const actual = applySelection(eson, selection)
let expected = eson
expected = setIn(expected, ['obj', 'arr', '0', '_meta', 'selected'], SELECTED_END)
expected = setIn(expected, ['obj', 'arr', '1', '_meta', 'selected'], SELECTED)
expected = setIn(expected, ['obj', 'arr', '0', META, 'selected'], SELECTED_END)
expected = setIn(expected, ['obj', 'arr', '1', META, 'selected'], SELECTED)
t.deepEqual(actual, expected)
assertDeepEqualEson(t, actual, expected)
})
test('selection (value)', t => {
@ -456,8 +456,8 @@ test('selection (value)', t => {
}
const actual = applySelection(eson, selection)
const expected = setIn(eson, ['obj', 'arr', '2', 'first', '_meta', 'selected'], SELECTED_END)
t.deepEqual(actual, expected)
const expected = setIn(eson, ['obj', 'arr', '2', 'first', META, 'selected'], SELECTED_END)
assertDeepEqualEson(t, actual, expected)
})
test('selection (node)', t => {
@ -475,8 +475,8 @@ test('selection (node)', t => {
}
const actual = applySelection(eson, selection)
const expected = setIn(eson, ['obj', 'arr', '_meta', 'selected'], SELECTED_END)
t.deepEqual(actual, expected)
const expected = setIn(eson, ['obj', 'arr', META, 'selected'], SELECTED_END)
assertDeepEqualEson(t, actual, expected)
})
test('pathsFromSelection (object)', t => {
@ -531,31 +531,34 @@ test('pathsFromSelection (after)', t => {
t.deepEqual(pathsFromSelection(ESON1, selection), [])
})
test('spliceEsonArray', t => {
const eson = jsonToEson([1,2,3])
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(replaceIds(spliceEsonArray(eson, 1, 1)), replaceIds(jsonToEson([1,3])))
t.deepEqual(replaceIds(spliceEsonArray(eson, 1, 5)), replaceIds(jsonToEson([1])))
t.deepEqual(replaceIds(spliceEsonArray(eson, 1, 0, [10])), replaceIds(jsonToEson([1,5,3])))
t.deepEqual(replaceIds(spliceEsonArray(eson, 1, 0, [10,20])), replaceIds(jsonToEson([1,10,20,3])))
})
t.deepEqual(actualMeta, expectedMeta, `Meta data not equal, path=[${path.join(', ')}]`)
// helper function to replace all id properties with a constant value
function replaceIds (eson, value = '[ID]') {
eson._meta.id = value
if (eson._meta.type === 'Object' || eson._meta.type === 'Array') {
for (let key in eson) {
if (eson.hasOwnProperty(key) && key !== '_meta') {
replaceIds(eson[key], value)
}
}
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(\', \')}]')
}
}
// helper function to replace all id properties with a constant value
function replaceIds2 (data, key = 'id', value = '[ID]') {
return deepMap(data, (v, k) => k === key ? value : v)
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
@ -566,6 +569,33 @@ function printJSON (json, message = null) {
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'))