diff --git a/src/jsonData.js b/src/jsonData.js index d1343f7..73912c0 100644 --- a/src/jsonData.js +++ b/src/jsonData.js @@ -3,10 +3,9 @@ * All functions are pure and don't mutate the JSONData. */ -import { isObject } from './utils/objectUtils' import { setIn, updateIn, getIn, deleteIn } from './utils/immutabilityHelpers' import { compareAsc, compareDesc } from './utils/arrayUtils' -import { stringConvert } from './utils/typeUtils' +import { isObject, stringConvert } from './utils/typeUtils' import { findUniqueName } from './utils/stringUtils' import isEqual from 'lodash/isEqual' import cloneDeep from 'lodash/isEqual' diff --git a/src/typedef.js b/src/typedef.js index 116bbe1..2a5f164 100644 --- a/src/typedef.js +++ b/src/typedef.js @@ -17,7 +17,7 @@ * value: *? * }} ValueData * - * @typedef {Array.} Path + * @typedef {Array.} Path * * @typedef {ObjectData | ArrayData | ValueData} JSONData * diff --git a/src/utils/immutabilityHelpers.js b/src/utils/immutabilityHelpers.js index ce6091d..e8eea5e 100644 --- a/src/utils/immutabilityHelpers.js +++ b/src/utils/immutabilityHelpers.js @@ -1,6 +1,7 @@ 'use strict'; -import { isObject, clone } from './objectUtils' +import clone from 'lodash/clone' +import { isObjectOrArray } from './typeUtils' /** * Immutability helpers @@ -26,7 +27,7 @@ export function getIn (object, path) { let i = 0 while(i < path.length) { - if (Array.isArray(value) || isObject(value)) { + if (isObjectOrArray(value)) { value = value[path[i]] } else { @@ -53,17 +54,20 @@ export function setIn (object, path, value) { return value } - const key = path[0] - const updated = cloneOrCreate(key, object) + if (!isObjectOrArray(object)) { + throw new Error('Path does not exist') + } - const updatedValue = setIn(updated[key], path.slice(1), value) - if (updated[key] === updatedValue) { + const key = path[0] + const updatedValue = setIn(object[key], path.slice(1), value) + if (object[key] === updatedValue) { // return original object unchanged when the new value is identical to the old one return object } else { - updated[key] = updatedValue - return updated + const updatedObject = clone(object) + updatedObject[key] = updatedValue + return updatedObject } } /** @@ -80,17 +84,20 @@ export function updateIn (object, path, callback) { return callback(object) } - const key = path[0] - const updated = cloneOrCreate(key, object) + if (!isObjectOrArray(object)) { + throw new Error('Path doesn\'t exist') + } + const key = path[0] const updatedValue = updateIn(object[key], path.slice(1), callback) - if (updated[key] === updatedValue) { + if (object[key] === updatedValue) { // return original object unchanged when the new value is identical to the old one return object } else { - updated[key] = updatedValue - return updated + const updatedObject = clone(object) + updatedObject[key] = updatedValue + return updatedObject } } @@ -107,48 +114,39 @@ export function deleteIn (object, path) { return object } + if (!isObjectOrArray(object)) { + return object + } + if (path.length === 1) { const key = path[0] - const updated = clone(object) - if (Array.isArray(updated)) { - updated.splice(key, 1) + if (object[key] === undefined) { + // key doesn't exist. return object unchanged + return object } else { - delete updated[key] - } + const updatedObject = clone(object) - return updated + if (Array.isArray(updatedObject)) { + updatedObject.splice(key, 1) + } + else { + delete updatedObject[key] + } + + return updatedObject + } } const key = path[0] - const child = object[key] - if (Array.isArray(child) || isObject(child)) { - const updated = clone(object) - updated[key] = deleteIn(child, path.slice(1)) - return updated - } - else { - // child property doesn't exist. just do nothing + const updatedValue = deleteIn(object[key], path.slice(1)) + if (object[key] === updatedValue) { + // object is unchanged return object } -} - -/** - * Helper function to clone an array or object, or to create a new object - * when `object` is undefined. When object is anything else, the function will - * throw an error - * @param {string | number} key - * @param {Object | Array | undefined} object - * @return {Array | Object} - */ -function cloneOrCreate (key, object) { - if (object === undefined) { - return (typeof key === 'number') ? [] : {} // create new object or array + else { + const updatedObject = clone(object) + updatedObject[key] = updatedValue + return updatedObject } - - if (typeof object === 'object' || Array.isArray(object)) { - return clone(object) - } - - throw new Error('Cannot override existing property ' + JSON.stringify(object)) } diff --git a/src/utils/objectUtils.js b/src/utils/objectUtils.js deleted file mode 100644 index 467b55f..0000000 --- a/src/utils/objectUtils.js +++ /dev/null @@ -1,67 +0,0 @@ - -// TODO: unit test isObject - -/** - * Test whether a value is an object (and not an Array or Date or primitive value) - * - * @param {*} value - * @return {boolean} - */ -export function isObject (value) { - return typeof value === 'object' && - value !== null && - !Array.isArray(value) && - value.toString() === '[object Object]' -} - -// TODO: unit test clone - -/** - * Flat clone the properties of an object or array - * @param {Object | Array} value - * @return {Object | Array} Returns a flat clone of the object or Array - */ -export function clone (value) { - if (Array.isArray(value)) { - return value.slice(0) - } - else if (isObject(value)) { - const cloned = {} - - Object.keys(value).forEach(key => { - cloned[key] = value[key] - }) - - return cloned - } - else { - // a primitive value - return value - } -} - -// TODO: test cloneDeep - -/** - * Deep clone the properties of an object or array - * @param {Object | Array} value - * @return {Object | Array} Returns a deep clone of the object or Array - */ -export function cloneDeep (value) { - if (Array.isArray(value)) { - return value.map(cloneDeep) - } - else if (isObject(value)) { - const cloned = {} - - Object.keys(value).forEach(key => { - cloned[key] = cloneDeep(value[key]) - }) - - return cloned - } - else { - // a primitive value - return value - } -} diff --git a/src/utils/typeUtils.js b/src/utils/typeUtils.js index f8bdac7..0534d89 100644 --- a/src/utils/typeUtils.js +++ b/src/utils/typeUtils.js @@ -1,4 +1,28 @@ +// TODO: unit test isObject + +/** + * Test whether a value is an Object (and not an Array!) + * + * @param {*} value + * @return {boolean} + */ +export function isObject (value) { + return typeof value === 'object' && + value !== null && + !Array.isArray(value) +} + +/** + * Test whether a value is an Object or an Array + * + * @param {*} value + * @return {boolean} + */ +export function isObjectOrArray (value) { + return typeof value === 'object' && value !== null +} + /** * Get the type of a value * @param {*} value diff --git a/test/immutabilityHelpers.test.js b/test/immutabilityHelpers.test.js index 2362215..8d026ed 100644 --- a/test/immutabilityHelpers.test.js +++ b/test/immutabilityHelpers.test.js @@ -20,8 +20,8 @@ test('getIn', t => { } t.deepEqual(getIn(obj, ['a', 'b']), {c: 2}) - t.is(getIn(obj, ['e', 1, 'f']), 5) - t.is(getIn(obj, ['e', 999, 'f']), undefined) + t.is(getIn(obj, ['e', '1', 'f']), 5) + t.is(getIn(obj, ['e', '999', 'f']), undefined) t.is(getIn(obj, ['non', 'existing', 'path']), undefined) }) @@ -61,15 +61,7 @@ test('setIn basic', t => { test('setIn non existing path', t => { const obj = {} - const updated = setIn(obj, ['a', 'b', 'c'], 4) - - t.deepEqual (updated, { - a: { - b: { - c: 4 - } - } - }) + t.throws(() => setIn(obj, ['a', 'b', 'c'], 4), /Path does not exist/) }) test('setIn replace value with object should throw an exception', t => { @@ -78,9 +70,7 @@ test('setIn replace value with object should throw an exception', t => { d: 3 } - t.throws(function () { - const updated = setIn(obj, ['a', 'b', 'c'], 4) - }, /Cannot override existing property/) + t.throws(() => setIn(obj, ['a', 'b', 'c'], 4), /Path does not exist/) }) test('setIn replace value inside nested array', t => { @@ -96,7 +86,7 @@ test('setIn replace value inside nested array', t => { d: 5 } - const updated = setIn(obj, ['a', 2, 'c'], 8) + const updated = setIn(obj, ['a', '2', 'c'], 8) t.deepEqual (updated, { a: [ @@ -254,7 +244,7 @@ test('deleteIn array', t => { e: 5 } - const updated = deleteIn(obj, ['a', 'b', 1, 'c']) + const updated = deleteIn(obj, ['a', 'b', '1', 'c']) t.deepEqual (updated, { a: { b: [1, {d: 3} , 4] @@ -271,4 +261,11 @@ test('deleteIn array', t => { }) t.truthy (obj !== updated) +}) + +test('deleteIn non existing path', t => { + const obj = { a: {}} + + const updated = deleteIn(obj, ['a', 'b']) + t.truthy (updated === obj) }) \ No newline at end of file