Skip to content

Commit

Permalink
feat: add optional replacer function in parse and stringify functions (
Browse files Browse the repository at this point in the history
…#16)

* feat: add optional replacer function in stringify function
* feat: add optional replacer function in parse function

Signed-off-by: Rong Sen Ng (motss) <[email protected]>
  • Loading branch information
motss authored Feb 18, 2023
1 parent 620032e commit ca44c86
Show file tree
Hide file tree
Showing 9 changed files with 451 additions and 124 deletions.
5 changes: 5 additions & 0 deletions .changeset/angry-foxes-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"tyqs": minor
---

feat: add optional replacer function in stringify function
5 changes: 5 additions & 0 deletions .changeset/nine-rockets-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"tyqs": minor
---

feat: add optional replacer function in parse function
156 changes: 140 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@

- [Pre-requisite](#pre-requisite)
- [Install](#install)
- [Features](#features)
- [Usage](#usage)
- [TypeScript or ES Modules](#typescript-or-es-modules)
- [Optional replacer function](#optional-replacer-function)
- [API Reference](#api-reference)
- [parse(searchParams\[, options\])](#parsesearchparams-options)
- [stringify(value)](#stringifyvalue)
- [parse(searchParams\[, replacer\])](#parsesearchparams-replacer)
- [stringify(input\[, replacer\])](#stringifyinput-replacer)
- [Contributing](#contributing)
- [Code of Conduct](#code-of-conduct)
- [License](#license)
Expand All @@ -49,12 +51,29 @@
$ npm i tyqs
```

## Features

| Support | Feature | Description | Example |
| --- | --- | --- | --- |
|| [parse] | Decodes URL search params into an object. | `parse('a=a&b=1')` returns `{ a: 'a', b: '1' }`. |
|| [stringify] | Encodes an object into URL search params. | `stringify({ a: 'a', b: 1 })` gives `a=a&b=1`. |
|| Parse multiple values | Parses comma-separated param into an array of values. | `parse('a=a,b')` returns `{ a: ['a', 'b'] }`. |
|| Parse single value | Parses single-value param into a string. | `parse('a=a')` returns `{ a: 'a' }`. |
|| Parse multiple params of the same name | Parses multiple params of the same name into an array of values. | `parse('a=a,b&a=c')` returns `{ a: ['a', 'b', 'c'] }`. |
|| Parse nested params | Parses nested params with dot or bracket notation. | `parse('a.a=a&b[a]=b&c[a].b=c&d.a[b].c=d')` returns `{ a: { a: 'a' }, b: { a: 'b' }, c: { a: { b: 'c' } }, d: { a: { b: { c: 'd' } } } }`. |
|| Stringify nested params | Stringifies nested params with dot or bracket notation. | `stringify({ a: { a: 'a' } } )` gives `a.a=a`. |
|| Optional replacer function for parsing | Optionally alters final parsed value. | See [Optional replacer function][optional-replacer-function-url]. |
|| Optional replacer function for stringify | Optionally alters final stringified value. | See [Optional replacer function][optional-replacer-function-url]. |
|| Omit nullish value in stringify | By default, all nullish values are omitted when stringify-ing an object. | `stringify({ a: 'a', b: undefined, c: null })` gives `a=a`. |
|| Parse `a[0]=a&a[1]=b` into array | Not supported but it should work. For arrays, use comma-separated value. | `parse('a[0]=a&a[1]=b')` returns `{ a: { 0: 'a', 1: 'b' } }`. |
| 🚧 | Stringify non-JavaScript primitives | Stringifies all non-JavaScript primitives with its best effort. | `stringify({ a() {return;} })` gives `a=a%28%29+%7Breturn%3B%7D`. |

## Usage

### TypeScript or ES Modules

```ts
import { parse } from 'tyqs';
import { parse, stringify } from 'tyqs';

parse('a=a'); // { a: 'a' }
parse('a=a&a=b'); // { a: ['a', 'b'] }
Expand All @@ -63,26 +82,126 @@ parse('a.a=a'); // { a: { a: 'a' } }
parse('a[a]=a'); // { a: { a: 'a' } }
parse('a[a].b=a'); // { a: { a: { b: 'a' } } }
parse('a[a].b=a,b'); // { a: { a: { b: ['a', 'b'] } } }
parse('a=1'); // { a: '1' }
parse('a.a=1'); // { a: { a: '1' } }
parse('a.a[b]=1'); // { a: { a: { b: '1' } } }

stringify({ a: 'a' }); // a=a
stringify({ a: [1, 2] }); // a=1,2
stringify({ a: { a: [1, 2] } }); // a.a=1,2
stringify({ a: 'a', b: undefined, c: null }); // a=a
```

### Optional replacer function

All functions provided accepts an optional `replacer` function to alter the final output of each parameter.

```ts
// parse(searchParams, replacer)
const searchParams = new URLSearchParams('a=1,2,3&b=true&c=&a=4');
const parseOptions = {
replacer({
firstRawValue: [firstRawValue],
key,
rawValue,
value,
}) {
switch (key) {
case 'a': return rawValue.map(n => Number(n));
case 'b': return firstRawValue === 'true';
case 'c': return firstRawValue === '' ? undefined : firstRawValue;
default: return value;
}
},
};

parse(searchParams);
/**
* output:
* {
* a: ['1', '2', '3', '4'],
* b: 'true',
* c: '',
* }
*/

parse(searchParams, parseOptions.replacer);
/**
* output:
* {
* a: [1, 2, 3, 4],
* b: true,
* c: undefined,
* }
*/



// stringify(input, replacer)
const input = {
a: null,
b: undefined,
c: {
a: null,
d: {
a: undefined,
},
},
d() { return; }
};
const stringifyOptions = {
replacer({
rawValue,
value,
key,
flattenedKey,
}) {
if (key === 'b' || flattenedKey === 'c.d.a') return '<nil>';
if (rawValue == null) return '';

/** Returning a nullish value to omit the current key-value pair in the output. */
if (typeof(rawValue) === 'function') return;

return value;
},
};

stringify(input);
/** output: d=d%28%29+%7B+return%3B+%7D */

stringify(input, stringifyOptions.replacer);
/** output: a=&b=%3Cnil%3E&c.a=&c.d.a=%3Cnil%3E */
```

## API Reference

### parse(searchParams[, options])
### parse(searchParams[, replacer])

- `searchParams` <[string][string-mdn-url] | [URLSearchParams]> URL search parameters.
- `options` <?[object][object-mdn-url]> Optional parsing options.
- `singles` <?[Array][array-mdn-url]<[string][string-mdn-url]>> A list of keys that need to be decoded as single string value instead of an array of values.
- `smart` <?[boolean][boolean-mdn-url]> Defaults to true. The decoder will assume all URL search params to be an array of values. With smart mode enabled, it will not force a single-value search param into an array.
- returns: <[object][object-mdn-url]> An object of decoded URL search params from a given string.
- `replacer` <?[Function][function-mdn-url]> Optional replacer function that allows you to alter the final parsed value.
- `firstRawValue` <[Array][array-mdn-url]<[string][string-mdn-url]>> This returns an array of values of the first key-value pair of a given key, e.g. *`a=a&a=b` will return `{ a: ['a'] }`*.
- `key` <[string][string-mdn-url]> Parameter name.
- `rawValue` <[Array][array-mdn-url]<[string][string-mdn-url]>> This returns an array of values from all key-value pairs of the same key, e.g. *`a=a&a=b` will return `{ a: ['a', 'b'] }`*.
- `value` <[string][string-mdn-url] | [Array][array-mdn-url]<[string][string-mdn-url]>> This returns the best value of a given parameter key which is heuristically determined by the library, e.g. *`a=a,b&b=a&a=c` will return `{ a: ['a', 'b', 'c'] }` (an array of values) and `b='a'` (single value)*.
- returns: <[Object][object-mdn-url]> An object of decoded URL search params from a given string.

This method decodes/ parses a string value into an object.
This method decodes/ parses a string value into an object. By default, [URLSearchParams.prototype.getAll] is used to retrieve the values from all key-value pairs of the same name, e.g. *`a=a&a=b` will return `{ a: ['a', 'b'] }`*. As you can see, this approach will be able to get all param values when you define multiple pairs of the same key. However, there is a downside which is when you have just **1** key-value pair and you expect it to be a single-value param, say `a=a&b=b`, will give `{ a: ['a'], b: ['b'] }`. To avoid any confusion, the library automatically parses such single-value param into a single value instead, e.g. *`a=a&b=b` will always give `{ a: 'a', b: 'b' }`*.

### stringify(value)
Under some circumstances, you might want it to behave differently. For that you can alter the outcome with an [optional `replacer` function][optional-replacer-function-url].

### stringify(input[, replacer])

- `value` <`unknown`> Any value of unknown type. It accepts any JavaScript primitives and objects.
- returns: <[string][string-mdn-url]> A string of encoded URL search params from a given input.
- `replacer` <?[Function][function-mdn-url]> Optional replacer function that allows you to alter the final stringified value.
- `flattenedKey` <[string][string-mdn-url]> Flattened key, e.g. *`{ a: { b: { c: 'a' } } }`'s key will be flattened to `a.b.c`*.
- `key` <[string][string-mdn-url]> Parameter name.
- `rawValue` <`unknown`> Raw value of a parameter.
- `value` <[string][string-mdn-url]> Stringified value.
- returns: <[string][string-mdn-url]> A string of encoded URL search params from a given object.

This method encodes/ stringifies an object into a string. When a raw value is nullish, it will be omitted in the stringified output, e.g. *`{ a: 'a', b: null, c: undefined }` will return `a=a` as `null` and `undefined` are nullish values*.

This method encodes/ stringifies an input into a string.
If you want to include nullish values in the stringified output, you can override that with an [optional `replacer` function][optional-replacer-function-url].

## Contributing

Expand All @@ -98,17 +217,22 @@ Please note that this project is released with a [Contributor Code of Conduct][c

<!-- References -->
[ES Modules]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
[optional-replacer-function-url]: #optional-replacer-function
[parse]: #parsesearchparams-replacer
[stringify]: #stringifyinput-replacer
[URLSearchParams.prototype.getAll]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/getAll
[URLSearchParams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams

<!-- MDN -->
[array-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
[map-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
[string-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
[object-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object
[number-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number
[boolean-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[function-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function
[html-style-element-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLStyleElement
[map-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
[number-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number
[object-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object
[promise-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
[string-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String

<!-- Badges -->
[buy-me-a-coffee-badge]: https://img.shields.io/badge/buy%20me%20a-coffee-ff813f?logo=buymeacoffee&style=flat-square
Expand Down
100 changes: 74 additions & 26 deletions src/benchmarks/parse.bench.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { bench } from 'vitest';

import { parse } from '../parse.js';
import type { ParseOptions } from '../types.js';

interface BenchParams {
value: string;
options: ParseOptions;
}

bench('<empty_string>', () => {
parse('');
Expand Down Expand Up @@ -31,32 +37,74 @@ bench('<empty_string>', () => {
});
});

// optional replacer function
([
[
'a=a&a=b',
['a'],
],
[
'a=a,b&a=a',
['a'],
],
[
'a.a=a&a[a]=b',
['a.a'],
],
] as [string, string[]][]).forEach(([value, singles]) => {
bench(`${value};options.singles=${singles.join(',')}`, () => {
parse(value, { singles });
});
});

[
'a=a',
'a=a&a=b',
'a=a,b',
'a.a=a&a.b=b',
].forEach((value) => {
bench(`${value};options.smart=false`, () => {
parse(value, { smart: false });
{
options: {
replacer({ firstRawValue: [fv], key, value }) {
if (key === 'a' ) return fv;
return value;
},
},
value: 'a=a&a=b',
},
{
options: {
replacer({ firstRawValue, key, value }) {
if (key === 'a' ) return firstRawValue;
return value;
},
},
value: 'a=a,b&a=b',
},
{
options: {
replacer({ firstRawValue: [fv], key, value }) {
if (key === 'a.a') return fv;
return value;
},
},
value: 'a.a=a&a[a]=b',
},
{
options: {
replacer({ firstRawValue, key, value }) {
if (key === 'a') return firstRawValue.map(n => Number(n));
return value;
},
},
value: 'a=1,2,3',
},
{
options: {
replacer({ firstRawValue, key, value }) {
if (key === 'a') return firstRawValue.map(n => Number(n));
if (key === 'b') return firstRawValue.at(0) === 'true';
return value;
},
},
value: 'a=1,2,3&b=true',
},
{
options: {
replacer({ firstRawValue: [fv], key, value }) {
switch (key) {
case 'a': return Number(fv);
case 'b.a': return fv === 'true';
case 'c': return fv === '' ? undefined : fv;
case 'd': return fv === 'null' ? null : fv;
case 'e': return fv === 'undefined' ? undefined : fv;
default: return value;
}
},
},
value: 'a=1&b.a=true&c=&d=null&e=undefined',
},
] as BenchParams[]).forEach(({
options,
value,
}) => {
bench(`${value} (options: ${options})`, () => {
parse(value, options.replacer);
});
});
Loading

0 comments on commit ca44c86

Please sign in to comment.