diff --git a/.eslintrc b/.eslintrc index e3bde898..4ca6c417 100644 --- a/.eslintrc +++ b/.eslintrc @@ -11,7 +11,7 @@ "indent": [2, 4], "max-lines-per-function": [2, { "max": 150 }], "max-params": [2, 14], - "max-statements": [2, 52], + "max-statements": [2, 53], "multiline-comment-style": 0, "no-continue": 1, "no-magic-numbers": 0, diff --git a/lib/parse.js b/lib/parse.js index fd675089..8e30dfb6 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -13,6 +13,7 @@ var defaults = { decoder: utils.decode, delimiter: '&', depth: 5, + emptyValue: '', ignoreQueryPrefix: false, interpretNumericEntities: false, parameterLimit: 1000, @@ -72,7 +73,7 @@ var parseValues = function parseQueryStringValues(str, options) { var key, val; if (pos === -1) { key = options.decoder(part, defaults.decoder, charset); - val = options.strictNullHandling ? null : ''; + val = options.strictNullHandling ? null : options.emptyValue; } else { key = options.decoder(part.slice(0, pos), defaults.decoder, charset); val = options.decoder(part.slice(pos + 1), defaults.decoder, charset); @@ -187,6 +188,10 @@ module.exports = function (str, opts) { throw new TypeError('Decoder has to be a function.'); } + if (options.strictNullHandling && has.call(options, 'emptyValue')) { + throw new TypeError('strictNullHandling and emptyValue are not compatible.'); + } + options.ignoreQueryPrefix = options.ignoreQueryPrefix === true; options.delimiter = typeof options.delimiter === 'string' || utils.isRegExp(options.delimiter) ? options.delimiter : defaults.delimiter; options.depth = typeof options.depth === 'number' ? options.depth : defaults.depth; @@ -198,6 +203,7 @@ module.exports = function (str, opts) { options.allowPrototypes = typeof options.allowPrototypes === 'boolean' ? options.allowPrototypes : defaults.allowPrototypes; options.parameterLimit = typeof options.parameterLimit === 'number' ? options.parameterLimit : defaults.parameterLimit; options.strictNullHandling = typeof options.strictNullHandling === 'boolean' ? options.strictNullHandling : defaults.strictNullHandling; + options.emptyValue = has.call(options, 'emptyValue') ? options.emptyValue : defaults.emptyValue; if (typeof options.charset !== 'undefined' && options.charset !== 'utf-8' && options.charset !== 'iso-8859-1') { throw new Error('The charset option must be either utf-8, iso-8859-1, or undefined'); diff --git a/lib/stringify.js b/lib/stringify.js index b5e69ead..a5cf8dba 100644 --- a/lib/stringify.js +++ b/lib/stringify.js @@ -3,6 +3,8 @@ var utils = require('./utils'); var formats = require('./formats'); +var has = Object.prototype.hasOwnProperty; + var arrayPrefixGenerators = { brackets: function brackets(prefix) { // eslint-disable-line func-name-matching return prefix + '[]'; @@ -29,6 +31,7 @@ var defaults = { charset: 'utf-8', charsetSentinel: false, delimiter: '&', + emptyValue: null, encode: true, encoder: utils.encode, encodeValuesOnly: false, @@ -46,6 +49,7 @@ var stringify = function stringify( // eslint-disable-line func-name-matching prefix, generateArrayPrefix, strictNullHandling, + emptyValue, skipNulls, encoder, filter, @@ -63,8 +67,8 @@ var stringify = function stringify( // eslint-disable-line func-name-matching obj = serializeDate(obj); } - if (obj === null) { - if (strictNullHandling) { + if (obj === null || obj === emptyValue) { + if (strictNullHandling || (obj === emptyValue && emptyValue !== null)) { return encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder, charset) : prefix; } @@ -106,6 +110,7 @@ var stringify = function stringify( // eslint-disable-line func-name-matching generateArrayPrefix(prefix, key), generateArrayPrefix, strictNullHandling, + emptyValue, skipNulls, encoder, filter, @@ -122,6 +127,7 @@ var stringify = function stringify( // eslint-disable-line func-name-matching prefix + (allowDots ? '.' + key : '[' + key + ']'), generateArrayPrefix, strictNullHandling, + emptyValue, skipNulls, encoder, filter, @@ -148,6 +154,7 @@ module.exports = function (object, opts) { var delimiter = typeof options.delimiter === 'undefined' ? defaults.delimiter : options.delimiter; var strictNullHandling = typeof options.strictNullHandling === 'boolean' ? options.strictNullHandling : defaults.strictNullHandling; + var emptyValue = has.call(options, 'emptyValue') ? options.emptyValue : defaults.emptyValue; var skipNulls = typeof options.skipNulls === 'boolean' ? options.skipNulls : defaults.skipNulls; var encode = typeof options.encode === 'boolean' ? options.encode : defaults.encode; var encoder = typeof options.encoder === 'function' ? options.encoder : defaults.encoder; @@ -162,7 +169,7 @@ module.exports = function (object, opts) { if (typeof options.format === 'undefined') { options.format = formats['default']; - } else if (!Object.prototype.hasOwnProperty.call(formats.formatters, options.format)) { + } else if (!has.call(formats.formatters, options.format)) { throw new TypeError('Unknown format option provided.'); } var formatter = formats.formatters[options.format]; @@ -213,6 +220,7 @@ module.exports = function (object, opts) { key, generateArrayPrefix, strictNullHandling, + emptyValue, skipNulls, encode ? encoder : null, filter, diff --git a/test/parse.js b/test/parse.js index c888bf59..a392df99 100644 --- a/test/parse.js +++ b/test/parse.js @@ -14,6 +14,9 @@ test('parse()', function (t) { st.deepEqual(qs.parse('a[<=>]==23'), { a: { '<=>': '=23' } }); st.deepEqual(qs.parse('a[==]=23'), { a: { '==': '23' } }); st.deepEqual(qs.parse('foo', { strictNullHandling: true }), { foo: null }); + st.deepEqual(qs.parse('foo=', { strictNullHandling: true }), { foo: '' }); + st.deepEqual(qs.parse('foo', { emptyValue: true }), { foo: true }); + st.deepEqual(qs.parse('foo=', { emptyValue: true }), { foo: '' }); st.deepEqual(qs.parse('foo'), { foo: '' }); st.deepEqual(qs.parse('foo='), { foo: '' }); st.deepEqual(qs.parse('foo=bar'), { foo: 'bar' }); @@ -22,6 +25,7 @@ test('parse()', function (t) { st.deepEqual(qs.parse('foo=bar&bar=baz'), { foo: 'bar', bar: 'baz' }); st.deepEqual(qs.parse('foo2=bar2&baz2='), { foo2: 'bar2', baz2: '' }); st.deepEqual(qs.parse('foo=bar&baz', { strictNullHandling: true }), { foo: 'bar', baz: null }); + st.deepEqual(qs.parse('foo=bar&baz', { emptyValue: 'quux' }), { foo: 'bar', baz: 'quux' }); st.deepEqual(qs.parse('foo=bar&baz'), { foo: 'bar', baz: '' }); st.deepEqual(qs.parse('cht=p3&chd=t:60,40&chs=250x100&chl=Hello|World'), { cht: 'p3', @@ -161,6 +165,7 @@ test('parse()', function (t) { t.test('supports malformed uri characters', function (st) { st.deepEqual(qs.parse('{%:%}', { strictNullHandling: true }), { '{%:%}': null }); + st.deepEqual(qs.parse('{%:%}', { emptyValue: 'foo' }), { '{%:%}': 'foo' }); st.deepEqual(qs.parse('{%:%}='), { '{%:%}': '' }); st.deepEqual(qs.parse('foo=%:%}'), { foo: '%:%}' }); st.end(); @@ -192,22 +197,42 @@ test('parse()', function (t) { { a: ['b', null, 'c', ''] }, 'with arrayLimit 20 + array indices: null then empty string works' ); + st.deepEqual( + qs.parse('a[0]=b&a[1]&a[2]=c&a[19]=', { emptyValue: 'foo', arrayLimit: 20 }), + { a: ['b', 'foo', 'c', ''] }, + 'with arrayLimit 20 + array indices: null then empty string works' + ); st.deepEqual( qs.parse('a[]=b&a[]&a[]=c&a[]=', { strictNullHandling: true, arrayLimit: 0 }), { a: ['b', null, 'c', ''] }, 'with arrayLimit 0 + array brackets: null then empty string works' ); + st.deepEqual( + qs.parse('a[]=b&a[]&a[]=c&a[]=', { emptyValue: 'foo', arrayLimit: 0 }), + { a: ['b', 'foo', 'c', ''] }, + 'with arrayLimit 0 + array brackets: null then empty string works' + ); st.deepEqual( qs.parse('a[0]=b&a[1]=&a[2]=c&a[19]', { strictNullHandling: true, arrayLimit: 20 }), { a: ['b', '', 'c', null] }, 'with arrayLimit 20 + array indices: empty string then null works' ); + st.deepEqual( + qs.parse('a[0]=b&a[1]=&a[2]=c&a[19]', { emptyValue: 'foo', arrayLimit: 20 }), + { a: ['b', '', 'c', 'foo'] }, + 'with arrayLimit 20 + array indices: empty string then null works' + ); st.deepEqual( qs.parse('a[]=b&a[]=&a[]=c&a[]', { strictNullHandling: true, arrayLimit: 0 }), { a: ['b', '', 'c', null] }, 'with arrayLimit 0 + array brackets: empty string then null works' ); + st.deepEqual( + qs.parse('a[]=b&a[]=&a[]=c&a[]', { emptyValue: 'foo', arrayLimit: 0 }), + { a: ['b', '', 'c', 'foo'] }, + 'with arrayLimit 0 + array brackets: empty string then null works' + ); st.deepEqual( qs.parse('a[]=&a[]=b&a[]=c'), @@ -240,6 +265,7 @@ test('parse()', function (t) { t.test('continues parsing when no parent is found', function (st) { st.deepEqual(qs.parse('[]=&a=b'), { 0: '', a: 'b' }); st.deepEqual(qs.parse('[]&a=b', { strictNullHandling: true }), { 0: null, a: 'b' }); + st.deepEqual(qs.parse('[]&a=b', { emptyValue: 'foo' }), { 0: 'foo', a: 'b' }); st.deepEqual(qs.parse('[foo]=bar'), { foo: 'bar' }); st.end(); }); @@ -570,6 +596,13 @@ test('parse()', function (t) { st.end(); }); + t.test('throws error when both strictNullHandling and emptyValue are provided', function (st) { + st['throws'](function () { + qs.parse({}, { strictNullHandling: true, emptyValue: 'foo ' }); + }, new TypeError('strictNullHandling and emptyValue are not compatible.')); + st.end(); + }); + t.test('does not mutate the options argument', function (st) { var options = {}; qs.parse('a[b]=true', options); diff --git a/test/stringify.js b/test/stringify.js index 7901ea00..98aa54a0 100644 --- a/test/stringify.js +++ b/test/stringify.js @@ -259,6 +259,7 @@ test('stringify()', function (t) { t.test('stringifies an empty value', function (st) { st.equal(qs.stringify({ a: '' }), 'a='); + st.equal(qs.stringify({ a: null }), 'a='); st.equal(qs.stringify({ a: null }, { strictNullHandling: true }), 'a'); st.equal(qs.stringify({ a: '', b: '' }), 'a=&b='); @@ -268,6 +269,10 @@ test('stringify()', function (t) { st.equal(qs.stringify({ a: { b: null } }, { strictNullHandling: true }), 'a%5Bb%5D'); st.equal(qs.stringify({ a: { b: null } }, { strictNullHandling: false }), 'a%5Bb%5D='); + st.equal(qs.stringify({ a: true }, { emptyValue: true }), 'a'); + + st.equal(qs.stringify({ a: null, b: true }, { emptyValue: true }), 'a=&b'); + st.equal(qs.stringify({ a: null, b: true }, { strictNullHandling: true, emptyValue: true }), 'a&b'); st.end(); });