Skip to content

Commit

Permalink
[New] parse/stringify: add allowEmptyArrays option
Browse files Browse the repository at this point in the history
  • Loading branch information
aks- authored and ljharb committed Jan 26, 2024
1 parent 04f422f commit c40572b
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 4 deletions.
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(withEmptyString, { 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
8 changes: 7 additions & 1 deletion 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 = typeof leaf === 'string' && 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,6 +208,10 @@ var normalizeParseOptions = function normalizeParseOptions(opts) {
return defaults;
}

if (opts.allowEmptyArrays && typeof opts.allowEmptyArrays !== 'boolean') {
throw new TypeError('allowEmptyArrays option property can only be true or false');
}

if (opts.decoder !== null && 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
16 changes: 14 additions & 2 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 @@ -74,7 +75,8 @@ var stringify = function stringify(
formatter,
encodeValuesOnly,
charset,
sideChannel
sideChannel,
allowEmptyArrays
) {
var obj = object;

Expand Down Expand Up @@ -148,6 +150,10 @@ var stringify = function stringify(

var adjustedPrefix = commaRoundTrip && isArray(obj) && obj.length === 1 ? prefix + '[]' : prefix;

if (isArray(obj) && obj.length === 0 && allowEmptyArrays) {
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 Down Expand Up @@ -191,6 +197,10 @@ var normalizeStringifyOptions = function normalizeStringifyOptions(opts) {
return defaults;
}

if (opts.allowEmptyArrays && typeof opts.allowEmptyArrays !== 'boolean') {
throw new TypeError('allowEmptyArrays option property can only be true or false');
}

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 +240,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 @@ -303,7 +314,8 @@ module.exports = function (object, opts) {
options.formatter,
options.encodeValuesOnly,
options.charset,
sideChannel
sideChannel,
options.allowEmptyArrays
));
}

Expand Down
6 changes: 6 additions & 0 deletions test/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ test('parse()', function (t) {
st.end();
});

t.test('allows empty arrays in obj values', function (st) {
st.deepEqual(qs.parse('foo[]&bar=baz', { allowEmptyArrays: true }), { foo: [], bar: 'baz' });
st.deepEqual(qs.parse('foo[]&bar=baz', { allowEmptyArrays: false }), { bar: 'baz' });
st.end();
});

t.deepEqual(qs.parse('a[b]=c'), { a: { b: 'c' } }, 'parses a single nested string');
t.deepEqual(qs.parse('a[b][c]=d'), { a: { b: { c: 'd' } } }, 'parses a double nested string');
t.deepEqual(
Expand Down
9 changes: 9 additions & 0 deletions test/stringify.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ test('stringify()', function (t) {
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');
});

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' }), 'a[]b=zz', { allowEmptyArrays: true });
st.equal(qs.stringify({ a: [], b: 'zz' }), 'b=zz', { allowEmptyArrays: false });
});

t.test('stringifies an array value with one item vs multiple items', function (st) {
st.test('non-array item', function (s2t) {
s2t.equal(qs.stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a=c');
Expand Down

0 comments on commit c40572b

Please sign in to comment.