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
Original file line number Diff line number Diff line change
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');
Richienb marked this conversation as resolved.
Show resolved Hide resolved
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
151 changes: 137 additions & 14 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,136 @@ const disallowedKeys = new Set([
'constructor'
]);

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

function getPathSegments(path) {
const pathArray = path.split('.');
const parts = [];

for (let i = 0; i < pathArray.length; i++) {
let p = pathArray[i];
let isIgnoring = false;
let isPath = true;
let isIndex = false;
let currentPathSegment = '';

for (const character of path) {
switch (character) {
case '\\':
if (isIgnoring) {
isIgnoring = false;
currentPathSegment += '\\';
}

isIgnoring = !isIgnoring;
break;

case '.':
if (isIgnoring) {
isIgnoring = false;
currentPathSegment += '.';
break;
}

if (isIndex) {
isIndex = false;
currentPathSegment = `[${currentPathSegment}`;
}

if (isPath && currentPathSegment.length > 0) {
if (disallowedKeys.has(currentPathSegment)) {
return [];
}

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

isPath = true;
break;

case '[':
if (isIgnoring) {
isIgnoring = false;
currentPathSegment += '[';
break;
}

if (isPath) {
if (currentPathSegment !== '' || parts.length === 0) {
isPath = false;
isIndex = true;

if (currentPathSegment.length > 0) {
if (disallowedKeys.has(currentPathSegment)) {
return [];
}

parts.push(currentPathSegment);
currentPathSegment = '';
}
} else {
currentPathSegment += '[';
}

break;
}

if (isIndex) {
isPath = true;
currentPathSegment = `[${currentPathSegment}`;
}

isIndex = !isIndex;
break;

case ']':
if (isIgnoring && isIndex) {
isIgnoring = false;
isIndex = false;
currentPathSegment += ']';
break;
}

if (isIndex) {
isIndex = false;
isPath = true;
const index = Number.parseInt(currentPathSegment, 10);
if (Number.isNaN(index)) {
if (disallowedKeys.has(currentPathSegment)) {
return [];
}

parts.push(currentPathSegment);
} else {
parts.push(index);
}

currentPathSegment = '';
break;
}

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

default:
if (isIgnoring) {
isIgnoring = false;
currentPathSegment += '\\';
}

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

parts.push(p);
if (isIndex) {
currentPathSegment = `[${currentPathSegment}`;
}

if (!isValidPath(parts)) {
return [];
if (isIgnoring) {
currentPathSegment += '\\';
}

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

parts.push(currentPathSegment);
}

return parts;
Expand All @@ -43,12 +154,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 +193,11 @@ module.exports = {
const p = pathArray[i];

if (!isObject(object[p])) {
object[p] = {};
if (Number.isInteger(pathArray[i + 1])) {
object[p] = [];
} else {
object[p] = {};
}
Richienb marked this conversation as resolved.
Show resolved Hide resolved
}

if (i === pathArray.length - 1) {
Expand Down
11 changes: 9 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
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
82 changes: 82 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,29 @@ test('get', t => {
t.true(dotProp.get({'foo\\.bar': true}, 'foo\\\\.bar'));
t.is(dotProp.get({foo: 1}, 'foo.bar'), undefined);

t.true(dotProp.get([true, false, false], '[0]'));
Richienb marked this conversation as resolved.
Show resolved Hide resolved
t.true(dotProp.get([{foo: [true]}], '[0].foo[0]'));
t.true(dotProp.get({foo: [0, {bar: true}]}, 'foo[1].bar'));

t.false(dotProp.get(['a', 'b', 'c'], '3', false));
t.false(dotProp.get([{foo: [1]}], '[0].bar[0]', false));
t.false(dotProp.get([{foo: [1]}], '[0].foo[1]', false));
t.false(dotProp.get({foo: [0, {bar: 2}]}, 'foo[0].bar', false));
t.false(dotProp.get({foo: [0, {bar: 2}]}, 'foo[2].bar', false));
t.false(dotProp.get({foo: [0, {bar: 2}]}, 'foo[1].biz', false));
t.false(dotProp.get({foo: [0, {bar: 2}]}, 'bar[0].bar', false));
Richienb marked this conversation as resolved.
Show resolved Hide resolved
t.true(dotProp.get({
bar: {
'[0]': true
}
}, 'bar.[0]'));

t.false(dotProp.get([], 'foo[0].bar', false));
t.true(dotProp.get({foo: [{bar: true}]}, 'foo[0].bar'));
t.false(dotProp.get({foo: ['bar']}, 'foo[1]', false));

t.false(dotProp.get([true], '0', false));

const fixture2 = {};
Object.defineProperty(fixture2, 'foo', {
value: 'bar',
Expand Down Expand Up @@ -117,6 +140,24 @@ test('set', t => {
const output4 = dotProp.set(fixture4, 'foo.bar', 2);
t.is(fixture4, 'noobject');
t.is(output4, fixture4);

const fixture5 = [];

dotProp.set(fixture5, '1', true);
t.true(fixture5[1]);

dotProp.set(fixture5, '0.foo.0', true);
t.true(fixture5[0].foo[0]);

const fixture6 = {};

dotProp.set(fixture6, 'foo[0].bar', true);
t.true(fixture6.foo[0].bar);
t.deepEqual(fixture6, {
foo: [{
bar: true
}]
});
});

test('delete', t => {
Expand Down Expand Up @@ -180,6 +221,37 @@ test('delete', t => {
const fixture3 = {foo: null};
t.false(dotProp.delete(fixture3, 'foo.bar'));
t.deepEqual(fixture3, {foo: null});

const fixture4 = [{
top: {
dog: 'sindre'
}
}];

t.true(dotProp.delete(fixture4, '0.top.dog'));
t.deepEqual(fixture4, [{top: {}}]);

const fixture5 = {
foo: [{
bar: ['foo', 'bar']
}]
};

dotProp.delete(fixture5, 'foo[0].bar[0]');

const fixtureArray = [];
fixtureArray[1] = 'bar';

t.deepEqual(fixture5, {
foo: [{
bar: fixtureArray
}]
});

const fixture6 = {};

dotProp.set(fixture6, 'foo.bar.0', 'fizz');
t.is(fixture6.foo.bar[0], 'fizz');
});

test('has', t => {
Expand All @@ -205,6 +277,16 @@ test('has', t => {
t.true(dotProp.has({'foo.baz': {bar: true}}, 'foo\\.baz.bar'));
t.true(dotProp.has({'fo.ob.az': {bar: true}}, 'fo\\.ob\\.az.bar'));
t.false(dotProp.has(undefined, 'fo\\.ob\\.az.bar'));

t.true(dotProp.has({
foo: [{bar: ['bar', 'bizz']}]
}, 'foo[0].bar.1'));
t.false(dotProp.has({
foo: [{bar: ['bar', 'bizz']}]
}, 'foo[0].bar.2'));
t.false(dotProp.has({
foo: [{bar: ['bar', 'bizz']}]
}, 'foo[1].bar.1'));
});

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