Skip to content

Commit 0a7daf0

Browse files
committed
Add setIn helper
1 parent f44c16d commit 0a7daf0

File tree

2 files changed

+185
-0
lines changed

2 files changed

+185
-0
lines changed

src/set-in.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// -----------------------------------------------------------------------------
2+
// Deps
3+
// -----------------------------------------------------------------------------
4+
5+
import { setIn } from './set-in';
6+
import { IPlainObject } from './types';
7+
8+
// -----------------------------------------------------------------------------
9+
// Tests
10+
// -----------------------------------------------------------------------------
11+
12+
const testCases: {
13+
parameters: Parameters<typeof setIn>;
14+
result: IPlainObject;
15+
}[] = [
16+
{
17+
parameters: [{}, 'key', 'value'],
18+
result: { key: 'value' }
19+
},
20+
{
21+
parameters: [{}, 'key1.key2.key3.key4', 'value'],
22+
result: {
23+
key1: {
24+
key2: {
25+
key3: {
26+
key4: 'value'
27+
}
28+
}
29+
}
30+
}
31+
},
32+
{
33+
parameters: [
34+
{
35+
key1: {
36+
key2: {
37+
xxx: true,
38+
zzz: false
39+
}
40+
}
41+
},
42+
'key1.key2.key3.key4',
43+
'value'
44+
],
45+
result: {
46+
key1: {
47+
key2: {
48+
xxx: true,
49+
zzz: false,
50+
key3: {
51+
key4: 'value'
52+
}
53+
}
54+
}
55+
}
56+
},
57+
{
58+
parameters: [{ list: [] }, 'list[2]', 'value'],
59+
result: {
60+
list: [undefined, undefined, 'value']
61+
}
62+
},
63+
{
64+
parameters: [{}, 'list[2]', 'value'],
65+
result: {
66+
list: [undefined, undefined, 'value']
67+
}
68+
},
69+
{
70+
parameters: [{}, 'key1[0].key3', 'value'],
71+
result: {
72+
key1: [
73+
{
74+
key3: 'value'
75+
}
76+
]
77+
}
78+
}
79+
];
80+
81+
describe('setIn tool', () => {
82+
testCases.forEach(({ parameters, result }, i) => {
83+
test(`test #${i + 1}: ${parameters.join(', ')}`, () => {
84+
if (typeof result === 'string') {
85+
} else {
86+
setIn(...parameters);
87+
const _expect = JSON.stringify(parameters[0], undefined, '\t');
88+
const _result = JSON.stringify(result, undefined, '\t');
89+
expect(_expect).toStrictEqual(_result);
90+
}
91+
});
92+
});
93+
});
94+
95+
// -----------------------------------------------------------------------------
96+
// Error handling
97+
// -----------------------------------------------------------------------------
98+
99+
const errorCases: {
100+
parameters: Parameters<typeof setIn>;
101+
error: string | RegExp;
102+
}[] = [
103+
{
104+
parameters: [{ list: [] }, 'list.item', 'value'],
105+
error: /must be an array index \(correct int\)/
106+
}
107+
];
108+
109+
describe('setIn tool: error handling', () => {
110+
errorCases.forEach(({ parameters, error }, i) => {
111+
test(`error #${i + 1}: ${error}`, () => {
112+
expect(() => setIn(...parameters)).toThrow(error);
113+
});
114+
});
115+
});

src/set-in.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// -----------------------------------------------------------------------------
2+
// Deps
3+
// -----------------------------------------------------------------------------
4+
5+
import { toPath } from './to-path';
6+
import { isPlainObject } from './is-plain-object';
7+
import { IPlainObject } from './types';
8+
9+
// -----------------------------------------------------------------------------
10+
// Helper
11+
// -----------------------------------------------------------------------------
12+
13+
const setInRecursive = (
14+
current: any | any[],
15+
value: any,
16+
path: (string | number)[],
17+
index: number
18+
) => {
19+
if (index >= path.length) {
20+
return;
21+
}
22+
const key = path[index];
23+
const nextIndex = index + 1;
24+
const numberKey = typeof key === 'number';
25+
const sampleIsArray = Array.isArray(current);
26+
27+
if (nextIndex === path.length) {
28+
if (sampleIsArray) {
29+
if (numberKey) {
30+
current[key] = value;
31+
} else {
32+
throw new Error(`Key \`${key}\` must be an array index (correct int)`);
33+
}
34+
} else if (isPlainObject(current)) {
35+
current[key] = value;
36+
} else {
37+
throw new Error('Last chain must be an object or array');
38+
}
39+
} else {
40+
if (isPlainObject(current)) {
41+
if (!current.hasOwnProperty(key)) {
42+
// setting inner prop based on next path index
43+
current[key] = typeof path[nextIndex] === 'number' ? [] : {};
44+
}
45+
setInRecursive(current[key], value, path, nextIndex);
46+
} else {
47+
throw new Error('`setIn()` tool ends up with wrong result');
48+
}
49+
}
50+
};
51+
52+
/**
53+
* Setting value in Object by path
54+
* Inspired by final-form's `setIn` helper
55+
* {@link https://github.com/final-form/final-form/blob/master/src/structure/setIn.js}
56+
* @param {IPlainObject} sample
57+
* @param {string} keyPath
58+
* @param {*} value
59+
*/
60+
export function setIn(sample: IPlainObject, keyPath: string, value: any) {
61+
return setInRecursive(
62+
sample,
63+
value,
64+
toPath(keyPath).map((key) => {
65+
const index = parseInt(key);
66+
return isNaN(index) ? key : index;
67+
}),
68+
0
69+
);
70+
}

0 commit comments

Comments
 (0)