immutabilityHelpers don't accept non-existing paths
This commit is contained in:
parent
fe0f98dfe0
commit
bb8850fb91
|
@ -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'
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* value: *?
|
||||
* }} ValueData
|
||||
*
|
||||
* @typedef {Array.<string | number>} Path
|
||||
* @typedef {Array.<string>} Path
|
||||
*
|
||||
* @typedef {ObjectData | ArrayData | ValueData} JSONData
|
||||
*
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
@ -272,3 +262,10 @@ 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)
|
||||
})
|
Loading…
Reference in New Issue