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
9 changes: 8 additions & 1 deletion index.d.ts
Expand Up @@ -23,13 +23,16 @@ 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.
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 Down
151 changes: 137 additions & 14 deletions index.js
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
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
90 changes: 86 additions & 4 deletions test.js
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 All @@ -34,7 +57,7 @@ test('get', t => {
t.is(dotProp.get(fixture2, 'foo'), 'bar');
t.is(dotProp.get({}, 'hasOwnProperty'), Object.prototype.hasOwnProperty);

function fn() {}
function fn() { }
fn.foo = {bar: 1};
t.is(dotProp.get(fn), fn);
t.is(dotProp.get(fn, 'foo'), fn.foo);
Expand All @@ -52,7 +75,7 @@ test('get', t => {
t.false(dotProp.get([], 'foo.bar', false));
t.false(dotProp.get(undefined, 'foo.bar', false));

class F4Class {}
class F4Class { }
F4Class.prototype.foo = 1;
const f4 = new F4Class();
t.is(dotProp.get(f4, 'foo'), 1); // #46
Expand Down Expand Up @@ -91,7 +114,7 @@ test('set', t => {
dotProp.set(fixture1, 'foo.function', func);
t.is(fixture1.foo.function, func);

function fn() {}
function fn() { }
dotProp.set(fn, 'foo.bar', 1);
t.is(fn.foo.bar, 1);

Expand All @@ -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 @@ -196,7 +268,7 @@ test('has', t => {
t.false(dotProp.has({foo: null}, 'foo.bar'));
t.false(dotProp.has({foo: ''}, 'foo.bar'));

function fn() {}
function fn() { }
fn.foo = {bar: 1};
t.false(dotProp.has(fn));
t.true(dotProp.has(fn, 'foo'));
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