Skip to content

Commit

Permalink
Add support for arrayFormat: 'bracket-separator' (#276)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
DV8FromTheWorld and sindresorhus committed Mar 18, 2021
1 parent 828f032 commit b10bc19
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 10 deletions.
5 changes: 4 additions & 1 deletion benchmark.js
Expand Up @@ -20,6 +20,7 @@ const TEST_STRING = stringify(TEST_OBJECT);
const TEST_BRACKETS_STRING = stringify(TEST_OBJECT, {arrayFormat: 'bracket'});
const TEST_INDEX_STRING = stringify(TEST_OBJECT, {arrayFormat: 'index'});
const TEST_COMMA_STRING = stringify(TEST_OBJECT, {arrayFormat: 'comma'});
const TEST_BRACKET_SEPARATOR_STRING = stringify(TEST_OBJECT, {arrayFormat: 'bracket-separator'});
const TEST_URL = stringifyUrl({url: TEST_HOST, query: TEST_OBJECT});

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

// Stringify
defineTestCase('stringify', TEST_OBJECT);
Expand All @@ -51,6 +53,7 @@ defineTestCase('stringify', TEST_OBJECT, {skipEmptyString: true});
defineTestCase('stringify', TEST_OBJECT, {arrayFormat: 'bracket'});
defineTestCase('stringify', TEST_OBJECT, {arrayFormat: 'index'});
defineTestCase('stringify', TEST_OBJECT, {arrayFormat: 'comma'});
defineTestCase('stringify', TEST_OBJECT, {arrayFormat: 'bracket-separator'});

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

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

suite.run();
57 changes: 54 additions & 3 deletions index.d.ts
Expand Up @@ -45,6 +45,30 @@ export interface ParseOptions {
//=> {foo: ['1', '2', '3']}
```
- `bracket-separator`: Parse arrays (that are explicitly marked with brackets) with elements separated by a custom character:
```
import queryString = require('query-string');
queryString.parse('foo[]', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: []}
queryString.parse('foo[]=', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: ['']}
queryString.parse('foo[]=1', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: ['1']}
queryString.parse('foo[]=1|2|3', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: ['1', '2', '3']}
queryString.parse('foo[]=1||3|||6', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: ['1', '', 3, '', '', '6']}
queryString.parse('foo[]=1|2|3&bar=fluffy&baz[]=4', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: ['1', '2', '3'], bar: 'fluffy', baz:['4']}
```
- `none`: Parse arrays with elements using duplicate keys:
```
Expand All @@ -54,7 +78,7 @@ export interface ParseOptions {
//=> {foo: ['1', '2', '3']}
```
*/
readonly arrayFormat?: 'bracket' | 'index' | 'comma' | 'separator' | 'none';
readonly arrayFormat?: 'bracket' | 'index' | 'comma' | 'separator' | 'bracket-separator' | 'none';

/**
The character used to separate array elements when using `{arrayFormat: 'separator'}`.
Expand Down Expand Up @@ -236,7 +260,7 @@ export interface StringifyOptions {
// and `.parse('foo=1,,')` would return `{foo: [1, '', '']}`.
```
- `separator`: Serialize arrays by separating elements with character:
- `separator`: Serialize arrays by separating elements with character:
```
import queryString = require('query-string');
Expand All @@ -245,6 +269,33 @@ export interface StringifyOptions {
//=> 'foo=1|2|3'
```
- `bracket-separator`: Serialize arrays by explicitly post-fixing array names with brackets and separating elements with a custom character:
```
import queryString = require('query-string');
queryString.stringify({foo: []}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> 'foo[]'
queryString.stringify({foo: ['']}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> 'foo[]='
queryString.stringify({foo: [1]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> 'foo[]=1'
queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> 'foo[]=1|2|3'
queryString.stringify({foo: [1, '', 3, null, null, 6]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> 'foo[]=1||3|||6'
queryString.stringify({foo: [1, '', 3, null, null, 6]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|', skipNull: true});
//=> 'foo[]=1||3|6'
queryString.stringify({foo: [1, 2, 3], bar: 'fluffy', baz: [4]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> 'foo[]=1|2|3&bar=fluffy&baz[]=4'
```
- `none`: Serialize arrays by using duplicate keys:
```
Expand All @@ -254,7 +305,7 @@ export interface StringifyOptions {
//=> 'foo=1&foo=2&foo=3'
```
*/
readonly arrayFormat?: 'bracket' | 'index' | 'comma' | 'separator' | 'none';
readonly arrayFormat?: 'bracket' | 'index' | 'comma' | 'separator' | 'bracket-separator' | 'none';

/**
The character used to separate array elements when using `{arrayFormat: 'separator'}`.
Expand Down
43 changes: 37 additions & 6 deletions index.js
Expand Up @@ -49,6 +49,11 @@ function encoderForArrayFormat(options) {

case 'comma':
case 'separator':
case 'bracket-separator': {
const keyValueSep = options.arrayFormat === 'bracket-separator' ?
'[]=' :
'=';

return key => (result, value) => {
if (
value === undefined ||
Expand All @@ -58,16 +63,16 @@ function encoderForArrayFormat(options) {
return result;
}

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

if (value === null || value === '') {
return [[result, ''].join(options.arrayFormatSeparator)];
if (result.length === 0) {
return [[encode(key, options), keyValueSep, encode(value, options)].join('')];
}

return [[result, encode(value, options)].join(options.arrayFormatSeparator)];
};
}

default:
return key => (result, value) => {
Expand Down Expand Up @@ -138,6 +143,28 @@ function parserForArrayFormat(options) {
accumulator[key] = newValue;
};

case 'bracket-separator':
return (key, value, accumulator) => {
const isArray = /(\[\])$/.test(key);
key = key.replace(/\[\]$/, '');

if (!isArray) {
accumulator[key] = value ? decode(value, options) : value;
return;
}

const arrayValue = value === null ?
[] :
value.split(options.arrayFormatSeparator).map(item => decode(item, options));

if (accumulator[key] === undefined) {
accumulator[key] = arrayValue;
return;
}

accumulator[key] = [].concat(accumulator[key], arrayValue);
};

default:
return (key, value, accumulator) => {
if (accumulator[key] === undefined) {
Expand Down Expand Up @@ -261,7 +288,7 @@ function parse(query, options) {

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

Expand Down Expand Up @@ -343,6 +370,10 @@ exports.stringify = (object, options) => {
}

if (Array.isArray(value)) {
if (value.length === 0 && options.arrayFormat === 'bracket-separator') {
return encode(key, options) + '[]';
}

return value
.reduce(formatter(key), [])
.join('&');
Expand Down
60 changes: 60 additions & 0 deletions readme.md
Expand Up @@ -138,6 +138,30 @@ queryString.parse('foo=1|2|3', {arrayFormat: 'separator', arrayFormatSeparator:
//=> {foo: ['1', '2', '3']}
```

- `'bracket-separator'`: Parse arrays (that are explicitly marked with brackets) with elements separated by a custom character:

```js
const queryString = require('query-string');

queryString.parse('foo[]', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: []}

queryString.parse('foo[]=', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: ['']}

queryString.parse('foo[]=1', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: ['1']}

queryString.parse('foo[]=1|2|3', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: ['1', '2', '3']}

queryString.parse('foo[]=1||3|||6', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: ['1', '', 3, '', '', '6']}

queryString.parse('foo[]=1|2|3&bar=fluffy&baz[]=4', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: ['1', '2', '3'], bar: 'fluffy', baz:['4']}
```

- `'none'`: Parse arrays with elements using duplicate keys:

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

- `'separator'`: Serialize arrays by separating elements with a custom character:

```js
const queryString = require('query-string');

queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'separator', arrayFormatSeparator: '|'});
//=> 'foo=1|2|3'
```

- `'bracket-separator'`: Serialize arrays by explicitly post-fixing array names with brackets and separating elements with a custom character:

```js
const queryString = require('query-string');

queryString.stringify({foo: []}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> 'foo[]'

queryString.stringify({foo: ['']}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> 'foo[]='

queryString.stringify({foo: [1]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> 'foo[]=1'

queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> 'foo[]=1|2|3'

queryString.stringify({foo: [1, '', 3, null, null, 6]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> 'foo[]=1||3|||6'

queryString.stringify({foo: [1, '', 3, null, null, 6]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|', skipNull: true});
//=> 'foo[]=1||3|6'

queryString.stringify({foo: [1, 2, 3], bar: 'fluffy', baz: [4]}, {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> 'foo[]=1|2|3&bar=fluffy&baz[]=4'
```

- `'none'`: Serialize arrays by using duplicate keys:

```js
Expand Down
30 changes: 30 additions & 0 deletions test/parse.js
Expand Up @@ -184,6 +184,36 @@ test('query strings having indexed arrays and format option as `index`', t => {
}), {foo: ['bar', 'baz']});
});

test('query strings having brackets+separator arrays and format option as `bracket-separator` with 1 value', t => {
t.deepEqual(queryString.parse('foo[]=bar', {
arrayFormat: 'bracket-separator'
}), {foo: ['bar']});
});

test('query strings having brackets+separator arrays and format option as `bracket-separator` with multiple values', t => {
t.deepEqual(queryString.parse('foo[]=bar,baz,,,biz', {
arrayFormat: 'bracket-separator'
}), {foo: ['bar', 'baz', '', '', 'biz']});
});

test('query strings with multiple brackets+separator arrays and format option as `bracket-separator` using same key name', t => {
t.deepEqual(queryString.parse('foo[]=bar,baz&foo[]=biz,boz', {
arrayFormat: 'bracket-separator'
}), {foo: ['bar', 'baz', 'biz', 'boz']});
});

test('query strings having an empty brackets+separator array and format option as `bracket-separator`', t => {
t.deepEqual(queryString.parse('foo[]', {
arrayFormat: 'bracket-separator'
}), {foo: []});
});

test('query strings having a brackets+separator array and format option as `bracket-separator` with a single empty string', t => {
t.deepEqual(queryString.parse('foo[]=', {
arrayFormat: 'bracket-separator'
}), {foo: ['']});
});

test('query strings having = within parameters (i.e. GraphQL IDs)', t => {
t.deepEqual(queryString.parse('foo=bar=&foo=ba=z='), {foo: ['bar=', 'ba=z=']});
});
Expand Down
65 changes: 65 additions & 0 deletions test/stringify.js
Expand Up @@ -172,6 +172,71 @@ test('array stringify representation with array indexes and sparse array', t =>
t.is(queryString.stringify({bar: fixture}, {arrayFormat: 'index'}), 'bar[0]=one&bar[1]=two&bar[2]=three');
});

test('array stringify representation with brackets and separators with empty array', t => {
t.is(queryString.stringify({
foo: null,
bar: []
}, {
arrayFormat: 'bracket-separator'
}), 'bar[]&foo');
});

test('array stringify representation with brackets and separators with single value', t => {
t.is(queryString.stringify({
foo: null,
bar: ['one']
}, {
arrayFormat: 'bracket-separator'
}), 'bar[]=one&foo');
});

test('array stringify representation with brackets and separators with multiple values', t => {
t.is(queryString.stringify({
foo: null,
bar: ['one', 'two', 'three']
}, {
arrayFormat: 'bracket-separator'
}), 'bar[]=one,two,three&foo');
});

test('array stringify representation with brackets and separators with a single empty string', t => {
t.is(queryString.stringify({
foo: null,
bar: ['']
}, {
arrayFormat: 'bracket-separator'
}), 'bar[]=&foo');
});

test('array stringify representation with brackets and separators with a multiple empty string', t => {
t.is(queryString.stringify({
foo: null,
bar: ['', 'two', '']
}, {
arrayFormat: 'bracket-separator'
}), 'bar[]=,two,&foo');
});

test('array stringify representation with brackets and separators with dropped empty strings', t => {
t.is(queryString.stringify({
foo: null,
bar: ['', 'two', '']
}, {
arrayFormat: 'bracket-separator',
skipEmptyString: true
}), 'bar[]=two&foo');
});

test('array stringify representation with brackets and separators with dropped null values', t => {
t.is(queryString.stringify({
foo: null,
bar: ['one', null, 'three', null, '', 'six']
}, {
arrayFormat: 'bracket-separator',
skipNull: true
}), 'bar[]=one,three,,six');
});

test('should sort keys in given order', t => {
const fixture = ['c', 'a', 'b'];
const sort = (key1, key2) => fixture.indexOf(key1) - fixture.indexOf(key2);
Expand Down

0 comments on commit b10bc19

Please sign in to comment.