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.
|
* 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'
|
||||||
|
|
|
@ -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
|
||||||
*
|
*
|
||||||
|
|
|
@ -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 (!isObjectOrArray(object)) {
|
||||||
|
return object
|
||||||
|
}
|
||||||
|
|
||||||
if (path.length === 1) {
|
if (path.length === 1) {
|
||||||
const key = path[0]
|
const key = path[0]
|
||||||
const updated = clone(object)
|
if (object[key] === undefined) {
|
||||||
if (Array.isArray(updated)) {
|
// key doesn't exist. return object unchanged
|
||||||
updated.splice(key, 1)
|
return object
|
||||||
}
|
}
|
||||||
else {
|
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 key = path[0]
|
||||||
const child = object[key]
|
const updatedValue = deleteIn(object[key], path.slice(1))
|
||||||
if (Array.isArray(child) || isObject(child)) {
|
if (object[key] === updatedValue) {
|
||||||
const updated = clone(object)
|
// object is unchanged
|
||||||
updated[key] = deleteIn(child, path.slice(1))
|
|
||||||
return updated
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// child property doesn't exist. just do nothing
|
|
||||||
return object
|
return object
|
||||||
}
|
}
|
||||||
}
|
else {
|
||||||
|
const updatedObject = clone(object)
|
||||||
/**
|
updatedObject[key] = updatedValue
|
||||||
* Helper function to clone an array or object, or to create a new object
|
return updatedObject
|
||||||
* 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
* Get the type of a value
|
||||||
* @param {*} value
|
* @param {*} value
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
Loading…
Reference in New Issue