Skip to content

Commit

Permalink
feat: implement own deep setter (#18)
Browse files Browse the repository at this point in the history
Implements our own deep set function so we can optimise for our
particular inputs and drop a dependency.
  • Loading branch information
43081j authored Jun 22, 2024
1 parent 99bdbda commit bf549f7
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 15 deletions.
9 changes: 0 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
"typescript-eslint": "^7.7.1"
},
"dependencies": {
"dset": "^3.1.3",
"fast-decode-uri-component": "^1.0.1"
}
}
43 changes: 43 additions & 0 deletions src/object-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,49 @@ export function getDeepValue(obj: unknown, keys: PropertyKey[]): unknown {
return obj;
}

type KeyableObject = Record<PropertyKey, unknown>;

export function setDeepValue(
obj: unknown,
keys: PropertyKey[],
val: unknown
): void {
const len = keys.length;
const lastKey = len - 1;
let k;
let curr = obj as KeyableObject;
let currVal;
let nextKey;

for (let i = 0; i < len; i++) {
k = keys[i];

if (k === '__proto__' || k === 'constructor' || k === 'prototype') {
break;
}

if (i === lastKey) {
curr[k] = val;
} else {
currVal = curr[k];
if (typeof currVal === 'object' && currVal !== null) {
curr = currVal as KeyableObject;
} else {
nextKey = keys[i + 1];
if (
typeof nextKey === 'string' &&
((nextKey as unknown as number) * 0 !== 0 ||
nextKey.indexOf('.') > -1)
) {
curr = curr[k] = {};
} else {
curr = curr[k] = [] as unknown as KeyableObject;
}
}
}
}
}

const MAX_DEPTH = 20;
const strBracketPair = '[]';
const strBracketLeft = '[';
Expand Down
7 changes: 3 additions & 4 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import {
defaultOptions
} from './shared.js';
import fastDecode from 'fast-decode-uri-component';
import {dset} from 'dset';
import {getDeepValue} from './object-util.js';
import {getDeepValue, setDeepValue} from './object-util.js';

export type ParsedQuery = Record<PropertyKey, unknown>;
export type ParseOptions = Partial<Options>;
Expand Down Expand Up @@ -147,7 +146,7 @@ export function parse(input: string, options?: ParseOptions): ParsedQuery {

if (currentValue === undefined || !arrayRepeat) {
if (hasNestedKey) {
dset(result, keyPath as string[], newValue);
setDeepValue(result, keyPath, newValue);
} else {
result[lastKeyPathPart] = newValue;
}
Expand All @@ -157,7 +156,7 @@ export function parse(input: string, options?: ParseOptions): ParsedQuery {
(currentValue as unknown[]).push(newValue);
} else {
if (hasNestedKey) {
dset(result, keyPath as string[], [currentValue, newValue]);
setDeepValue(result, keyPath, [currentValue, newValue]);
} else {
result[lastKeyPathPart] = [currentValue, newValue];
}
Expand Down
86 changes: 85 additions & 1 deletion src/test/object-util_test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as assert from 'node:assert/strict';
import {test} from 'node:test';
import {getDeepValue, getNestedValues} from '../object-util.js';
import {getDeepValue, getNestedValues, setDeepValue} from '../object-util.js';

test('getDeepValue', async (t) => {
await t.test('retrieves by single key', () => {
Expand Down Expand Up @@ -51,6 +51,90 @@ test('getDeepValue', async (t) => {
});
});

const disallowedKeys = ['__proto__', 'constructor', 'prototype'];

test('setDeepValue', async (t) => {
for (const key of disallowedKeys) {
await t.test(`cannot set ${key}`, () => {
const obj = {};
setDeepValue(obj, [key], 123);
assert.deepEqual(obj, {});
});

await t.test('cannot set proto deeply', () => {
const obj = {foo: {}};
setDeepValue(obj, ['foo', key], 123);
assert.deepEqual(obj, {foo: {}});
});
}

await t.test('sets top level key', () => {
const obj: Record<PropertyKey, unknown> = {};
setDeepValue(obj, ['foo'], 303);
assert.deepEqual(obj, {foo: 303});
});

await t.test('sets deep key', () => {
const obj: Record<PropertyKey, unknown> = {foo: {}};
setDeepValue(obj, ['foo', 'bar'], 303);
assert.deepEqual(obj, {foo: {bar: 303}});
});

await t.test('replaces null object value with object', () => {
const obj: Record<PropertyKey, unknown> = {foo: null};
setDeepValue(obj, ['foo', 'bar'], 303);
assert.deepEqual(obj, {
foo: {
bar: 303
}
});
});

await t.test('replaces null array value with array', () => {
const obj: Record<PropertyKey, unknown> = {foo: null};
setDeepValue(obj, ['foo', 0], 303);
assert.deepEqual(obj, {
foo: [303]
});
});

await t.test('creates new objects for object values', () => {
const obj: Record<PropertyKey, unknown> = {};
setDeepValue(obj, ['foo', 'bar'], 303);
assert.deepEqual(obj, {
foo: {
bar: 303
}
});
});

await t.test('creates new arrays for array values', () => {
const obj: Record<PropertyKey, unknown> = {};
setDeepValue(obj, ['foo', 0], 303);
assert.deepEqual(obj, {
foo: [303]
});
});

await t.test('creates new arrays with string indices', () => {
const obj: Record<PropertyKey, unknown> = {};
setDeepValue(obj, ['foo', '0'], 303);
assert.deepEqual(obj, {
foo: [303]
});
});

await t.test('treats decimal strings as regular keys', () => {
const obj: Record<PropertyKey, unknown> = {};
setDeepValue(obj, ['foo', '10.0'], 303);
assert.deepEqual(obj, {
foo: {
'10.0': 303
}
});
});
});

test('getNestedValues', async (t) => {
await t.test('shallow values', () => {
const obj = {
Expand Down

0 comments on commit bf549f7

Please sign in to comment.