Skip to content

Commit

Permalink
Support array indexes (#82)
Browse files Browse the repository at this point in the history
  • Loading branch information
Richienb committed Jan 21, 2022
1 parent d400c8d commit d64e27b
Show file tree
Hide file tree
Showing 4 changed files with 348 additions and 24 deletions.
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
162 changes: 148 additions & 14 deletions index.js
Expand Up @@ -7,30 +7,156 @@ const disallowedKeys = new Set([
'constructor'
]);

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

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

for (const character of path) {
switch (character) {
case '\\':
if (currentPart === 'index') {
throw new Error('Invalid character in an index');
}

if (currentPart === 'indexEnd') {
throw new Error('Invalid character after an index');
}

if (isIgnoring) {
currentSegment += character;
}

currentPart = 'property';
isIgnoring = !isIgnoring;
break;

for (let i = 0; i < pathArray.length; i++) {
let p = pathArray[i];
case '.':
if (currentPart === 'index') {
throw new Error('Invalid character in an index');
}

if (currentPart === 'indexEnd') {
currentPart = 'property';
break;
}

if (isIgnoring) {
isIgnoring = false;
currentSegment += character;
break;
}

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

parts.push(currentSegment);
currentSegment = '';
currentPart = 'property';
break;

case '[':
if (currentPart === 'index') {
throw new Error('Invalid character in an index');
}

if (currentPart === 'indexEnd') {
currentPart = 'index';
break;
}

if (isIgnoring) {
isIgnoring = false;
currentSegment += character;
break;
}

if (currentPart === 'property') {
if (disallowedKeys.has(currentSegment)) {
return [];
}

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

currentPart = 'index';
break;

while (p[p.length - 1] === '\\' && pathArray[i + 1] !== undefined) {
p = p.slice(0, -1) + '.';
p += pathArray[++i];
case ']':
if (currentPart === 'index') {
parts.push(Number.parseInt(currentSegment, 10));
currentSegment = '';
currentPart = 'indexEnd';
break;
}

if (currentPart === 'indexEnd') {
throw new Error('Invalid character after an index');
}

// Falls through

default:
if (currentPart === 'index' && !digits.has(character)) {
throw new Error('Invalid character in an index');
}

if (currentPart === 'indexEnd') {
throw new Error('Invalid character after an index');
}

if (currentPart === 'start') {
currentPart = 'property';
}

if (isIgnoring) {
isIgnoring = false;
currentSegment += '\\';
}

currentSegment += character;
}
}

parts.push(p);
if (isIgnoring) {
currentSegment += '\\';
}

if (!isValidPath(parts)) {
return [];
if (currentPart === 'property') {
if (disallowedKeys.has(currentSegment)) {
return [];
}

parts.push(currentSegment);
} else if (currentPart === 'index') {
throw new Error('Index was not closed');
} else if (currentPart === 'start') {
parts.push('');
}

return parts;
}

function isStringIndex(object, key) {
if (typeof key !== 'number' && Array.isArray(object)) {
const index = Number.parseInt(key, 10);
return Number.isInteger(index) && object[index] === object[key];
}

return false;
}

function assertNotStringIndex(object, key) {
if (isStringIndex(object, key)) {
throw new Error('Cannot use string index');
}
}

module.exports = {
get(object, path, value) {
if (!isObject(object) || typeof path !== 'string') {
Expand All @@ -43,12 +169,18 @@ module.exports = {
}

for (let i = 0; i < pathArray.length; i++) {
object = object[pathArray[i]];
const key = pathArray[i];

if (isStringIndex(object, key)) {
object = i === pathArray.length - 1 ? undefined : null;
} 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 @@ -72,9 +204,10 @@ module.exports = {

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

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

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

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

if (i === pathArray.length - 1) {
delete object[p];
Expand Down Expand Up @@ -123,7 +257,7 @@ module.exports = {
// eslint-disable-next-line unicorn/no-for-loop
for (let i = 0; i < pathArray.length; i++) {
if (isObject(object)) {
if (!(pathArray[i] in object)) {
if (!(pathArray[i] in object && !isStringIndex(object, pathArray[i]))) {
return false;
}

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

0 comments on commit d64e27b

Please sign in to comment.