immutabilityHelpers don't accept non-existing paths

This commit is contained in:
jos 2016-09-09 11:43:36 +02:00
parent fe0f98dfe0
commit bb8850fb91
6 changed files with 83 additions and 132 deletions

View File

@ -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'

View File

@ -17,7 +17,7 @@
* value: *?
* }} ValueData
*
* @typedef {Array.<string | number>} Path
* @typedef {Array.<string>} Path
*
* @typedef {ObjectData | ArrayData | ValueData} JSONData
*

View File

@ -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))
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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)
})