Skip to content

Commit d64e27b

Browse files
authored
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

Lines changed: 12 additions & 5 deletions
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

Lines changed: 148 additions & 14 deletions
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

Lines changed: 9 additions & 2 deletions
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

0 commit comments

Comments
 (0)