43081j / picoquery

A small library for parsing and serializing query strings
MIT License
69 stars 2 forks source link

picoquery

CI NPM version

A lightweight query string parser/stringifier with support for nesting and some configurability.

Built on top of fast-querystring.

Install

npm i -S picoquery

CommonJS vs ESM

2.x and above are ESM only (i.e. your project will need type: "module" in the package.json).

If you cannot yet move to ESM, you may continue to use 1.x which will still receive non-breaking changes, backported from main.

Usage

Parsing a query string:

import {parse} from 'picoquery';

parse('foo.bar=abc&baz=def');

/*
  {
    foo: {
      bar: 'abc'
    },
    baz: 'def'
  }
*/

Stringifying an object:

import {stringify} from 'picoquery';

stringify({
  foo: {
    bar: 123
  }
});

/*
foo.bar=123
*/

API

stringify(object[, options])

Converts the given object into a query string, optionally with configured options.

parse(str[, options])

Parses the given query string into an object, optionally with configured options.

Options

Default options

The default options are as follows:

{
  nesting: true,
  nestingSyntax: 'dot',
  arrayRepeat: false,
  arrayRepeatSyntax: 'repeat',
  delimiter: '&'
}

nesting

When true, nested objects are supported.

For example, when parsing:

parse('foo.bar=baz', {nesting: true});

// {foo: {bar: 'baz'}}

When stringifying:

stringify({foo: {bar: 'baz'}}, {nesting: true});

// foo.bar=baz

This also results in arrays being supported:

parse('foo.0=bar', {nesting: true});
// {foo: ['bar']}

stringify({foo: ['bar']}, {nesting: true});
// foo.0=bar

nestingSyntax

Sets which style of nesting syntax should be used. The choices are:

arrayRepeat

If true, this will treat repeated keys as arrays.

For example:

parse('foo=x&foo=y', {arrayRepeat: true});
// {foo: ['x', 'y']}

stringify({foo: ['x', 'y']}, {arrayRepeat: true});
// foo=x&foo=y

arrayRepeatSyntax

Sets which style of array repetition syntax should be used. The choices are:

delimiter

Sets the delimiter to be used instead of &.

For example:

parse('foo=x;bar=y', {delimiter: ';'});
// {foo: 'x', bar: 'y'}

stringify({foo: 'x', bar: 'y'}, {delimiter: ';'});
// foo=x;bar=y

valueDeserializer

Can be set to a function which will be used to deserialize each value during parsing.

It will be called with the value and the already deserialized key (i.e. (value: string, key: PropertyKey) => *).

For example:

parse('foo=300', {
  valueDeserializer: (value) => {
    const asNum = Number(value);
    return Number.isNaN(asNum) ? value : asNum;
  }
});

// {foo: 300}

keyDeserializer

Can be set to a function which will be used to deserialize each key during parsing.

It will be called with the key from the query string (i.e. (key) => PropertyKey).

For example:

parse('300=foo', {
  keyDeserializer: (key) => {
    const asNum = Number(key);
    return Number.isNaN(asNum) ? key : asNum;
  }
});

// {300: 'foo'}

shouldSerializeObject

Can be set to a function which determines if an object-like value should be serialized instead of being treated as a nested object.

All non-object primitives will always be serialized.

For example:

// Assuming `StringifableObject` returns its constructor value when `toString`
// is called.
stringify({
  foo: new StringifiableObject('test')
}, {
  shouldSerializeObject(val) {
    return val instanceof StringifableObject;
  },
  valueSerializer: (value) => {
    return String(value);
  }
});

// foo=test

If you want to fall back to the default logic, you can import the default function:

import {defaultShouldSerializeObject, stringify} from 'picoquery';

stringify({
  foo: new StringifiableObject('test')
}, {
  shouldSerializeObject(val) {
    if (val instanceof StringifableObject) {
      return true;
    }
    return defaultShouldSerializeObject(val);
  }
});

valueSerializer

Can be set to a function which will be used to serialize each value during stringifying.

It will be called with the value and the key (i.e. (value: unknown, key: PropertyKey) => string).

For example:

stringify({foo: 'bar'}, {
  valueSerializer: (val) => String(val) + String(val)
});

// foo=barbar

Note that you can import the default serializer if you only want to handle some cases.

For example:

import {defaultValueSerializer, stringify} from 'picoquery';

stringify({foo: 'bar'}, {
  valueSerializer: (val, key) => {
    if (val instanceof Date) {
      return val.toISOString();
    }

    // Call the original serializer otherwise
    return defaultValueSerializer(val, key);
  }
});

Benchmarks

IMPORTANT: there are a few things to take into account with these benchmarks:

Parse

Benchmark: Basic (no nesting)
┌─────────┬─────────────────────────────────┬─────────────┬────────────────────┬──────────┬─────────┐
│ (index) │ Task Name                       │ ops/sec     │ Average Time (ns)  │ Margin   │ Samples │
├─────────┼─────────────────────────────────┼─────────────┼────────────────────┼──────────┼─────────┤
│ 0       │ 'picoquery'                     │ '2,393,539' │ 417.79130492907393 │ '±0.42%' │ 1196770 │
│ 1       │ 'qs'                            │ '382,933'   │ 2611.4212945313157 │ '±1.04%' │ 191467  │
│ 2       │ 'fast-querystring (no nesting)' │ '2,633,148' │ 379.77342802356833 │ '±1.58%' │ 1316575 │
└─────────┴─────────────────────────────────┴─────────────┴────────────────────┴──────────┴─────────┘
Benchmark: Dot-syntax nesting
┌─────────┬─────────────────────────────────┬─────────────┬───────────────────┬──────────┬─────────┐
│ (index) │ Task Name                       │ ops/sec     │ Average Time (ns) │ Margin   │ Samples │
├─────────┼─────────────────────────────────┼─────────────┼───────────────────┼──────────┼─────────┤
│ 0       │ 'picoquery'                     │ '1,406,039' │ 711.2176054736389 │ '±0.56%' │ 703020  │
│ 1       │ 'qs'                            │ '205,611'   │ 4863.536155478235 │ '±0.98%' │ 102806  │
│ 2       │ 'fast-querystring (no nesting)' │ '2,588,560' │ 386.3151031345139 │ '±0.66%' │ 1294281 │
└─────────┴─────────────────────────────────┴─────────────┴───────────────────┴──────────┴─────────┘

Stringify

Benchmark: Basic (no nesting)
┌─────────┬─────────────────────────────────┬─────────────┬────────────────────┬──────────┬─────────┐
│ (index) │ Task Name                       │ ops/sec     │ Average Time (ns)  │ Margin   │ Samples │
├─────────┼─────────────────────────────────┼─────────────┼────────────────────┼──────────┼─────────┤
│ 0       │ 'picoquery'                     │ '4,163,092' │ 240.20605684139227 │ '±0.16%' │ 2081547 │
│ 1       │ 'qs'                            │ '843,630'   │ 1185.352608720127  │ '±0.76%' │ 421816  │
│ 2       │ 'fast-querystring (no nesting)' │ '3,795,565' │ 263.46536774751036 │ '±0.24%' │ 1897783 │
└─────────┴─────────────────────────────────┴─────────────┴────────────────────┴──────────┴─────────┘
Benchmark: Dot-syntax nesting
┌─────────┬─────────────────────────────────┬─────────────┬────────────────────┬──────────┬─────────┐
│ (index) │ Task Name                       │ ops/sec     │ Average Time (ns)  │ Margin   │ Samples │
├─────────┼─────────────────────────────────┼─────────────┼────────────────────┼──────────┼─────────┤
│ 0       │ 'picoquery'                     │ '1,768,770' │ 565.3644818387182  │ '±1.08%' │ 884387  │
│ 1       │ 'qs'                            │ '332,254'   │ 3009.743667534287  │ '±1.38%' │ 166128  │
│ 2       │ 'fast-querystring (no nesting)' │ '7,227,031' │ 138.36940410118828 │ '±1.15%' │ 3613517 │
└─────────┴─────────────────────────────────┴─────────────┴────────────────────┴──────────┴─────────┘

License

MIT