diff --git a/benchmark.js b/benchmark.js index af120ea..b152460 100644 --- a/benchmark.js +++ b/benchmark.js @@ -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 @@ -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); @@ -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); @@ -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(); diff --git a/index.d.ts b/index.d.ts index b6d651b..847336d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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: ``` @@ -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'}`. @@ -229,9 +253,14 @@ export interface StringifyOptions { queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'comma'}); //=> 'foo=1,2,3' + + queryString.stringify({foo: [1, null, '']}, {arrayFormat: 'comma'}); + //=> 'foo=1,,' + // Note that typing information for null values is lost + // 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'); @@ -240,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: ``` @@ -249,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'}`. @@ -372,7 +428,7 @@ export interface UrlObject { /** Overrides queries in the `url` property. */ - readonly query: StringifiableRecord; + readonly query?: StringifiableRecord; /** Overrides the fragment identifier in the `url` property. diff --git a/index.js b/index.js index 423b9d6..7ab5d92 100644 --- a/index.js +++ b/index.js @@ -49,17 +49,30 @@ function encoderForArrayFormat(options) { case 'comma': case 'separator': + case 'bracket-separator': { + const keyValueSep = options.arrayFormat === 'bracket-separator' ? + '[]=' : + '='; + return key => (result, value) => { - if (value === null || value === undefined || value.length === 0) { + if ( + value === undefined || + (options.skipNull && value === null) || + (options.skipEmptyString && value === '') + ) { return result; } + // Translate null to an empty string so that it doesn't serialize as 'null' + value = value === null ? '' : value; + 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)]; }; + } default: return key => (result, value) => { @@ -130,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) { @@ -253,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); } @@ -335,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('&'); diff --git a/package.json b/package.json index 3b90b26..2680bc1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "query-string", - "version": "6.14.0", + "version": "7.0.0", "description": "Parse and stringify URL query strings", "license": "MIT", "repository": "sindresorhus/query-string", diff --git a/readme.md b/readme.md index 280972e..600a971 100644 --- a/readme.md +++ b/readme.md @@ -17,7 +17,20 @@

- + + +
+
+ +
+ Doppler +
+ All your environment variables, in one place +
+ Stop struggling with scattered API keys, hacking together home-brewed tools, +
+ and avoiding access controls. Keep your team and servers in sync with Doppler. +

@@ -125,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 @@ -228,6 +265,47 @@ const queryString = require('query-string'); queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'comma'}); //=> 'foo=1,2,3' + +queryString.stringify({foo: [1, null, '']}, {arrayFormat: 'comma'}); +//=> 'foo=1,,' +// Note that typing information for null values is lost +// 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: diff --git a/test/parse.js b/test/parse.js index 9bfc034..731a848 100644 --- a/test/parse.js +++ b/test/parse.js @@ -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=']}); }); @@ -218,7 +248,7 @@ test('query strings having ordered index arrays and format option as `index`', t }), {bat: 'buz', foo: ['zero', 'two', 'one', 'three']}); }); -test('circuit parse -> stringify', t => { +test('circuit parse → stringify', t => { const original = 'foo[3]=foo&foo[2]&foo[1]=one&foo[0]=&bat=buz'; const sortedOriginal = 'bat=buz&foo[0]=&foo[1]=one&foo[2]&foo[3]=foo'; const expected = {bat: 'buz', foo: ['', 'one', null, 'foo']}; @@ -231,7 +261,7 @@ test('circuit parse -> stringify', t => { t.is(queryString.stringify(expected, options), sortedOriginal); }); -test('circuit original -> parse - > stringify -> sorted original', t => { +test('circuit original → parse → stringify → sorted original', t => { const original = 'foo[21474836471]=foo&foo[21474836470]&foo[1]=one&foo[0]=&bat=buz'; const sortedOriginal = 'bat=buz&foo[0]=&foo[1]=one&foo[2]&foo[3]=foo'; const options = { @@ -241,6 +271,33 @@ test('circuit original -> parse - > stringify -> sorted original', t => { t.deepEqual(queryString.stringify(queryString.parse(original, options), options), sortedOriginal); }); +test('circuit parse → stringify with array commas', t => { + const original = 'c=,a,,&b=&a='; + const sortedOriginal = 'a=&b=&c=,a,,'; + const expected = { + c: ['', 'a', '', ''], + b: '', + a: '' + }; + const options = { + arrayFormat: 'comma' + }; + + t.deepEqual(queryString.parse(original, options), expected); + + t.is(queryString.stringify(expected, options), sortedOriginal); +}); + +test('circuit original → parse → stringify with array commas → sorted original', t => { + const original = 'c=,a,,&b=&a='; + const sortedOriginal = 'a=&b=&c=,a,,'; + const options = { + arrayFormat: 'comma' + }; + + t.deepEqual(queryString.stringify(queryString.parse(original, options), options), sortedOriginal); +}); + test('decode keys and values', t => { t.deepEqual(queryString.parse('st%C3%A5le=foo'), {ståle: 'foo'}); t.deepEqual(queryString.parse('foo=%7B%ab%%7C%de%%7D+%%7Bst%C3%A5le%7D%'), {foo: '{%ab%|%de%} %{ståle}%'}); diff --git a/test/stringify.js b/test/stringify.js index 5c3487b..c8751eb 100644 --- a/test/stringify.js +++ b/test/stringify.js @@ -126,13 +126,26 @@ test('array stringify representation with array commas', t => { }), 'bar=one,two&foo'); }); -test('array stringify representation with array commas and null value', t => { +test('array stringify representation with array commas, null & empty string', t => { t.is(queryString.stringify({ - foo: [null, 'a', null, ''], - bar: [null] + c: [null, 'a', '', null], + b: [null], + a: [''] + }, { + arrayFormat: 'comma' + }), 'a=&b=&c=,a,,'); +}); + +test('array stringify representation with array commas, null & empty string (skip both)', t => { + t.is(queryString.stringify({ + c: [null, 'a', '', null], + b: [null], + a: [''] }, { + skipNull: true, + skipEmptyString: true, arrayFormat: 'comma' - }), 'foo=a'); + }), 'c=a'); }); test('array stringify representation with array commas and 0 value', t => { @@ -141,7 +154,7 @@ test('array stringify representation with array commas and 0 value', t => { bar: [null] }, { arrayFormat: 'comma' - }), 'foo=a,0'); + }), 'bar=&foo=a,,0'); }); test('array stringify representation with a bad array format', t => { @@ -159,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);