Skip to content

Commit

Permalink
feat: stringify implementation
Browse files Browse the repository at this point in the history
Adds the stringify function and updates benchmarks.
  • Loading branch information
43081j committed Jun 21, 2024
1 parent 0131bb5 commit 00f0fc5
Show file tree
Hide file tree
Showing 13 changed files with 786 additions and 254 deletions.
37 changes: 6 additions & 31 deletions bench/main.js → bench/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ const suites = [
{
name: 'Basic (no nesting)',
inputs: [
'foo=bar',
'foo=123&bar=456',
'foo.bar=123&baz=456',
'foo=123&bar=456&foo=123',
'foo.bar.baz=woof'
'foo=a&bar=b&baz=c'
],
options: {
qs: {allowDots: false},
Expand All @@ -21,10 +17,7 @@ const suites = [
{
name: 'Dot-syntax nesting',
inputs: [
'foo=bar',
'foo=123&bar=456',
'foo.bar=123&baz=456',
'foo.bar.baz=woof'
'foo.bar.x=303&foo.bar.y=808'
],
options: {
qs: {allowDots: true},
Expand All @@ -34,11 +27,7 @@ const suites = [
{
name: 'Index-syntax nesting',
inputs: [
'foo=bar',
'foo=bar&bar=baz',
'foo[bar]=baz',
'some[deeply][nested][key]=value',
'foo[foo]=a&bar[bar]=b'
'foo[bar][x]=303&foo[bar][y]=808'
],
options: {
qs: {allowDots: false},
Expand All @@ -47,34 +36,22 @@ const suites = [
},
{
name: 'Custom delimiter',
inputs: [
'foo=a;bar=b',
'foo=a',
'foo=a;bar=b;baz=c'
],
inputs: ['foo=a;bar=b;baz=c'],
options: {
qs: {delimiter: ';'},
pico: {nested: false, delimiter: ';'}
}
},
{
name: 'Bracket-style arrays',
inputs: [
'foo[]=a&foo[]=b',
'foo=a&bar[]=b&baz=c&bar[]=b',
'foo=bar'
],
inputs: ['foo[]=a&foo[]=b&foo[]=c'],
options: {
pico: {arrayRepeat: true, arrayRepeatSyntax: 'bracket'}
}
},
{
name: 'Repeat-style arrays',
inputs: [
'foo=a&foo=b',
'foo=a&bar=b&baz=c&bar=b',
'foo=bar'
],
inputs: ['foo=a&foo=b&foo=c'],
options: {
pico: {arrayRepeat: true, arrayRepeatSyntax: 'repeat'}
}
Expand Down Expand Up @@ -108,5 +85,3 @@ for (const suite of suites) {

console.table(bench.table());
}

setInterval(() => {}, 5000);
107 changes: 107 additions & 0 deletions bench/stringify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {Bench} from 'tinybench';
import {stringify} from '../lib/main.js';
import {stringify as fastStringify} from 'fast-querystring';
import {stringify as qsStringify} from 'qs';

const suites = [
{
name: 'Basic (no nesting)',
inputs: [
{foo: 'x', bar: 'y', baz: 'z'}
],
options: {
qs: {allowDots: false},
pico: {nested: false}
}
},
{
name: 'Dot-syntax nesting',
inputs: [
{
foo: {
bar0: 'x',
bar1: {
baz0: 'y',
baz1: 'z'
}
}
}
],
options: {
qs: {allowDots: true},
pico: {nested: true}
}
},
{
name: 'Index-syntax nesting',
inputs: [
{
foo: {
bar0: 'x',
bar1: {
baz0: 'y',
baz1: 'z'
}
}
}
],
options: {
qs: {allowDots: false},
pico: {nested: true, nestingSyntax: 'index'}
}
},
{
name: 'Custom delimiter',
inputs: [{foo: 'a', bar: 'b', baz: 'c'}],
options: {
qs: {delimiter: ';'},
pico: {nested: false, delimiter: ';'}
}
},
{
name: 'Bracket-style arrays',
inputs: [
{foo: ['a', 'b', 'c'], bar: 'y'}
],
options: {
pico: {arrayRepeat: true, arrayRepeatSyntax: 'bracket'}
}
},
{
name: 'Repeat-style arrays',
inputs: [
{foo: ['a', 'b', 'c'], bar: 'y'}
],
options: {
pico: {arrayRepeat: true, arrayRepeatSyntax: 'repeat'}
}
}
];

for (const suite of suites) {
const bench = new Bench();

bench
.add('picoquery', () => {
for (const input of suite.inputs) {
stringify(input, suite.options?.pico);
}
})
.add('qs', () => {
for (const input of suite.inputs) {
qsStringify(input, suite.options?.qs);
}
})
.add('fast-querystring (no nesting)', () => {
for (const input of suite.inputs) {
fastStringify(input);
}
});

console.log('Benchmark:', suite.name);

await bench.warmup();
await bench.run();

console.table(bench.table());
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
"build": "tsc",
"test": "npm run build && c8 node --test",
"lint": "eslint src",
"format": "prettier --write src",
"bench": "node ./bench/main.js",
"format": "prettier --write src bench",
"bench:parse": "node ./bench/parse.js",
"bench:stringify": "node ./bench/stringify.js",
"prepare": "npm run build"
},
"repository": {
Expand Down
1 change: 1 addition & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export {parse} from './parse.js';
export {stringify} from './stringify.js';
75 changes: 75 additions & 0 deletions src/object-util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {type Options, defaultOptions} from './shared.js';

export function getDeepValue(obj: unknown, keys: PropertyKey[]): unknown {
const keysLength = keys.length;
for (let i = 0; i < keysLength; i++) {
Expand All @@ -8,3 +10,76 @@ export function getDeepValue(obj: unknown, keys: PropertyKey[]): unknown {
}
return obj;
}

const MAX_DEPTH = 20;
const strBracketPair = '[]';

export type KeyValuePair = [PropertyKey, unknown];

function walkNestedValues(
obj: Record<PropertyKey, unknown>,
options: Partial<Options>,
out: KeyValuePair[],
depth: number = 0,
parentKey?: string,
useArrayRepeatKey?: boolean
): void {
const {
nestingSyntax = defaultOptions.nestingSyntax,
arrayRepeat = defaultOptions.arrayRepeat,
arrayRepeatSyntax = defaultOptions.arrayRepeatSyntax
} = options;
if (depth > MAX_DEPTH) {
return;
}

for (const key in obj) {
const value = obj[key];
let path;
if (parentKey) {
path = parentKey;
if (useArrayRepeatKey) {
if (arrayRepeatSyntax === 'bracket') {
path += strBracketPair;
}
} else if (nestingSyntax === 'dot') {
path += '.';
path += key;
} else {
path += '[';
path += key;
path += ']';
}
} else {
path = key;
}

if (typeof value === 'object' && value !== null) {
walkNestedValues(
value as Record<PropertyKey, unknown>,
options,
out,
depth + 1,
path,
arrayRepeat && Array.isArray(value)
);
} else {
out.push([path, value]);
}
}
}

export function getNestedValues(
obj: object,
options: Partial<Options>
): KeyValuePair[] {
const result: KeyValuePair[] = [];

if (obj === null) {
return result;
}

walkNestedValues(obj as Record<PropertyKey, unknown>, options, result);

return result;
}
21 changes: 5 additions & 16 deletions src/parse.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import {
type ParseOptions,
type Options,
type DeserializeKeyFunction,
type DeserializeValueFunction
type DeserializeValueFunction,
defaultOptions
} from './shared.js';
import fastDecode from 'fast-decode-uri-component';
import {dset} from 'dset';
import {getDeepValue} from './object-util.js';
import {splitByIndexPattern} from './string-util.js';

export type ParsedQuery = Record<PropertyKey, unknown>;
export type UserParseOptions = Partial<ParseOptions>;
export type ParseOptions = Partial<Options>;

export const numberKeyDeserializer: DeserializeKeyFunction = (key) => {
const asNumber = Number(key);
Expand All @@ -27,18 +28,6 @@ export const numberValueDeserializer: DeserializeValueFunction = (value) => {
return value;
};

const identityFunc = <T>(v: T): T => v;

const defaultOptions: ParseOptions = {
nested: true,
nestingSyntax: 'dot',
arrayRepeat: false,
arrayRepeatSyntax: 'repeat',
delimiter: 38,
valueDeserializer: identityFunc,
keyDeserializer: identityFunc
};

const regexPlus = /\+/g;
const Empty = function () {} as unknown as {new (): ParsedQuery};
Empty.prototype = Object.create(null);
Expand All @@ -48,7 +37,7 @@ Empty.prototype = Object.create(null);
* @param {string} input
* @param {ParseOptions=} options
*/
export function parse(input: string, options?: UserParseOptions): ParsedQuery {
export function parse(input: string, options?: ParseOptions): ParsedQuery {
const {
valueDeserializer = defaultOptions.valueDeserializer,
keyDeserializer = defaultOptions.keyDeserializer,
Expand Down
14 changes: 13 additions & 1 deletion src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export type DeserializeKeyFunction = (
key: string
) => PropertyKey | typeof CONTINUE;

export interface ParseOptions {
export interface Options {
// Enable parsing nested objects and arrays
// default: true
nested: boolean;
Expand All @@ -50,3 +50,15 @@ export interface ParseOptions {
valueDeserializer: DeserializeValueFunction;
keyDeserializer: DeserializeKeyFunction;
}

const identityFunc = <T>(v: T): T => v;

export const defaultOptions: Options = {
nested: true,
nestingSyntax: 'dot',
arrayRepeat: false,
arrayRepeatSyntax: 'repeat',
delimiter: 38,
valueDeserializer: identityFunc,
keyDeserializer: identityFunc
};
Loading

0 comments on commit 00f0fc5

Please sign in to comment.