Skip to content

Commit d64e27b

Browse files
authoredJan 21, 2022
Support array indexes (#82)
1 parent d400c8d commit d64e27b

File tree

4 files changed

+348
-24
lines changed

4 files changed

+348
-24
lines changed
 

‎index.d.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ declare const dotProp: {
44
/**
55
Get the value of the property at the given path.
66
7-
@param object - Object to get the `path` value.
7+
@param object - Object or array to get the `path` value.
88
@param path - Path of the property in the object, using `.` to separate each nested key. Use `\\.` if you have a `.` in the key.
99
@param defaultValue - Default value.
1010
@@ -23,18 +23,21 @@ declare const dotProp: {
2323
2424
dotProp.get({foo: {'dot.dot': 'unicorn'}}, 'foo.dot\\.dot');
2525
//=> 'unicorn'
26+
27+
dotProp.get({foo: [{bar: 'unicorn'}]}, 'foo[0].bar');
28+
//=> 'unicorn'
2629
```
2730
*/
2831
get: <ObjectType, PathType extends string, DefaultValue = undefined>(
2932
object: ObjectType,
3033
path: PathType,
3134
defaultValue?: DefaultValue
32-
) => ObjectType extends Record<string, unknown> ? (Get<ObjectType, PathType> extends unknown ? DefaultValue : Get<ObjectType, PathType>) : undefined; // TODO: When adding array index support (https://github.com/sindresorhus/dot-prop/issues/71) add ` | unknown[]` after `Record<string, unknown>`
35+
) => ObjectType extends Record<string, unknown> | unknown[] ? (Get<ObjectType, PathType> extends unknown ? DefaultValue : Get<ObjectType, PathType>) : undefined;
3336

3437
/**
3538
Set the property at the given path to the given value.
3639
37-
@param object - Object to set the `path` value.
40+
@param object - Object or array to set the `path` value.
3841
@param path - Path of the property in the object, using `.` to separate each nested key. Use `\\.` if you have a `.` in the key.
3942
@param value - Value to set at `path`.
4043
@returns The object.
@@ -55,6 +58,10 @@ declare const dotProp: {
5558
dotProp.set(object, 'foo.baz', 'x');
5659
console.log(object);
5760
//=> {foo: {bar: 'b', baz: 'x'}}
61+
62+
dotProp.set(object, 'foo.biz[0]', 'a');
63+
console.log(object);
64+
//=> {foo: {bar: 'b', baz: 'x', biz: ['a']}}
5865
```
5966
*/
6067
set: <ObjectType extends {[key: string]: any}>(
@@ -66,7 +73,7 @@ declare const dotProp: {
6673
/**
6774
Check whether the property at the given path exists.
6875
69-
@param object - Object to test the `path` value.
76+
@param object - Object or array to test the `path` value.
7077
@param path - Path of the property in the object, using `.` to separate each nested key. Use `\\.` if you have a `.` in the key.
7178
7279
@example
@@ -82,7 +89,7 @@ declare const dotProp: {
8289
/**
8390
Delete the property at the given path.
8491
85-
@param object - Object to delete the `path` value.
92+
@param object - Object or array to delete the `path` value.
8693
@param path - Path of the property in the object, using `.` to separate each nested key. Use `\\.` if you have a `.` in the key.
8794
@returns A boolean of whether the property existed before being deleted.
8895

‎index.js

+148-14
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,156 @@ const disallowedKeys = new Set([
77
'constructor'
88
]);
99

10-
const isValidPath = pathSegments => !pathSegments.some(segment => disallowedKeys.has(segment));
10+
const digits = new Set('0123456789');
1111

1212
function getPathSegments(path) {
13-
const pathArray = path.split('.');
1413
const parts = [];
14+
let currentSegment = '';
15+
let currentPart = 'start';
16+
let isIgnoring = false;
17+
18+
for (const character of path) {
19+
switch (character) {
20+
case '\\':
21+
if (currentPart === 'index') {
22+
throw new Error('Invalid character in an index');
23+
}
24+
25+
if (currentPart === 'indexEnd') {
26+
throw new Error('Invalid character after an index');
27+
}
28+
29+
if (isIgnoring) {
30+
currentSegment += character;
31+
}
32+
33+
currentPart = 'property';
34+
isIgnoring = !isIgnoring;
35+
break;
1536

16-
for (let i = 0; i < pathArray.length; i++) {
17-
let p = pathArray[i];
37+
case '.':
38+
if (currentPart === 'index') {
39+
throw new Error('Invalid character in an index');
40+
}
41+
42+
if (currentPart === 'indexEnd') {
43+
currentPart = 'property';
44+
break;
45+
}
46+
47+
if (isIgnoring) {
48+
isIgnoring = false;
49+
currentSegment += character;
50+
break;
51+
}
52+
53+
if (disallowedKeys.has(currentSegment)) {
54+
return [];
55+
}
56+
57+
parts.push(currentSegment);
58+
currentSegment = '';
59+
currentPart = 'property';
60+
break;
61+
62+
case '[':
63+
if (currentPart === 'index') {
64+
throw new Error('Invalid character in an index');
65+
}
66+
67+
if (currentPart === 'indexEnd') {
68+
currentPart = 'index';
69+
break;
70+
}
71+
72+
if (isIgnoring) {
73+
isIgnoring = false;
74+
currentSegment += character;
75+
break;
76+
}
77+
78+
if (currentPart === 'property') {
79+
if (disallowedKeys.has(currentSegment)) {
80+
return [];
81+
}
82+
83+
parts.push(currentSegment);
84+
currentSegment = '';
85+
}
86+
87+
currentPart = 'index';
88+
break;
1889

19-
while (p[p.length - 1] === '\\' && pathArray[i + 1] !== undefined) {
20-
p = p.slice(0, -1) + '.';
21-
p += pathArray[++i];
90+
case ']':
91+
if (currentPart === 'index') {
92+
parts.push(Number.parseInt(currentSegment, 10));
93+
currentSegment = '';
94+
currentPart = 'indexEnd';
95+
break;
96+
}
97+
98+
if (currentPart === 'indexEnd') {
99+
throw new Error('Invalid character after an index');
100+
}
101+
102+
// Falls through
103+
104+
default:
105+
if (currentPart === 'index' && !digits.has(character)) {
106+
throw new Error('Invalid character in an index');
107+
}
108+
109+
if (currentPart === 'indexEnd') {
110+
throw new Error('Invalid character after an index');
111+
}
112+
113+
if (currentPart === 'start') {
114+
currentPart = 'property';
115+
}
116+
117+
if (isIgnoring) {
118+
isIgnoring = false;
119+
currentSegment += '\\';
120+
}
121+
122+
currentSegment += character;
22123
}
124+
}
23125

24-
parts.push(p);
126+
if (isIgnoring) {
127+
currentSegment += '\\';
25128
}
26129

27-
if (!isValidPath(parts)) {
28-
return [];
130+
if (currentPart === 'property') {
131+
if (disallowedKeys.has(currentSegment)) {
132+
return [];
133+
}
134+
135+
parts.push(currentSegment);
136+
} else if (currentPart === 'index') {
137+
throw new Error('Index was not closed');
138+
} else if (currentPart === 'start') {
139+
parts.push('');
29140
}
30141

31142
return parts;
32143
}
33144

145+
function isStringIndex(object, key) {
146+
if (typeof key !== 'number' && Array.isArray(object)) {
147+
const index = Number.parseInt(key, 10);
148+
return Number.isInteger(index) && object[index] === object[key];
149+
}
150+
151+
return false;
152+
}
153+
154+
function assertNotStringIndex(object, key) {
155+
if (isStringIndex(object, key)) {
156+
throw new Error('Cannot use string index');
157+
}
158+
}
159+
34160
module.exports = {
35161
get(object, path, value) {
36162
if (!isObject(object) || typeof path !== 'string') {
@@ -43,12 +169,18 @@ module.exports = {
43169
}
44170

45171
for (let i = 0; i < pathArray.length; i++) {
46-
object = object[pathArray[i]];
172+
const key = pathArray[i];
173+
174+
if (isStringIndex(object, key)) {
175+
object = i === pathArray.length - 1 ? undefined : null;
176+
} else {
177+
object = object[key];
178+
}
47179

48180
if (object === undefined || object === null) {
49181
// `object` is either `undefined` or `null` so we want to stop the loop, and
50182
// if this is not the last bit of the path, and
51-
// if it did't return `undefined`
183+
// if it didn't return `undefined`
52184
// it would return `null` if `object` is `null`
53185
// but we want `get({foo: null}, 'foo.bar')` to equal `undefined`, or the supplied value, not `null`
54186
if (i !== pathArray.length - 1) {
@@ -72,9 +204,10 @@ module.exports = {
72204

73205
for (let i = 0; i < pathArray.length; i++) {
74206
const p = pathArray[i];
207+
assertNotStringIndex(object, p);
75208

76209
if (!isObject(object[p])) {
77-
object[p] = {};
210+
object[p] = Number.isInteger(pathArray[i + 1]) ? [] : {};
78211
}
79212

80213
if (i === pathArray.length - 1) {
@@ -96,6 +229,7 @@ module.exports = {
96229

97230
for (let i = 0; i < pathArray.length; i++) {
98231
const p = pathArray[i];
232+
assertNotStringIndex(object, p);
99233

100234
if (i === pathArray.length - 1) {
101235
delete object[p];
@@ -123,7 +257,7 @@ module.exports = {
123257
// eslint-disable-next-line unicorn/no-for-loop
124258
for (let i = 0; i < pathArray.length; i++) {
125259
if (isObject(object)) {
126-
if (!(pathArray[i] in object)) {
260+
if (!(pathArray[i] in object && !isStringIndex(object, pathArray[i]))) {
127261
return false;
128262
}
129263

‎readme.md

+9-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ dotProp.get({foo: {bar: 'a'}}, 'foo.notDefined.deep', 'default value');
2626
dotProp.get({foo: {'dot.dot': 'unicorn'}}, 'foo.dot\\.dot');
2727
//=> 'unicorn'
2828

29+
dotProp.get({foo: [{bar: 'unicorn'}]}, 'foo[0].bar');
30+
//=> 'unicorn'
31+
2932
// Setter
3033
const object = {foo: {bar: 'a'}};
3134
dotProp.set(object, 'foo.bar', 'b');
@@ -40,6 +43,10 @@ dotProp.set(object, 'foo.baz', 'x');
4043
console.log(object);
4144
//=> {foo: {bar: 'b', baz: 'x'}}
4245

46+
dotProp.set(object, 'foo.biz.0', 'a');
47+
console.log(object);
48+
//=> {foo: {bar: 'b', baz: 'x', biz: ['a']}}
49+
4350
// Has
4451
dotProp.has({foo: {bar: 'unicorn'}}, 'foo.bar');
4552
//=> true
@@ -84,9 +91,9 @@ Returns a boolean of whether the property existed before being deleted.
8491

8592
#### object
8693

87-
Type: `object`
94+
Type: `object | array`
8895

89-
Object to get, set, or delete the `path` value.
96+
Object or array to get, set, or delete the `path` value.
9097

9198
You are allowed to pass in `undefined` as the object to the `get` and `has` functions.
9299

‎test.js

+179-3
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,17 @@ test('get', t => {
1919
t.is(dotProp.get({foo: {}}, 'foo.fake', 'some value'), 'some value');
2020
t.true(dotProp.get({'\\': true}, '\\'));
2121
t.true(dotProp.get({'\\foo': true}, '\\foo'));
22+
t.true(dotProp.get({'\\foo': true}, '\\\\foo'));
23+
t.true(dotProp.get({'foo\\': true}, 'foo\\\\'));
2224
t.true(dotProp.get({'bar\\': true}, 'bar\\'));
2325
t.true(dotProp.get({'foo\\bar': true}, 'foo\\bar'));
24-
t.true(dotProp.get({'\\.foo': true}, '\\\\.foo'));
25-
t.true(dotProp.get({'bar\\.': true}, 'bar\\\\.'));
26-
t.true(dotProp.get({'foo\\.bar': true}, 'foo\\\\.bar'));
26+
t.true(dotProp.get({'\\': {foo: true}}, '\\\\.foo'));
27+
t.true(dotProp.get({'bar\\.': true}, 'bar\\\\\\.'));
28+
t.true(dotProp.get({'foo\\': {
29+
bar: true
30+
}}, 'foo\\\\.bar'));
2731
t.is(dotProp.get({foo: 1}, 'foo.bar'), undefined);
32+
t.true(dotProp.get({'foo\\': true}, 'foo\\'));
2833

2934
const fixture2 = {};
3035
Object.defineProperty(fixture2, 'foo', {
@@ -56,6 +61,103 @@ test('get', t => {
5661
F4Class.prototype.foo = 1;
5762
const f4 = new F4Class();
5863
t.is(dotProp.get(f4, 'foo'), 1); // #46
64+
65+
t.true(dotProp.get({'': {'': {'': true}}}, '..'));
66+
t.true(dotProp.get({'': {'': true}}, '.'));
67+
});
68+
69+
test('get with array indexes', t => {
70+
t.true(dotProp.get([true, false, false], '[0]'));
71+
t.true(dotProp.get([[false, true, false], false, false], '[0][1]'));
72+
t.true(dotProp.get([{foo: [true]}], '[0].foo[0]'));
73+
t.true(dotProp.get({foo: [0, {bar: true}]}, 'foo[1].bar'));
74+
75+
t.false(dotProp.get(['a', 'b', 'c'], '3', false));
76+
t.false(dotProp.get([{foo: [1]}], '[0].bar[0]', false));
77+
t.false(dotProp.get([{foo: [1]}], '[0].foo[1]', false));
78+
t.false(dotProp.get({foo: [0, {bar: 2}]}, 'foo[0].bar', false));
79+
t.false(dotProp.get({foo: [0, {bar: 2}]}, 'foo[2].bar', false));
80+
t.false(dotProp.get({foo: [0, {bar: 2}]}, 'foo[1].biz', false));
81+
t.false(dotProp.get({foo: [0, {bar: 2}]}, 'bar[0].bar', false));
82+
t.true(dotProp.get({
83+
bar: {
84+
'[0]': true
85+
}
86+
}, 'bar.\\[0]'));
87+
t.true(dotProp.get({
88+
bar: {
89+
'': [true]
90+
}
91+
}, 'bar.[0]'));
92+
t.throws(() => dotProp.get({
93+
'foo[5[': true
94+
}, 'foo[5['), {
95+
message: 'Invalid character in an index'
96+
});
97+
t.throws(() => dotProp.get({
98+
'foo[5': {
99+
bar: true
100+
}
101+
}, 'foo[5.bar'), {
102+
message: 'Invalid character in an index'
103+
});
104+
t.true(dotProp.get({
105+
'foo[5]': {
106+
bar: true
107+
}
108+
}, 'foo\\[5].bar'));
109+
t.throws(() => dotProp.get({
110+
'foo[5\\]': {
111+
bar: true
112+
}
113+
}, 'foo[5\\].bar'), {
114+
message: 'Invalid character in an index'
115+
});
116+
t.throws(() => dotProp.get({
117+
'foo[5': true
118+
}, 'foo[5'), {
119+
message: 'Index was not closed'
120+
});
121+
t.throws(() => dotProp.get({
122+
'foo[bar]': true
123+
}, 'foo[bar]'), {
124+
message: 'Invalid character in an index'
125+
});
126+
t.false(dotProp.get({}, 'constructor[0]', false));
127+
t.throws(() => dotProp.get({}, 'foo[constructor]', false), {
128+
message: 'Invalid character in an index'
129+
});
130+
131+
t.false(dotProp.get([], 'foo[0].bar', false));
132+
t.true(dotProp.get({foo: [{bar: true}]}, 'foo[0].bar'));
133+
t.false(dotProp.get({foo: ['bar']}, 'foo[1]', false));
134+
135+
t.false(dotProp.get([true], '0', false));
136+
137+
t.false(dotProp.get({foo: [true]}, 'foo.0', false));
138+
t.true(dotProp.get({foo: {
139+
0: true
140+
}}, 'foo.0'));
141+
142+
t.true(dotProp.get([{
143+
'[1]': true
144+
}, false, false], '[0].\\[1]'));
145+
146+
t.true(dotProp.get({foo: {'[0]': true}}, 'foo.\\[0]'));
147+
t.throws(() => dotProp.get({foo: {'[0]': true}}, 'foo.[0\\]'), {
148+
message: 'Invalid character in an index'
149+
});
150+
t.true(dotProp.get({foo: {'\\': [true]}}, 'foo.\\\\[0]'));
151+
t.throws(() => dotProp.get({foo: {'[0]': true}}, 'foo.[0\\]'), {
152+
message: 'Invalid character in an index'
153+
});
154+
155+
t.throws(() => dotProp.get({'foo[0': {'9]': true}}, 'foo[0.9]'), {
156+
message: 'Invalid character in an index'
157+
});
158+
t.throws(() => dotProp.get({'foo[-1]': true}, 'foo[-1]'), {
159+
message: 'Invalid character in an index'
160+
});
59161
});
60162

61163
test('set', t => {
@@ -117,6 +219,32 @@ test('set', t => {
117219
const output4 = dotProp.set(fixture4, 'foo.bar', 2);
118220
t.is(fixture4, 'noobject');
119221
t.is(output4, fixture4);
222+
223+
const fixture5 = [];
224+
225+
dotProp.set(fixture5, '[1]', true);
226+
t.is(fixture5[1], true);
227+
228+
dotProp.set(fixture5, '[0].foo[0]', true);
229+
t.is(fixture5[0].foo[0], true);
230+
231+
t.throws(() => dotProp.set(fixture5, '1', true), {
232+
message: 'Cannot use string index'
233+
});
234+
235+
t.throws(() => dotProp.set(fixture5, '0.foo.0', true), {
236+
message: 'Cannot use string index'
237+
});
238+
239+
const fixture6 = {};
240+
241+
dotProp.set(fixture6, 'foo[0].bar', true);
242+
t.true(fixture6.foo[0].bar);
243+
t.deepEqual(fixture6, {
244+
foo: [{
245+
bar: true
246+
}]
247+
});
120248
});
121249

122250
test('delete', t => {
@@ -180,6 +308,39 @@ test('delete', t => {
180308
const fixture3 = {foo: null};
181309
t.false(dotProp.delete(fixture3, 'foo.bar'));
182310
t.deepEqual(fixture3, {foo: null});
311+
312+
const fixture4 = [{
313+
top: {
314+
dog: 'sindre'
315+
}
316+
}];
317+
318+
t.throws(() => dotProp.delete(fixture4, '0.top.dog'), {
319+
message: 'Cannot use string index'
320+
});
321+
t.true(dotProp.delete(fixture4, '[0].top.dog'));
322+
t.deepEqual(fixture4, [{top: {}}]);
323+
324+
const fixture5 = {
325+
foo: [{
326+
bar: ['foo', 'bar']
327+
}]
328+
};
329+
330+
dotProp.delete(fixture5, 'foo[0].bar[0]');
331+
332+
const fixtureArray = [];
333+
fixtureArray[1] = 'bar';
334+
335+
t.deepEqual(fixture5, {
336+
foo: [{
337+
bar: fixtureArray
338+
}]
339+
});
340+
341+
const fixture6 = {};
342+
343+
dotProp.set(fixture6, 'foo.bar.0', 'fizz');
183344
});
184345

185346
test('has', t => {
@@ -205,6 +366,21 @@ test('has', t => {
205366
t.true(dotProp.has({'foo.baz': {bar: true}}, 'foo\\.baz.bar'));
206367
t.true(dotProp.has({'fo.ob.az': {bar: true}}, 'fo\\.ob\\.az.bar'));
207368
t.false(dotProp.has(undefined, 'fo\\.ob\\.az.bar'));
369+
370+
t.false(dotProp.has({
371+
foo: [{bar: ['bar', 'bizz']}]
372+
}, 'foo[0].bar.1'));
373+
t.false(dotProp.has({
374+
foo: [{bar: ['bar', 'bizz']}]
375+
}, 'foo[0].bar.2'));
376+
t.false(dotProp.has({
377+
foo: [{bar: ['bar', 'bizz']}]
378+
}, 'foo[1].bar.1'));
379+
t.true(dotProp.has({
380+
foo: [{bar: {
381+
1: 'bar'
382+
}}]
383+
}, 'foo[0].bar.1'));
208384
});
209385

210386
test('prevent setting/getting `__proto__`', t => {

0 commit comments

Comments
 (0)
Please sign in to comment.