Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature> emptyValue options for parse & stringify #226

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 7 additions & 4 deletions lib/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ var defaults = {
decoder: utils.decode,
delimiter: '&',
depth: 5,
emptyValue: '',
ignoreQueryPrefix: false,
interpretNumericEntities: false,
parameterLimit: 1000,
parseArrays: true,
plainObjects: false,
strictNullHandling: false
plainObjects: false
};

var interpretNumericEntities = function (str) {
Expand Down Expand Up @@ -82,7 +82,7 @@ var parseValues = function parseQueryStringValues(str, options) {
var key, val;
if (pos === -1) {
key = options.decoder(part, defaults.decoder, charset, 'key');
val = options.strictNullHandling ? null : '';
val = options.emptyValue;
} else {
key = options.decoder(part.slice(0, pos), defaults.decoder, charset, 'key');
val = utils.maybeMap(
Expand Down Expand Up @@ -212,6 +212,9 @@ var normalizeParseOptions = function normalizeParseOptions(opts) {
if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') {
throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined');
}

var defaultEmptyValue = opts.strictNullHandling === true ? null : defaults.emptyValue

var charset = typeof opts.charset === 'undefined' ? defaults.charset : opts.charset;

return {
Expand All @@ -225,12 +228,12 @@ var normalizeParseOptions = function normalizeParseOptions(opts) {
delimiter: typeof opts.delimiter === 'string' || utils.isRegExp(opts.delimiter) ? opts.delimiter : defaults.delimiter,
// eslint-disable-next-line no-implicit-coercion, no-extra-parens
depth: (typeof opts.depth === 'number' || opts.depth === false) ? +opts.depth : defaults.depth,
emptyValue: has.call(opts, 'emptyValue') ? opts.emptyValue : defaultEmptyValue,
ignoreQueryPrefix: opts.ignoreQueryPrefix === true,
interpretNumericEntities: typeof opts.interpretNumericEntities === 'boolean' ? opts.interpretNumericEntities : defaults.interpretNumericEntities,
parameterLimit: typeof opts.parameterLimit === 'number' ? opts.parameterLimit : defaults.parameterLimit,
parseArrays: opts.parseArrays !== false,
plainObjects: typeof opts.plainObjects === 'boolean' ? opts.plainObjects : defaults.plainObjects,
strictNullHandling: typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling
};
};

Expand Down
13 changes: 9 additions & 4 deletions lib/stringify.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var defaults = {
charset: 'utf-8',
charsetSentinel: false,
delimiter: '&',
emptyValue: null,
encode: true,
encoder: utils.encode,
encodeValuesOnly: false,
Expand Down Expand Up @@ -59,6 +60,7 @@ var stringify = function stringify(
prefix,
generateArrayPrefix,
strictNullHandling,
emptyValue,
skipNulls,
encoder,
filter,
Expand All @@ -83,11 +85,11 @@ var stringify = function stringify(
}).join(',');
}

if (obj === null) {
if (strictNullHandling) {
return encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder, charset, 'key') : prefix;
}
if ((strictNullHandling && obj === null) || (emptyValue !== null && obj === emptyValue)) {
return encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder, charset, 'key') : prefix;
}

if (obj === null) {
obj = '';
}

Expand Down Expand Up @@ -130,6 +132,7 @@ var stringify = function stringify(
keyPrefix,
generateArrayPrefix,
strictNullHandling,
emptyValue,
skipNulls,
encoder,
filter,
Expand Down Expand Up @@ -179,6 +182,7 @@ var normalizeStringifyOptions = function normalizeStringifyOptions(opts) {
charset: charset,
charsetSentinel: typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel,
delimiter: typeof opts.delimiter === 'undefined' ? defaults.delimiter : opts.delimiter,
emptyValue: has.call(options, 'emptyValue') ? options.emptyValue : defaults.emptyValue,
encode: typeof opts.encode === 'boolean' ? opts.encode : defaults.encode,
encoder: typeof opts.encoder === 'function' ? opts.encoder : defaults.encoder,
encodeValuesOnly: typeof opts.encodeValuesOnly === 'boolean' ? opts.encodeValuesOnly : defaults.encodeValuesOnly,
Expand Down Expand Up @@ -242,6 +246,7 @@ module.exports = function (object, opts) {
key,
generateArrayPrefix,
options.strictNullHandling,
options.emptyValue,
options.skipNulls,
options.encode ? options.encoder : null,
options.filter,
Expand Down
33 changes: 33 additions & 0 deletions test/parse.js
Original file line number Diff line number Diff line change
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 @@ -205,6 +209,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 @@ -236,22 +241,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 @@ -292,6 +317,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 @@ -668,6 +694,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
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,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 @@ -333,6 +334,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