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

[New] parse/stringify: add allowEmptyArrays option to allow [] in object values #487

Merged
merged 1 commit into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions lib/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ var isArray = Array.isArray;

var defaults = {
allowDots: false,
allowEmptyArrays: false,
allowPrototypes: false,
allowSparse: false,
arrayLimit: 20,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.');
}

Expand All @@ -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,
Expand Down
13 changes: 13 additions & 0 deletions lib/stringify.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var defaultFormat = formats['default'];
var defaults = {
addQueryPrefix: false,
allowDots: false,
allowEmptyArrays: false,
arrayFormat: 'indices',
charset: 'utf-8',
charsetSentinel: false,
Expand Down Expand Up @@ -63,6 +64,7 @@ var stringify = function stringify(
prefix,
generateArrayPrefix,
commaRoundTrip,
allowEmptyArrays,
strictNullHandling,
skipNulls,
encoder,
Expand Down Expand Up @@ -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];
Expand All @@ -168,6 +174,7 @@ var stringify = function stringify(
keyPrefix,
generateArrayPrefix,
commaRoundTrip,
allowEmptyArrays,
strictNullHandling,
skipNulls,
generateArrayPrefix === 'comma' && encodeValuesOnly && isArray(obj) ? null : encoder,
Expand All @@ -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.');
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -292,6 +304,7 @@ module.exports = function (object, opts) {
key,
generateArrayPrefix,
commaRoundTrip,
options.allowEmptyArrays,
options.strictNullHandling,
options.skipNulls,
options.encode ? options.encoder : null,
Expand Down
31 changes: 31 additions & 0 deletions test/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
aks- marked this conversation as resolved.
Show resolved Hide resolved
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();
});

Expand Down
39 changes: 39 additions & 0 deletions test/stringify.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand Down
Loading