Skip to content

Commit

Permalink
[New] parse/stringify: add allowEmptyArrays option to allow [] …
Browse files Browse the repository at this point in the history
…in object values
  • Loading branch information
aks- authored and ljharb committed Jan 26, 2024
1 parent 04f422f commit f22b2bc
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
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
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
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
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
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' });
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
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

0 comments on commit f22b2bc

Please sign in to comment.