Skip to content

Commit b10bc19

Browse files
Add support for arrayFormat: 'bracket-separator' (#276)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent 828f032 commit b10bc19

6 files changed

Lines changed: 250 additions & 10 deletions

File tree

benchmark.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const TEST_STRING = stringify(TEST_OBJECT);
2020
const TEST_BRACKETS_STRING = stringify(TEST_OBJECT, {arrayFormat: 'bracket'});
2121
const TEST_INDEX_STRING = stringify(TEST_OBJECT, {arrayFormat: 'index'});
2222
const TEST_COMMA_STRING = stringify(TEST_OBJECT, {arrayFormat: 'comma'});
23+
const TEST_BRACKET_SEPARATOR_STRING = stringify(TEST_OBJECT, {arrayFormat: 'bracket-separator'});
2324
const TEST_URL = stringifyUrl({url: TEST_HOST, query: TEST_OBJECT});
2425

2526
// Creates a test case and adds it to the suite
@@ -41,6 +42,7 @@ defineTestCase('parse', TEST_STRING, {decode: false});
4142
defineTestCase('parse', TEST_BRACKETS_STRING, {arrayFormat: 'bracket'});
4243
defineTestCase('parse', TEST_INDEX_STRING, {arrayFormat: 'index'});
4344
defineTestCase('parse', TEST_COMMA_STRING, {arrayFormat: 'comma'});
45+
defineTestCase('parse', TEST_BRACKET_SEPARATOR_STRING, {arrayFormat: 'bracket-separator'});
4446

4547
// Stringify
4648
defineTestCase('stringify', TEST_OBJECT);
@@ -51,6 +53,7 @@ defineTestCase('stringify', TEST_OBJECT, {skipEmptyString: true});
5153
defineTestCase('stringify', TEST_OBJECT, {arrayFormat: 'bracket'});
5254
defineTestCase('stringify', TEST_OBJECT, {arrayFormat: 'index'});
5355
defineTestCase('stringify', TEST_OBJECT, {arrayFormat: 'comma'});
56+
defineTestCase('stringify', TEST_OBJECT, {arrayFormat: 'bracket-separator'});
5457

5558
// Extract
5659
defineTestCase('extract', TEST_URL);
@@ -66,7 +69,7 @@ suite.on('cycle', event => {
6669
const {name, hz} = event.target;
6770
const opsPerSec = Math.round(hz).toLocaleString();
6871

69-
console.log(name.padEnd(36, '_') + opsPerSec.padStart(12, '_') + ' ops/s');
72+
console.log(name.padEnd(46, '_') + opsPerSec.padStart(3, '_') + ' ops/s');
7073
});
7174

7275
suite.run();

index.d.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,30 @@ export interface ParseOptions {
4545
//=> {foo: ['1', '2', '3']}
4646
```
4747
48+
- `bracket-separator`: Parse arrays (that are explicitly marked with brackets) with elements separated by a custom character:
49+
50+
```
51+
import queryString = require('query-string');
52+
53+
queryString.parse('foo[]', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
54+
//=> {foo: []}
55+
56+
queryString.parse('foo[]=', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
57+
//=> {foo: ['']}
58+
59+
queryString.parse('foo[]=1', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
60+
//=> {foo: ['1']}
61+
62+
queryString.parse('foo[]=1|2|3', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
63+
//=> {foo: ['1', '2', '3']}
64+
65+
queryString.parse('foo[]=1||3|||6', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
66+
//=> {foo: ['1', '', 3, '', '', '6']}
67+
68+
queryString.parse('foo[]=1|2|3&bar=fluffy&baz[]=4', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
69+
//=> {foo: ['1', '2', '3'], bar: 'fluffy', baz:['4']}
70+
```
71+
4872
- `none`: Parse arrays with elements using duplicate keys:
4973
5074
```
@@ -54,7 +78,7 @@ export interface ParseOptions {
5478
//=> {foo: ['1', '2', '3']}
5579
```
5680
*/
57-
readonly arrayFormat?: 'bracket' | 'index' | 'comma' | 'separator' | 'none';
81+
readonly arrayFormat?: 'bracket' | 'index' | 'comma' | 'separator' | 'bracket-separator' | 'none';
5882

5983
/**
6084
The character used to separate array elements when using `{arrayFormat: 'separator'}`.
@@ -236,7 +260,7 @@ export interface StringifyOptions {
236260
// and `.parse('foo=1,,')` would return `{foo: [1, '', '']}`.
237261
```
238262
239-
- `separator`: Serialize arrays by separating elements with character:
263+
- `separator`: Serialize arrays by separating elements with character:
240264
241265
```
242266
import queryString = require('query-string');
@@ -245,6 +269,33 @@ export interface StringifyOptions {
245269
//=> 'foo=1|2|3'
246270
```
247271
272+
- `bracket-separator`: Serialize arrays by explicitly post-fixing array names with brackets and separating elements with a custom character:
273+
274+
```
275+
import queryString = require('query-string');
276+
277+
queryString.stringify({foo: []}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
278+
//=> 'foo[]'
279+
280+
queryString.stringify({foo: ['']}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
281+
//=> 'foo[]='
282+
283+
queryString.stringify({foo: [1]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
284+
//=> 'foo[]=1'
285+
286+
queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
287+
//=> 'foo[]=1|2|3'
288+
289+
queryString.stringify({foo: [1, '', 3, null, null, 6]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
290+
//=> 'foo[]=1||3|||6'
291+
292+
queryString.stringify({foo: [1, '', 3, null, null, 6]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|', skipNull: true});
293+
//=> 'foo[]=1||3|6'
294+
295+
queryString.stringify({foo: [1, 2, 3], bar: 'fluffy', baz: [4]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
296+
//=> 'foo[]=1|2|3&bar=fluffy&baz[]=4'
297+
```
298+
248299
- `none`: Serialize arrays by using duplicate keys:
249300
250301
```
@@ -254,7 +305,7 @@ export interface StringifyOptions {
254305
//=> 'foo=1&foo=2&foo=3'
255306
```
256307
*/
257-
readonly arrayFormat?: 'bracket' | 'index' | 'comma' | 'separator' | 'none';
308+
readonly arrayFormat?: 'bracket' | 'index' | 'comma' | 'separator' | 'bracket-separator' | 'none';
258309

259310
/**
260311
The character used to separate array elements when using `{arrayFormat: 'separator'}`.

index.js

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ function encoderForArrayFormat(options) {
4949

5050
case 'comma':
5151
case 'separator':
52+
case 'bracket-separator': {
53+
const keyValueSep = options.arrayFormat === 'bracket-separator' ?
54+
'[]=' :
55+
'=';
56+
5257
return key => (result, value) => {
5358
if (
5459
value === undefined ||
@@ -58,16 +63,16 @@ function encoderForArrayFormat(options) {
5863
return result;
5964
}
6065

61-
if (result.length === 0) {
62-
return [[encode(key, options), '=', encode(value === null ? '' : value, options)].join('')];
63-
}
66+
// Translate null to an empty string so that it doesn't serialize as 'null'
67+
value = value === null ? '' : value;
6468

65-
if (value === null || value === '') {
66-
return [[result, ''].join(options.arrayFormatSeparator)];
69+
if (result.length === 0) {
70+
return [[encode(key, options), keyValueSep, encode(value, options)].join('')];
6771
}
6872

6973
return [[result, encode(value, options)].join(options.arrayFormatSeparator)];
7074
};
75+
}
7176

7277
default:
7378
return key => (result, value) => {
@@ -138,6 +143,28 @@ function parserForArrayFormat(options) {
138143
accumulator[key] = newValue;
139144
};
140145

146+
case 'bracket-separator':
147+
return (key, value, accumulator) => {
148+
const isArray = /(\[\])$/.test(key);
149+
key = key.replace(/\[\]$/, '');
150+
151+
if (!isArray) {
152+
accumulator[key] = value ? decode(value, options) : value;
153+
return;
154+
}
155+
156+
const arrayValue = value === null ?
157+
[] :
158+
value.split(options.arrayFormatSeparator).map(item => decode(item, options));
159+
160+
if (accumulator[key] === undefined) {
161+
accumulator[key] = arrayValue;
162+
return;
163+
}
164+
165+
accumulator[key] = [].concat(accumulator[key], arrayValue);
166+
};
167+
141168
default:
142169
return (key, value, accumulator) => {
143170
if (accumulator[key] === undefined) {
@@ -261,7 +288,7 @@ function parse(query, options) {
261288

262289
// Missing `=` should be `null`:
263290
// http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters
264-
value = value === undefined ? null : ['comma', 'separator'].includes(options.arrayFormat) ? value : decode(value, options);
291+
value = value === undefined ? null : ['comma', 'separator', 'bracket-separator'].includes(options.arrayFormat) ? value : decode(value, options);
265292
formatter(decode(key, options), value, ret);
266293
}
267294

@@ -343,6 +370,10 @@ exports.stringify = (object, options) => {
343370
}
344371

345372
if (Array.isArray(value)) {
373+
if (value.length === 0 && options.arrayFormat === 'bracket-separator') {
374+
return encode(key, options) + '[]';
375+
}
376+
346377
return value
347378
.reduce(formatter(key), [])
348379
.join('&');

readme.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,30 @@ queryString.parse('foo=1|2|3', {arrayFormat: 'separator', arrayFormatSeparator:
138138
//=> {foo: ['1', '2', '3']}
139139
```
140140

141+
- `'bracket-separator'`: Parse arrays (that are explicitly marked with brackets) with elements separated by a custom character:
142+
143+
```js
144+
const queryString = require('query-string');
145+
146+
queryString.parse('foo[]', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
147+
//=> {foo: []}
148+
149+
queryString.parse('foo[]=', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
150+
//=> {foo: ['']}
151+
152+
queryString.parse('foo[]=1', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
153+
//=> {foo: ['1']}
154+
155+
queryString.parse('foo[]=1|2|3', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
156+
//=> {foo: ['1', '2', '3']}
157+
158+
queryString.parse('foo[]=1||3|||6', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
159+
//=> {foo: ['1', '', 3, '', '', '6']}
160+
161+
queryString.parse('foo[]=1|2|3&bar=fluffy&baz[]=4', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
162+
//=> {foo: ['1', '2', '3'], bar: 'fluffy', baz:['4']}
163+
```
164+
141165
- `'none'`: Parse arrays with elements using duplicate keys:
142166

143167
```js
@@ -248,6 +272,42 @@ queryString.stringify({foo: [1, null, '']}, {arrayFormat: 'comma'});
248272
// and `.parse('foo=1,,')` would return `{foo: [1, '', '']}`.
249273
```
250274

275+
- `'separator'`: Serialize arrays by separating elements with a custom character:
276+
277+
```js
278+
const queryString = require('query-string');
279+
280+
queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'separator', arrayFormatSeparator: '|'});
281+
//=> 'foo=1|2|3'
282+
```
283+
284+
- `'bracket-separator'`: Serialize arrays by explicitly post-fixing array names with brackets and separating elements with a custom character:
285+
286+
```js
287+
const queryString = require('query-string');
288+
289+
queryString.stringify({foo: []}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
290+
//=> 'foo[]'
291+
292+
queryString.stringify({foo: ['']}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
293+
//=> 'foo[]='
294+
295+
queryString.stringify({foo: [1]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
296+
//=> 'foo[]=1'
297+
298+
queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
299+
//=> 'foo[]=1|2|3'
300+
301+
queryString.stringify({foo: [1, '', 3, null, null, 6]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
302+
//=> 'foo[]=1||3|||6'
303+
304+
queryString.stringify({foo: [1, '', 3, null, null, 6]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|', skipNull: true});
305+
//=> 'foo[]=1||3|6'
306+
307+
queryString.stringify({foo: [1, 2, 3], bar: 'fluffy', baz: [4]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
308+
//=> 'foo[]=1|2|3&bar=fluffy&baz[]=4'
309+
```
310+
251311
- `'none'`: Serialize arrays by using duplicate keys:
252312

253313
```js

test/parse.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,36 @@ test('query strings having indexed arrays and format option as `index`', t => {
184184
}), {foo: ['bar', 'baz']});
185185
});
186186

187+
test('query strings having brackets+separator arrays and format option as `bracket-separator` with 1 value', t => {
188+
t.deepEqual(queryString.parse('foo[]=bar', {
189+
arrayFormat: 'bracket-separator'
190+
}), {foo: ['bar']});
191+
});
192+
193+
test('query strings having brackets+separator arrays and format option as `bracket-separator` with multiple values', t => {
194+
t.deepEqual(queryString.parse('foo[]=bar,baz,,,biz', {
195+
arrayFormat: 'bracket-separator'
196+
}), {foo: ['bar', 'baz', '', '', 'biz']});
197+
});
198+
199+
test('query strings with multiple brackets+separator arrays and format option as `bracket-separator` using same key name', t => {
200+
t.deepEqual(queryString.parse('foo[]=bar,baz&foo[]=biz,boz', {
201+
arrayFormat: 'bracket-separator'
202+
}), {foo: ['bar', 'baz', 'biz', 'boz']});
203+
});
204+
205+
test('query strings having an empty brackets+separator array and format option as `bracket-separator`', t => {
206+
t.deepEqual(queryString.parse('foo[]', {
207+
arrayFormat: 'bracket-separator'
208+
}), {foo: []});
209+
});
210+
211+
test('query strings having a brackets+separator array and format option as `bracket-separator` with a single empty string', t => {
212+
t.deepEqual(queryString.parse('foo[]=', {
213+
arrayFormat: 'bracket-separator'
214+
}), {foo: ['']});
215+
});
216+
187217
test('query strings having = within parameters (i.e. GraphQL IDs)', t => {
188218
t.deepEqual(queryString.parse('foo=bar=&foo=ba=z='), {foo: ['bar=', 'ba=z=']});
189219
});

test/stringify.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,71 @@ test('array stringify representation with array indexes and sparse array', t =>
172172
t.is(queryString.stringify({bar: fixture}, {arrayFormat: 'index'}), 'bar[0]=one&bar[1]=two&bar[2]=three');
173173
});
174174

175+
test('array stringify representation with brackets and separators with empty array', t => {
176+
t.is(queryString.stringify({
177+
foo: null,
178+
bar: []
179+
}, {
180+
arrayFormat: 'bracket-separator'
181+
}), 'bar[]&foo');
182+
});
183+
184+
test('array stringify representation with brackets and separators with single value', t => {
185+
t.is(queryString.stringify({
186+
foo: null,
187+
bar: ['one']
188+
}, {
189+
arrayFormat: 'bracket-separator'
190+
}), 'bar[]=one&foo');
191+
});
192+
193+
test('array stringify representation with brackets and separators with multiple values', t => {
194+
t.is(queryString.stringify({
195+
foo: null,
196+
bar: ['one', 'two', 'three']
197+
}, {
198+
arrayFormat: 'bracket-separator'
199+
}), 'bar[]=one,two,three&foo');
200+
});
201+
202+
test('array stringify representation with brackets and separators with a single empty string', t => {
203+
t.is(queryString.stringify({
204+
foo: null,
205+
bar: ['']
206+
}, {
207+
arrayFormat: 'bracket-separator'
208+
}), 'bar[]=&foo');
209+
});
210+
211+
test('array stringify representation with brackets and separators with a multiple empty string', t => {
212+
t.is(queryString.stringify({
213+
foo: null,
214+
bar: ['', 'two', '']
215+
}, {
216+
arrayFormat: 'bracket-separator'
217+
}), 'bar[]=,two,&foo');
218+
});
219+
220+
test('array stringify representation with brackets and separators with dropped empty strings', t => {
221+
t.is(queryString.stringify({
222+
foo: null,
223+
bar: ['', 'two', '']
224+
}, {
225+
arrayFormat: 'bracket-separator',
226+
skipEmptyString: true
227+
}), 'bar[]=two&foo');
228+
});
229+
230+
test('array stringify representation with brackets and separators with dropped null values', t => {
231+
t.is(queryString.stringify({
232+
foo: null,
233+
bar: ['one', null, 'three', null, '', 'six']
234+
}, {
235+
arrayFormat: 'bracket-separator',
236+
skipNull: true
237+
}), 'bar[]=one,three,,six');
238+
});
239+
175240
test('should sort keys in given order', t => {
176241
const fixture = ['c', 'a', 'b'];
177242
const sort = (key1, key2) => fixture.indexOf(key1) - fixture.indexOf(key2);

0 commit comments

Comments
 (0)