Skip to content

Commit

Permalink
Feature> emptyValue options for parse & stringify
Browse files Browse the repository at this point in the history
Addresses #223.

Behavior:
* When parsing and encountering empty values, replaces them with
opt.emptyValue (default to '')
* When stringifying and encountering value === opt.emptyValue, outputs a key
without a value
  • Loading branch information
timhwang21 committed Nov 24, 2018
1 parent 0a2d26f commit ea789cc
Show file tree
Hide file tree
Showing 5 changed files with 57 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Expand Up @@ -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,
Expand Down
8 changes: 7 additions & 1 deletion lib/parse.js
Expand Up @@ -13,6 +13,7 @@ var defaults = {
decoder: utils.decode,
delimiter: '&',
depth: 5,
emptyValue: '',
ignoreQueryPrefix: false,
interpretNumericEntities: false,
parameterLimit: 1000,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -187,6 +188,10 @@ module.exports = function (str, opts) {
throw new TypeError('Decoder has to be a function.');
}

if (options.strictNullHandling === true && 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;
Expand All @@ -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');
Expand Down
14 changes: 11 additions & 3 deletions lib/stringify.js
Expand Up @@ -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 + '[]';
Expand All @@ -29,6 +31,7 @@ var defaults = {
charset: 'utf-8',
charsetSentinel: false,
delimiter: '&',
emptyValue: null,
encode: true,
encoder: utils.encode,
encodeValuesOnly: false,
Expand All @@ -46,6 +49,7 @@ var stringify = function stringify( // eslint-disable-line func-name-matching
prefix,
generateArrayPrefix,
strictNullHandling,
emptyValue,
skipNulls,
encoder,
filter,
Expand All @@ -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;
}

Expand Down Expand Up @@ -106,6 +110,7 @@ var stringify = function stringify( // eslint-disable-line func-name-matching
generateArrayPrefix(prefix, key),
generateArrayPrefix,
strictNullHandling,
emptyValue,
skipNulls,
encoder,
filter,
Expand All @@ -122,6 +127,7 @@ var stringify = function stringify( // eslint-disable-line func-name-matching
prefix + (allowDots ? '.' + key : '[' + key + ']'),
generateArrayPrefix,
strictNullHandling,
emptyValue,
skipNulls,
encoder,
filter,
Expand All @@ -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;
Expand All @@ -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];
Expand Down Expand Up @@ -213,6 +220,7 @@ module.exports = function (object, opts) {
key,
generateArrayPrefix,
strictNullHandling,
emptyValue,
skipNulls,
encode ? encoder : null,
filter,
Expand Down
33 changes: 33 additions & 0 deletions test/parse.js
Expand Up @@ -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' });
Expand All @@ -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',
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -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();
});
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions test/stringify.js
Expand Up @@ -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=');
Expand All @@ -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();
});

Expand Down

0 comments on commit ea789cc

Please sign in to comment.