diff --git a/.eslintrc b/.eslintrc index 919dbc06..8a9467f1 100644 --- a/.eslintrc +++ b/.eslintrc @@ -14,7 +14,7 @@ "id-length": [2, { "min": 1, "max": 25, "properties": "never" }], "indent": [2, 4], "max-lines-per-function": [2, { "max": 150 }], - "max-params": [2, 16], + "max-params": [2, 17], "max-statements": [2, 100], "multiline-comment-style": 0, "no-continue": 1, diff --git a/README.md b/README.md index de23df0e..0b50af11 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,12 @@ var withDots = qs.parse('a.b=c', { allowDots: true }); assert.deepEqual(withDots, { a: { b: 'c' } }); ``` +Option `allowEmptyArrays` can be used to allowing empty array values in object +```javascript +var withEmptyArrays = qs.parse('foo[]&bar=baz', { allowEmptyArrays: true }); +assert.deepEqual(withEmptyArrays, { foo: [], bar: 'baz' }); +``` + If you have to deal with legacy browsers or services, there's also support for decoding percent-encoded octets as iso-8859-1: @@ -420,6 +426,12 @@ qs.stringify({ a: { b: { c: 'd', e: 'f' } } }, { allowDots: true }); // 'a.b.c=d&a.b.e=f' ``` +You may allow empty array values by setting the `allowEmptyArrays` option to `true`: +```javascript +qs.stringify({ foo: [], bar: 'baz' }, { allowEmptyArrays: true }); +// 'foo[]&bar=baz' +``` + Empty strings and null values will omit the value, but the equals sign (=) remains in place: ```javascript diff --git a/lib/parse.js b/lib/parse.js index ae194fb5..d8331701 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -7,6 +7,7 @@ var isArray = Array.isArray; var defaults = { allowDots: false, + allowEmptyArrays: false, allowPrototypes: false, allowSparse: false, arrayLimit: 20, @@ -121,7 +122,7 @@ var parseObject = function (chain, val, options, valuesParsed) { var root = chain[i]; if (root === '[]' && options.parseArrays) { - obj = [].concat(leaf); + obj = options.allowEmptyArrays && leaf === '' ? [] : [].concat(leaf); } else { obj = options.plainObjects ? Object.create(null) : {}; var cleanRoot = root.charAt(0) === '[' && root.charAt(root.length - 1) === ']' ? root.slice(1, -1) : root; @@ -207,7 +208,11 @@ var normalizeParseOptions = function normalizeParseOptions(opts) { return defaults; } - if (opts.decoder !== null && opts.decoder !== undefined && typeof opts.decoder !== 'function') { + if (typeof opts.allowEmptyArrays !== 'undefined' && typeof opts.allowEmptyArrays !== 'boolean') { + throw new TypeError('`allowEmptyArrays` option can only be `true` or `false`, when provided'); + } + + if (opts.decoder !== null && typeof opts.decoder !== 'undefined' && typeof opts.decoder !== 'function') { throw new TypeError('Decoder has to be a function.'); } @@ -218,6 +223,7 @@ var normalizeParseOptions = function normalizeParseOptions(opts) { return { allowDots: typeof opts.allowDots === 'undefined' ? defaults.allowDots : !!opts.allowDots, + allowEmptyArrays: typeof opts.allowEmptyArrays === 'boolean' ? !!opts.allowEmptyArrays : defaults.allowEmptyArrays, allowPrototypes: typeof opts.allowPrototypes === 'boolean' ? opts.allowPrototypes : defaults.allowPrototypes, allowSparse: typeof opts.allowSparse === 'boolean' ? opts.allowSparse : defaults.allowSparse, arrayLimit: typeof opts.arrayLimit === 'number' ? opts.arrayLimit : defaults.arrayLimit, diff --git a/lib/stringify.js b/lib/stringify.js index 5e17343e..393ef69a 100644 --- a/lib/stringify.js +++ b/lib/stringify.js @@ -30,6 +30,7 @@ var defaultFormat = formats['default']; var defaults = { addQueryPrefix: false, allowDots: false, + allowEmptyArrays: false, arrayFormat: 'indices', charset: 'utf-8', charsetSentinel: false, @@ -63,6 +64,7 @@ var stringify = function stringify( prefix, generateArrayPrefix, commaRoundTrip, + allowEmptyArrays, strictNullHandling, skipNulls, encoder, @@ -148,6 +150,10 @@ var stringify = function stringify( var adjustedPrefix = commaRoundTrip && isArray(obj) && obj.length === 1 ? prefix + '[]' : prefix; + if (allowEmptyArrays && isArray(obj) && obj.length === 0) { + return adjustedPrefix + '[]'; + } + for (var j = 0; j < objKeys.length; ++j) { var key = objKeys[j]; var value = typeof key === 'object' && typeof key.value !== 'undefined' ? key.value : obj[key]; @@ -168,6 +174,7 @@ var stringify = function stringify( keyPrefix, generateArrayPrefix, commaRoundTrip, + allowEmptyArrays, strictNullHandling, skipNulls, generateArrayPrefix === 'comma' && encodeValuesOnly && isArray(obj) ? null : encoder, @@ -191,6 +198,10 @@ var normalizeStringifyOptions = function normalizeStringifyOptions(opts) { return defaults; } + if (typeof opts.allowEmptyArrays !== 'undefined' && typeof opts.allowEmptyArrays !== 'boolean') { + throw new TypeError('`allowEmptyArrays` option can only be `true` or `false`, when provided'); + } + if (opts.encoder !== null && typeof opts.encoder !== 'undefined' && typeof opts.encoder !== 'function') { throw new TypeError('Encoder has to be a function.'); } @@ -230,6 +241,7 @@ var normalizeStringifyOptions = function normalizeStringifyOptions(opts) { return { addQueryPrefix: typeof opts.addQueryPrefix === 'boolean' ? opts.addQueryPrefix : defaults.addQueryPrefix, allowDots: typeof opts.allowDots === 'undefined' ? defaults.allowDots : !!opts.allowDots, + allowEmptyArrays: typeof opts.allowEmptyArrays === 'boolean' ? !!opts.allowEmptyArrays : defaults.allowEmptyArrays, arrayFormat: arrayFormat, charset: charset, charsetSentinel: typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel, @@ -292,6 +304,7 @@ module.exports = function (object, opts) { key, generateArrayPrefix, commaRoundTrip, + options.allowEmptyArrays, options.strictNullHandling, options.skipNulls, options.encode ? options.encoder : null, diff --git a/test/parse.js b/test/parse.js index 93ca98c0..c5113391 100644 --- a/test/parse.js +++ b/test/parse.js @@ -72,6 +72,37 @@ test('parse()', function (t) { t.test('allows enabling dot notation', function (st) { st.deepEqual(qs.parse('a.b=c'), { 'a.b': 'c' }); st.deepEqual(qs.parse('a.b=c', { allowDots: true }), { a: { b: 'c' } }); + + st.end(); + }); + + t.test('allows empty arrays in obj values', function (st) { + st.deepEqual(qs.parse('foo[]&bar=baz', { allowEmptyArrays: true }), { foo: [], bar: 'baz' }); + st.deepEqual(qs.parse('foo[]&bar=baz', { allowEmptyArrays: false }), { foo: [''], bar: 'baz' }); + + st.end(); + }); + + t.test('should throw when allowEmptyArrays is not of type boolean', function (st) { + st['throws']( + function () { qs.parse('foo[]&bar=baz', { allowEmptyArrays: 'foobar' }); }, + TypeError + ); + + st['throws']( + function () { qs.parse('foo[]&bar=baz', { allowEmptyArrays: 0 }); }, + TypeError + ); + st['throws']( + function () { qs.parse('foo[]&bar=baz', { allowEmptyArrays: NaN }); }, + TypeError + ); + + st['throws']( + function () { qs.parse('foo[]&bar=baz', { allowEmptyArrays: null }); }, + TypeError + ); + st.end(); }); diff --git a/test/stringify.js b/test/stringify.js index 4be97a5c..3303974e 100644 --- a/test/stringify.js +++ b/test/stringify.js @@ -140,6 +140,45 @@ test('stringify()', function (t) { t.test('omits array indices when asked', function (st) { st.equal(qs.stringify({ a: ['b', 'c', 'd'] }, { indices: false }), 'a=b&a=c&a=d'); + + st.end(); + }); + + t.test('omits object key/value pair when value is empty array', function (st) { + st.equal(qs.stringify({ a: [], b: 'zz' }), 'b=zz'); + + st.end(); + }); + + t.test('should not omit object key/value pair when value is empty array and when asked', function (st) { + st.equal(qs.stringify({ a: [], b: 'zz' }), 'b=zz'); + st.equal(qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: false }), 'b=zz'); + st.equal(qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: true }), 'a[]&b=zz'); + + st.end(); + }); + + t.test('should throw when allowEmptyArrays is not of type boolean', function (st) { + st['throws']( + function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 'foobar' }); }, + TypeError + ); + + st['throws']( + function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 0 }); }, + TypeError + ); + + st['throws']( + function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: NaN }); }, + TypeError + ); + + st['throws']( + function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: null }); }, + TypeError + ); + st.end(); });