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 Sep 16, 2017
1 parent 0e838da commit 5713e30
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 2 deletions.
9 changes: 8 additions & 1 deletion lib/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ var defaults = {
decoder: utils.decode,
delimiter: '&',
depth: 5,
emptyValue: '',
parameterLimit: 1000,
plainObjects: false,
strictNullHandling: false
Expand All @@ -31,7 +32,7 @@ var parseValues = function parseQueryStringValues(str, options) {
var key, val;
if (pos === -1) {
key = options.decoder(part, defaults.decoder);
val = options.strictNullHandling ? null : '';
val = options.strictNullHandling ? null : options.emptyValue;
} else {
key = options.decoder(part.slice(0, pos), defaults.decoder);
val = options.decoder(part.slice(pos + 1), defaults.decoder);
Expand Down Expand Up @@ -142,6 +143,11 @@ 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 takes precedence over emptyValue.');
// TODO: I don't think this has to throw an error, but console.warn is a lint error
}

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 @@ -153,6 +159,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 (str === '' || str === null || typeof str === 'undefined') {
return options.plainObjects ? Object.create(null) : {};
Expand Down
13 changes: 12 additions & 1 deletion lib/stringify.js
Original file line number Diff line number Diff line change
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 @@ -27,13 +29,15 @@ var defaults = {
},
skipNulls: false,
strictNullHandling: false
// emptyValue: some flag
};

var stringify = function stringify( // eslint-disable-line func-name-matching
object,
prefix,
generateArrayPrefix,
strictNullHandling,
emptyValue,
skipNulls,
encoder,
filter,
Expand All @@ -54,6 +58,9 @@ var stringify = function stringify( // eslint-disable-line func-name-matching
}

obj = '';
} else if (obj === emptyValue) {
// TODO: This needs a condition to avoid accidentally matching undefined
return encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder) : prefix;
}

if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean' || utils.isBuffer(obj)) {
Expand Down Expand Up @@ -91,6 +98,7 @@ var stringify = function stringify( // eslint-disable-line func-name-matching
generateArrayPrefix(prefix, key),
generateArrayPrefix,
strictNullHandling,
emptyValue,
skipNulls,
encoder,
filter,
Expand All @@ -107,6 +115,7 @@ var stringify = function stringify( // eslint-disable-line func-name-matching
generateArrayPrefix,
strictNullHandling,
skipNulls,
emptyValue,
encoder,
filter,
sort,
Expand All @@ -131,6 +140,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 : 'SOME_FLAG'; // TODO
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 @@ -140,7 +150,7 @@ module.exports = function (object, opts) {
var encodeValuesOnly = typeof options.encodeValuesOnly === 'boolean' ? options.encodeValuesOnly : defaults.encodeValuesOnly;
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 @@ -192,6 +202,7 @@ module.exports = function (object, opts) {
key,
generateArrayPrefix,
strictNullHandling,
emptyValue,
skipNulls,
encode ? encoder : null,
filter,
Expand Down
24 changes: 24 additions & 0 deletions test/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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', { emptyValue: 'bar' }), { foo: 'bar' });
st.deepEqual(qs.parse('foo'), { foo: '' });
st.deepEqual(qs.parse('foo='), { foo: '' });
st.deepEqual(qs.parse('foo=bar'), { foo: 'bar' });
Expand All @@ -21,6 +22,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 @@ -160,6 +162,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 @@ -191,22 +194,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 @@ -239,6 +262,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

0 comments on commit 5713e30

Please sign in to comment.