diff --git a/package-lock.json b/package-lock.json index 692e87e..6d92c06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 657937e..f6ab2e7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/eson.js b/src/eson.js index 4cc12a5..d89efb1 100644 --- a/src/eson.js +++ b/src/eson.js @@ -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 } }) diff --git a/src/utils/immutabilityHelpers.js b/src/utils/immutabilityHelpers.js index 7d4db0b..e673d19 100644 --- a/src/utils/immutabilityHelpers.js +++ b/src/utils/immutabilityHelpers.js @@ -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 } diff --git a/test/eson.test.js b/test/eson.test.js index 1088026..0618c36 100644 --- a/test/eson.test.js +++ b/test/eson.test.js @@ -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'))