Skip to content

Add support for arrayFormat: 'bracket-separator' #276

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Mar 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion benchmark.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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