Skip to content

Commit ca44c86

Browse files
authored
feat: add optional replacer function in parse and stringify functions (#16)
* feat: add optional replacer function in stringify function * feat: add optional replacer function in parse function Signed-off-by: Rong Sen Ng (motss) <wes.ngrongsen@gmail.com>
1 parent 620032e commit ca44c86

File tree

9 files changed

+451
-124
lines changed

9 files changed

+451
-124
lines changed

.changeset/angry-foxes-tap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tyqs": minor
3+
---
4+
5+
feat: add optional replacer function in stringify function

.changeset/nine-rockets-camp.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tyqs": minor
3+
---
4+
5+
feat: add optional replacer function in parse function

README.md

Lines changed: 140 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@
2828

2929
- [Pre-requisite](#pre-requisite)
3030
- [Install](#install)
31+
- [Features](#features)
3132
- [Usage](#usage)
3233
- [TypeScript or ES Modules](#typescript-or-es-modules)
34+
- [Optional replacer function](#optional-replacer-function)
3335
- [API Reference](#api-reference)
34-
- [parse(searchParams\[, options\])](#parsesearchparams-options)
35-
- [stringify(value)](#stringifyvalue)
36+
- [parse(searchParams\[, replacer\])](#parsesearchparams-replacer)
37+
- [stringify(input\[, replacer\])](#stringifyinput-replacer)
3638
- [Contributing](#contributing)
3739
- [Code of Conduct](#code-of-conduct)
3840
- [License](#license)
@@ -49,12 +51,29 @@
4951
$ npm i tyqs
5052
```
5153

54+
## Features
55+
56+
| Support | Feature | Description | Example |
57+
| --- | --- | --- | --- |
58+
|| [parse] | Decodes URL search params into an object. | `parse('a=a&b=1')` returns `{ a: 'a', b: '1' }`. |
59+
|| [stringify] | Encodes an object into URL search params. | `stringify({ a: 'a', b: 1 })` gives `a=a&b=1`. |
60+
|| Parse multiple values | Parses comma-separated param into an array of values. | `parse('a=a,b')` returns `{ a: ['a', 'b'] }`. |
61+
|| Parse single value | Parses single-value param into a string. | `parse('a=a')` returns `{ a: 'a' }`. |
62+
|| 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'] }`. |
63+
|| 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' } } } }`. |
64+
|| Stringify nested params | Stringifies nested params with dot or bracket notation. | `stringify({ a: { a: 'a' } } )` gives `a.a=a`. |
65+
|| Optional replacer function for parsing | Optionally alters final parsed value. | See [Optional replacer function][optional-replacer-function-url]. |
66+
|| Optional replacer function for stringify | Optionally alters final stringified value. | See [Optional replacer function][optional-replacer-function-url]. |
67+
|| 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`. |
68+
|| 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' } }`. |
69+
| 🚧 | Stringify non-JavaScript primitives | Stringifies all non-JavaScript primitives with its best effort. | `stringify({ a() {return;} })` gives `a=a%28%29+%7Breturn%3B%7D`. |
70+
5271
## Usage
5372

5473
### TypeScript or ES Modules
5574

5675
```ts
57-
import { parse } from 'tyqs';
76+
import { parse, stringify } from 'tyqs';
5877

5978
parse('a=a'); // { a: 'a' }
6079
parse('a=a&a=b'); // { a: ['a', 'b'] }
@@ -63,26 +82,126 @@ parse('a.a=a'); // { a: { a: 'a' } }
6382
parse('a[a]=a'); // { a: { a: 'a' } }
6483
parse('a[a].b=a'); // { a: { a: { b: 'a' } } }
6584
parse('a[a].b=a,b'); // { a: { a: { b: ['a', 'b'] } } }
85+
parse('a=1'); // { a: '1' }
86+
parse('a.a=1'); // { a: { a: '1' } }
87+
parse('a.a[b]=1'); // { a: { a: { b: '1' } } }
88+
89+
stringify({ a: 'a' }); // a=a
90+
stringify({ a: [1, 2] }); // a=1,2
91+
stringify({ a: { a: [1, 2] } }); // a.a=1,2
92+
stringify({ a: 'a', b: undefined, c: null }); // a=a
93+
```
94+
95+
### Optional replacer function
96+
97+
All functions provided accepts an optional `replacer` function to alter the final output of each parameter.
98+
99+
```ts
100+
// parse(searchParams, replacer)
101+
const searchParams = new URLSearchParams('a=1,2,3&b=true&c=&a=4');
102+
const parseOptions = {
103+
replacer({
104+
firstRawValue: [firstRawValue],
105+
key,
106+
rawValue,
107+
value,
108+
}) {
109+
switch (key) {
110+
case 'a': return rawValue.map(n => Number(n));
111+
case 'b': return firstRawValue === 'true';
112+
case 'c': return firstRawValue === '' ? undefined : firstRawValue;
113+
default: return value;
114+
}
115+
},
116+
};
117+
118+
parse(searchParams);
119+
/**
120+
* output:
121+
* {
122+
* a: ['1', '2', '3', '4'],
123+
* b: 'true',
124+
* c: '',
125+
* }
126+
*/
127+
128+
parse(searchParams, parseOptions.replacer);
129+
/**
130+
* output:
131+
* {
132+
* a: [1, 2, 3, 4],
133+
* b: true,
134+
* c: undefined,
135+
* }
136+
*/
137+
138+
139+
140+
// stringify(input, replacer)
141+
const input = {
142+
a: null,
143+
b: undefined,
144+
c: {
145+
a: null,
146+
d: {
147+
a: undefined,
148+
},
149+
},
150+
d() { return; }
151+
};
152+
const stringifyOptions = {
153+
replacer({
154+
rawValue,
155+
value,
156+
key,
157+
flattenedKey,
158+
}) {
159+
if (key === 'b' || flattenedKey === 'c.d.a') return '<nil>';
160+
if (rawValue == null) return '';
161+
162+
/** Returning a nullish value to omit the current key-value pair in the output. */
163+
if (typeof(rawValue) === 'function') return;
164+
165+
return value;
166+
},
167+
};
168+
169+
stringify(input);
170+
/** output: d=d%28%29+%7B+return%3B+%7D */
171+
172+
stringify(input, stringifyOptions.replacer);
173+
/** output: a=&b=%3Cnil%3E&c.a=&c.d.a=%3Cnil%3E */
66174
```
67175

68176
## API Reference
69177

70-
### parse(searchParams[, options])
178+
### parse(searchParams[, replacer])
71179

72180
- `searchParams` <[string][string-mdn-url] | [URLSearchParams]> URL search parameters.
73-
- `options` <?[object][object-mdn-url]> Optional parsing options.
74-
- `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.
75-
- `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.
76-
- returns: <[object][object-mdn-url]> An object of decoded URL search params from a given string.
181+
- `replacer` <?[Function][function-mdn-url]> Optional replacer function that allows you to alter the final parsed value.
182+
- `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'] }`*.
183+
- `key` <[string][string-mdn-url]> Parameter name.
184+
- `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'] }`*.
185+
- `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)*.
186+
- returns: <[Object][object-mdn-url]> An object of decoded URL search params from a given string.
77187

78-
This method decodes/ parses a string value into an object.
188+
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' }`*.
79189

80-
### stringify(value)
190+
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].
191+
192+
### stringify(input[, replacer])
81193

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

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

87206
## Contributing
88207

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

99218
<!-- References -->
100219
[ES Modules]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
220+
[optional-replacer-function-url]: #optional-replacer-function
221+
[parse]: #parsesearchparams-replacer
222+
[stringify]: #stringifyinput-replacer
223+
[URLSearchParams.prototype.getAll]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/getAll
101224
[URLSearchParams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
102225

103226
<!-- MDN -->
104227
[array-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
105-
[map-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
106-
[string-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
107-
[object-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object
108-
[number-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number
109228
[boolean-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean
229+
[function-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function
110230
[html-style-element-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLStyleElement
231+
[map-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
232+
[number-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number
233+
[object-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object
111234
[promise-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
235+
[string-mdn-url]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
112236

113237
<!-- Badges -->
114238
[buy-me-a-coffee-badge]: https://img.shields.io/badge/buy%20me%20a-coffee-ff813f?logo=buymeacoffee&style=flat-square

src/benchmarks/parse.bench.ts

Lines changed: 74 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { bench } from 'vitest';
22

33
import { parse } from '../parse.js';
4+
import type { ParseOptions } from '../types.js';
5+
6+
interface BenchParams {
7+
value: string;
8+
options: ParseOptions;
9+
}
410

511
bench('<empty_string>', () => {
612
parse('');
@@ -31,32 +37,74 @@ bench('<empty_string>', () => {
3137
});
3238
});
3339

40+
// optional replacer function
3441
([
35-
[
36-
'a=a&a=b',
37-
['a'],
38-
],
39-
[
40-
'a=a,b&a=a',
41-
['a'],
42-
],
43-
[
44-
'a.a=a&a[a]=b',
45-
['a.a'],
46-
],
47-
] as [string, string[]][]).forEach(([value, singles]) => {
48-
bench(`${value};options.singles=${singles.join(',')}`, () => {
49-
parse(value, { singles });
50-
});
51-
});
52-
53-
[
54-
'a=a',
55-
'a=a&a=b',
56-
'a=a,b',
57-
'a.a=a&a.b=b',
58-
].forEach((value) => {
59-
bench(`${value};options.smart=false`, () => {
60-
parse(value, { smart: false });
42+
{
43+
options: {
44+
replacer({ firstRawValue: [fv], key, value }) {
45+
if (key === 'a' ) return fv;
46+
return value;
47+
},
48+
},
49+
value: 'a=a&a=b',
50+
},
51+
{
52+
options: {
53+
replacer({ firstRawValue, key, value }) {
54+
if (key === 'a' ) return firstRawValue;
55+
return value;
56+
},
57+
},
58+
value: 'a=a,b&a=b',
59+
},
60+
{
61+
options: {
62+
replacer({ firstRawValue: [fv], key, value }) {
63+
if (key === 'a.a') return fv;
64+
return value;
65+
},
66+
},
67+
value: 'a.a=a&a[a]=b',
68+
},
69+
{
70+
options: {
71+
replacer({ firstRawValue, key, value }) {
72+
if (key === 'a') return firstRawValue.map(n => Number(n));
73+
return value;
74+
},
75+
},
76+
value: 'a=1,2,3',
77+
},
78+
{
79+
options: {
80+
replacer({ firstRawValue, key, value }) {
81+
if (key === 'a') return firstRawValue.map(n => Number(n));
82+
if (key === 'b') return firstRawValue.at(0) === 'true';
83+
return value;
84+
},
85+
},
86+
value: 'a=1,2,3&b=true',
87+
},
88+
{
89+
options: {
90+
replacer({ firstRawValue: [fv], key, value }) {
91+
switch (key) {
92+
case 'a': return Number(fv);
93+
case 'b.a': return fv === 'true';
94+
case 'c': return fv === '' ? undefined : fv;
95+
case 'd': return fv === 'null' ? null : fv;
96+
case 'e': return fv === 'undefined' ? undefined : fv;
97+
default: return value;
98+
}
99+
},
100+
},
101+
value: 'a=1&b.a=true&c=&d=null&e=undefined',
102+
},
103+
] as BenchParams[]).forEach(({
104+
options,
105+
value,
106+
}) => {
107+
bench(`${value} (options: ${options})`, () => {
108+
parse(value, options.replacer);
61109
});
62110
});

0 commit comments

Comments
 (0)