Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support array indexes #82

Merged
merged 14 commits into from Jan 21, 2022
17 changes: 12 additions & 5 deletions index.d.ts
Expand Up @@ -4,7 +4,7 @@ declare const dotProp: {
/**
Get the value of the property at the given path.

@param object - Object to get the `path` value.
@param object - Object or array to get the `path` value.
@param path - Path of the property in the object, using `.` to separate each nested key. Use `\\.` if you have a `.` in the key.
@param defaultValue - Default value.

Expand All @@ -23,18 +23,21 @@ declare const dotProp: {

dotProp.get({foo: {'dot.dot': 'unicorn'}}, 'foo.dot\\.dot');
//=> 'unicorn'

dotProp.get({foo: [{bar: 'unicorn'}]}, 'foo[0].bar');
//=> 'unicorn'
```
*/
get: <ObjectType, PathType extends string, DefaultValue = undefined>(
object: ObjectType,
path: PathType,
defaultValue?: DefaultValue
) => 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>`
) => ObjectType extends Record<string, unknown> | unknown[] ? (Get<ObjectType, PathType> extends unknown ? DefaultValue : Get<ObjectType, PathType>) : undefined;

/**
Set the property at the given path to the given value.

@param object - Object to set the `path` value.
@param object - Object or array to set the `path` value.
@param path - Path of the property in the object, using `.` to separate each nested key. Use `\\.` if you have a `.` in the key.
@param value - Value to set at `path`.
@returns The object.
Expand All @@ -55,6 +58,10 @@ declare const dotProp: {
dotProp.set(object, 'foo.baz', 'x');
console.log(object);
//=> {foo: {bar: 'b', baz: 'x'}}

dotProp.set(object, 'foo.biz[0]', 'a');
console.log(object);
//=> {foo: {bar: 'b', baz: 'x', biz: ['a']}}
```
*/
set: <ObjectType extends {[key: string]: any}>(
Expand All @@ -66,7 +73,7 @@ declare const dotProp: {
/**
Check whether the property at the given path exists.

@param object - Object to test the `path` value.
@param object - Object or array to test the `path` value.
@param path - Path of the property in the object, using `.` to separate each nested key. Use `\\.` if you have a `.` in the key.

@example
Expand All @@ -82,7 +89,7 @@ declare const dotProp: {
/**
Delete the property at the given path.

@param object - Object to delete the `path` value.
@param object - Object or array to delete the `path` value.
@param path - Path of the property in the object, using `.` to separate each nested key. Use `\\.` if you have a `.` in the key.
@returns A boolean of whether the property existed before being deleted.

Expand Down
148 changes: 134 additions & 14 deletions index.js
Expand Up @@ -7,25 +7,137 @@ const disallowedKeys = new Set([
'constructor'
]);

const isValidPath = pathSegments => !pathSegments.some(segment => disallowedKeys.has(segment));

function getPathSegments(path) {
const pathArray = path.split('.');
const parts = [];
let currentSegment = '';
let currentIndex = '';
let isIndex = false;
let isIgnoring = false;

for (const [index, character] of Object.entries(path)) {
switch (character) {
case '\\':
if (isIndex) {
isIndex = false;
currentSegment += `[${currentIndex}`;
currentIndex = '';
} else if (isIgnoring) {
// If `\\` was escaped
currentSegment += '\\';
}

isIgnoring = !isIgnoring;

break;

case '.':
if (isIgnoring) {
// If `.` was escaped
isIgnoring = false;
currentSegment += character;
break;
}

if (isIndex) {
currentSegment += `[${currentIndex}`;
currentIndex = '';
isIndex = false;
}

if (path[index - 1] === ']' && typeof parts[parts.length - 1] === 'number') {
// If the dot immediately proceeds an index, skip saving the empty string
break;
}

if (disallowedKeys.has(currentSegment)) {
return [];
}

parts.push(currentSegment);
currentSegment = '';

break;

case '[':
if (isIgnoring) {
// If `[` was escaped
isIgnoring = false;
currentSegment += character;
break;
}

for (let i = 0; i < pathArray.length; i++) {
let p = pathArray[i];
if (path[index - 1] === '.') {
currentSegment += character;
break;
}

if (!isIndex) {
isIndex = true;
break;
}

isIndex = false;
currentSegment += `[${currentIndex}[`;
currentIndex = '';
break;

case ']':
if (isIndex) {
const index = Number.parseFloat(currentIndex);
if (Number.isInteger(index) && index >= 0) {
if (currentSegment) {
if (disallowedKeys.has(currentSegment)) {
return [];
}

parts.push(currentSegment);
currentSegment = '';
}

parts.push(index);
} else {
currentSegment += `[${currentIndex}]`;
}

currentIndex = '';
isIndex = false;
break;
} else if (isIgnoring) {
currentSegment += ']';
isIgnoring = false;
break;
}

// Falls through
Richienb marked this conversation as resolved.
Show resolved Hide resolved

default:
if (isIndex) {
currentIndex += character;
break;
}

if (isIgnoring) {
// If no character was escaped
isIgnoring = false;
currentSegment += '\\';
}

while (p[p.length - 1] === '\\' && pathArray[i + 1] !== undefined) {
p = p.slice(0, -1) + '.';
p += pathArray[++i];
currentSegment += character;
}
}

parts.push(p);
if (currentIndex) {
currentSegment += `[${currentIndex}`;
} else if (isIgnoring) {
currentSegment += '\\';
}

if (!isValidPath(parts)) {
return [];
if (currentSegment.length > 0 || parts.length === 0) {
if (disallowedKeys.has(currentSegment)) {
return [];
}

parts.push(currentSegment);
}

return parts;
Expand All @@ -43,12 +155,20 @@ module.exports = {
}

for (let i = 0; i < pathArray.length; i++) {
object = object[pathArray[i]];
const key = pathArray[i];
const index = Number.parseInt(key, 10);

// Disallow string indexes
if (!Number.isInteger(key) && Array.isArray(object) && !Number.isNaN(index) && object[index] === object[key]) {
Richienb marked this conversation as resolved.
Show resolved Hide resolved
object = i === pathArray.length - 1 ? undefined : null;
Richienb marked this conversation as resolved.
Show resolved Hide resolved
} else {
object = object[key];
}

if (object === undefined || object === null) {
// `object` is either `undefined` or `null` so we want to stop the loop, and
// if this is not the last bit of the path, and
// if it did't return `undefined`
// if it didn't return `undefined`
// it would return `null` if `object` is `null`
// but we want `get({foo: null}, 'foo.bar')` to equal `undefined`, or the supplied value, not `null`
if (i !== pathArray.length - 1) {
Expand All @@ -74,7 +194,7 @@ module.exports = {
const p = pathArray[i];

if (!isObject(object[p])) {
object[p] = {};
object[p] = Number.isInteger(pathArray[i + 1]) ? [] : {};
}

if (i === pathArray.length - 1) {
Expand Down
11 changes: 9 additions & 2 deletions readme.md
Expand Up @@ -26,6 +26,9 @@ dotProp.get({foo: {bar: 'a'}}, 'foo.notDefined.deep', 'default value');
dotProp.get({foo: {'dot.dot': 'unicorn'}}, 'foo.dot\\.dot');
//=> 'unicorn'

dotProp.get({foo: [{bar: 'unicorn'}]}, 'foo[0].bar');
//=> 'unicorn'

// Setter
const object = {foo: {bar: 'a'}};
dotProp.set(object, 'foo.bar', 'b');
Expand All @@ -40,6 +43,10 @@ dotProp.set(object, 'foo.baz', 'x');
console.log(object);
//=> {foo: {bar: 'b', baz: 'x'}}

dotProp.set(object, 'foo.biz.0', 'a');
console.log(object);
//=> {foo: {bar: 'b', baz: 'x', biz: ['a']}}

// Has
dotProp.has({foo: {bar: 'unicorn'}}, 'foo.bar');
//=> true
Expand Down Expand Up @@ -84,9 +91,9 @@ Returns a boolean of whether the property existed before being deleted.

#### object

Type: `object`
Type: `object | array`

Object to get, set, or delete the `path` value.
Object or array to get, set, or delete the `path` value.

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

Expand Down