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. * All functions are pure and don't mutate the JSONData.
*/ */
import { isObject } from './utils/objectUtils'
import { setIn, updateIn, getIn, deleteIn } from './utils/immutabilityHelpers' import { setIn, updateIn, getIn, deleteIn } from './utils/immutabilityHelpers'
import { compareAsc, compareDesc } from './utils/arrayUtils' import { compareAsc, compareDesc } from './utils/arrayUtils'
import { stringConvert } from './utils/typeUtils' import { isObject, stringConvert } from './utils/typeUtils'
import { findUniqueName } from './utils/stringUtils' import { findUniqueName } from './utils/stringUtils'
import isEqual from 'lodash/isEqual' import isEqual from 'lodash/isEqual'
import cloneDeep from 'lodash/isEqual' import cloneDeep from 'lodash/isEqual'

View File

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

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
import { isObject, clone } from './objectUtils' import clone from 'lodash/clone'
import { isObjectOrArray } from './typeUtils'
/** /**
* Immutability helpers * Immutability helpers
@ -26,7 +27,7 @@ export function getIn (object, path) {
let i = 0 let i = 0
while(i < path.length) { while(i < path.length) {
if (Array.isArray(value) || isObject(value)) { if (isObjectOrArray(value)) {
value = value[path[i]] value = value[path[i]]
} }
else { else {
@ -53,17 +54,20 @@ export function setIn (object, path, value) {
return value return value
} }
const key = path[0] if (!isObjectOrArray(object)) {
const updated = cloneOrCreate(key, object) throw new Error('Path does not exist')
}
const updatedValue = setIn(updated[key], path.slice(1), value) const key = path[0]
if (updated[key] === updatedValue) { 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 original object unchanged when the new value is identical to the old one
return object return object
} }
else { else {
updated[key] = updatedValue const updatedObject = clone(object)
return updated updatedObject[key] = updatedValue
return updatedObject
} }
} }
/** /**
@ -80,17 +84,20 @@ export function updateIn (object, path, callback) {
return callback(object) return callback(object)
} }
const key = path[0] if (!isObjectOrArray(object)) {
const updated = cloneOrCreate(key, object) throw new Error('Path doesn\'t exist')
}
const key = path[0]
const updatedValue = updateIn(object[key], path.slice(1), callback) 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 original object unchanged when the new value is identical to the old one
return object return object
} }
else { else {
updated[key] = updatedValue const updatedObject = clone(object)
return updated updatedObject[key] = updatedValue
return updatedObject
} }
} }
@ -107,48 +114,39 @@ export function deleteIn (object, path) {
return object return object
} }
if (path.length === 1) { if (!isObjectOrArray(object)) {
const key = path[0]
const updated = clone(object)
if (Array.isArray(updated)) {
updated.splice(key, 1)
}
else {
delete updated[key]
}
return updated
}
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
return object return object
} }
}
/** if (path.length === 1) {
* Helper function to clone an array or object, or to create a new object const key = path[0]
* when `object` is undefined. When object is anything else, the function will if (object[key] === undefined) {
* throw an error // key doesn't exist. return object unchanged
* @param {string | number} key return object
* @param {Object | Array | undefined} object }
* @return {Array | Object} else {
*/ const updatedObject = clone(object)
function cloneOrCreate (key, object) {
if (object === undefined) { if (Array.isArray(updatedObject)) {
return (typeof key === 'number') ? [] : {} // create new object or array updatedObject.splice(key, 1)
}
else {
delete updatedObject[key]
} }
if (typeof object === 'object' || Array.isArray(object)) { return updatedObject
return clone(object) }
} }
throw new Error('Cannot override existing property ' + JSON.stringify(object)) const key = path[0]
const updatedValue = deleteIn(object[key], path.slice(1))
if (object[key] === updatedValue) {
// object is unchanged
return object
}
else {
const updatedObject = clone(object)
updatedObject[key] = updatedValue
return updatedObject
}
} }

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 * Get the type of a value
* @param {*} value * @param {*} value

View File

@ -20,8 +20,8 @@ test('getIn', t => {
} }
t.deepEqual(getIn(obj, ['a', 'b']), {c: 2}) t.deepEqual(getIn(obj, ['a', 'b']), {c: 2})
t.is(getIn(obj, ['e', 1, 'f']), 5) t.is(getIn(obj, ['e', '1', 'f']), 5)
t.is(getIn(obj, ['e', 999, 'f']), undefined) t.is(getIn(obj, ['e', '999', 'f']), undefined)
t.is(getIn(obj, ['non', 'existing', 'path']), undefined) t.is(getIn(obj, ['non', 'existing', 'path']), undefined)
}) })
@ -61,15 +61,7 @@ test('setIn basic', t => {
test('setIn non existing path', t => { test('setIn non existing path', t => {
const obj = {} const obj = {}
const updated = setIn(obj, ['a', 'b', 'c'], 4) t.throws(() => setIn(obj, ['a', 'b', 'c'], 4), /Path does not exist/)
t.deepEqual (updated, {
a: {
b: {
c: 4
}
}
})
}) })
test('setIn replace value with object should throw an exception', t => { 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 d: 3
} }
t.throws(function () { t.throws(() => setIn(obj, ['a', 'b', 'c'], 4), /Path does not exist/)
const updated = setIn(obj, ['a', 'b', 'c'], 4)
}, /Cannot override existing property/)
}) })
test('setIn replace value inside nested array', t => { test('setIn replace value inside nested array', t => {
@ -96,7 +86,7 @@ test('setIn replace value inside nested array', t => {
d: 5 d: 5
} }
const updated = setIn(obj, ['a', 2, 'c'], 8) const updated = setIn(obj, ['a', '2', 'c'], 8)
t.deepEqual (updated, { t.deepEqual (updated, {
a: [ a: [
@ -254,7 +244,7 @@ test('deleteIn array', t => {
e: 5 e: 5
} }
const updated = deleteIn(obj, ['a', 'b', 1, 'c']) const updated = deleteIn(obj, ['a', 'b', '1', 'c'])
t.deepEqual (updated, { t.deepEqual (updated, {
a: { a: {
b: [1, {d: 3} , 4] b: [1, {d: 3} , 4]
@ -272,3 +262,10 @@ test('deleteIn array', t => {
t.truthy (obj !== updated) t.truthy (obj !== updated)
}) })
test('deleteIn non existing path', t => {
const obj = { a: {}}
const updated = deleteIn(obj, ['a', 'b'])
t.truthy (updated === obj)
})