Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
yarn.lock
.idea/
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();
30 changes: 27 additions & 3 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ 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[]=1', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: ['1']}

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

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

```
Expand All @@ -54,7 +66,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 @@ -231,7 +243,7 @@ export interface StringifyOptions {
//=> 'foo=1,2,3'
```

- `separator`: Serialize arrays by separating elements with character:
- `separator`: Serialize arrays by separating elements with character:

```
import queryString = require('query-string');
Expand All @@ -240,6 +252,18 @@ export interface StringifyOptions {
//=> 'foo=1|2|3'
```

- `separator`: Serialize arrays by putting an explicitly trailing bracket and separating elements with character:

```
import queryString = require('query-string');

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

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

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

```
Expand All @@ -249,7 +273,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
47 changes: 45 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,29 @@ function encoderForArrayFormat(options) {
return result;
}

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

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

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

case 'bracket-separator':
return key => (result, value) => {
if (value === null || value === undefined) { // Explicitly allow empty strings
return result;
}

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

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

return [[result, encode(value, options)].join(options.arrayFormatSeparator)];
Expand Down Expand Up @@ -127,6 +148,28 @@ function parserForArrayFormat(options) {
accumulator[key] = newValue;
};

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

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

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

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

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

default:
return (key, value, accumulator) => {
if (accumulator[key] === undefined) {
Expand Down Expand Up @@ -246,7 +289,7 @@ function parse(input, 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
31 changes: 31 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,19 @@ queryString.parse('foo=1|2|3', {arrayFormat: 'separator', arrayFormatSeparator:
//=> {foo: ['1', '2', '3']}
```

- `'bracket-separator'`: Parse explicitly bracket-postfixed arrays with elements separated by a custom character:

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

//Can handle arrays on a single value to product explicit array
queryString.parse('foo[]=1', {arrayFormat: 'bracket-separator', arrayFormatSeparator: '|'});
//=> {foo: ['1']}

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

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

```js
Expand Down Expand Up @@ -230,6 +243,24 @@ queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'comma'});
//=> 'foo=1,2,3'
```

- `'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 postfixing arrays with brackets and separating elements with a custom character:

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

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

- `'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 @@ -174,6 +174,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
36 changes: 36 additions & 0 deletions test/stringify.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,42 @@ 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 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('should sort keys in given order', t => {
const fixture = ['c', 'a', 'b'];
const sort = (key1, key2) => fixture.indexOf(key1) - fixture.indexOf(key2);
Expand Down