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

Custom stringify arrays and objects #365

Draft
wants to merge 33 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e39c235
[Tests] `Buffer.from` in node v5.0-v5.9 and v4.0-v4.4 requires a Type…
ljharb Aug 17, 2019
760a670
[Dev Deps] update `eslint`, `@ljharb/eslint-config`, `evalmd`
ljharb Sep 13, 2019
97154a6
[Tests] up to `node` `v12.10`, `v11.15`, `v10.16`, `v8.16`
ljharb Sep 13, 2019
4019179
[Tests] add `posttest` using `npx aud` to run `npm audit` without a l…
ljharb Sep 13, 2019
7f216ee
[New] `parse`/`stringify`: Pass extra key/value argument to `decoder`
Sep 12, 2019
df0cb44
[Dev Deps] update `eslint`
ljharb Sep 21, 2019
dadf9db
[Tests] `parse`: add passing `arrayFormat` tests
dreyks Sep 8, 2019
670254b
v6.9.0
ljharb Sep 21, 2019
698b683
[fix] `parse`: with comma true, do not split non-string values
Oct 4, 2019
f884e2d
[Fix] `parse`: with comma true, handle field that holds an array of a…
Oct 4, 2019
1f35831
[Dev Deps] update `eslint`, `@ljharb/eslint-config`
ljharb Nov 7, 2019
b9a032f
[meta] add `funding` field
ljharb Nov 7, 2019
6151be3
[Tests] use shared travis-ci config
ljharb Nov 7, 2019
7b36800
v6.9.1
ljharb Nov 8, 2019
152b26c
[meta] add tidelift marketing copy
ljharb Dec 6, 2019
fe6384c
[Fix] `parse`: throw a TypeError instead of an Error for bad charset
ljharb Jan 14, 2020
76e4570
[actions] add automatic rebasing / merge commit blocking
ljharb Jan 14, 2020
72dc89f
[meta] fix indentation in package.json
ljharb Jan 14, 2020
5af2bf8
[meta] ignore eclint transitive audit warning
ljharb Jan 14, 2020
eac5616
[Dev Deps] update `eslint`, `@ljharb/eslint-config`, `object-inspect`…
ljharb Jan 14, 2020
0625c49
[Dev Deps] update `@ljharb/eslint-config`, `tape`
ljharb Jan 26, 2020
eecd28d
[Fix] `parse`: Fix parsing array from object with `comma` true
Mar 15, 2020
911efab
[Dev Deps] update `tape`, `mkdirp`, `iconv-lite`
ljharb Mar 22, 2020
ddc1ff9
v6.9.2
ljharb Mar 22, 2020
37f6a6b
Merge changelogs from v6.7.1, v6.8.1
ljharb Mar 24, 2020
cd9a3cd
[Fix] parses comma delimited array while having percent-encoded comma…
Oct 18, 2019
bf0ea91
[Fix] proper comma parsing of URL-encoded commas
ljharb Mar 25, 2020
8d1dea2
Merge changelogs from v6.7.2, v6.8.2
ljharb Mar 25, 2020
511e1c9
v6.9.3
ljharb Mar 25, 2020
0932740
Add stringify objects with curly braces and add possibility stringify…
leninlin May 2, 2020
e25d9aa
Revert dist files
leninlin May 2, 2020
ac71264
Add mirror to parse.js
leninlin May 3, 2020
422d2c2
Fix example in README
leninlin May 3, 2020
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
40 changes: 35 additions & 5 deletions README.md
Expand Up @@ -139,10 +139,25 @@ var regexed = qs.parse('a=b;c=d,e=f', { delimiter: /[;,]/ });
assert.deepEqual(regexed, { a: 'b', c: 'd', e: 'f' });
```

Option `allowDots` can be used to enable dot notation:
You may set `objectFormat: 'dots'` to enable dot notation:

```javascript
var withDots = qs.parse('a.b=c', { allowDots: true });
var withDots = qs.parse('a.b=c', { objectFormat: 'dots' });
assert.deepEqual(withDots, { a: { b: 'c' } });
```

You may set `objectFormat: 'curly'` to enable curly brackets notation:

```javascript
var withDots = qs.parse('a{b}=c', { objectFormat: 'curly' });
assert.deepEqual(withDots, { a: { b: 'c' } });
Comment on lines +149 to +153
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an extremely confusing and strange notation i've never seen anywhere else; i'm fine adding a function form so someone can do this themselves, but i'd need a lot of convincing that this format deserves first-class support.

```

You may use function in `objectFormat` to parse custom notation
(function should replace your custom format with default brackets format):

```javascript
var withDots = qs.parse('a/b=c', { objectFormat: function (key) { return key.replace(/\/([^.[]+)/g, '[$1]'); } });
assert.deepEqual(withDots, { a: { b: 'c' } });
```

Expand Down Expand Up @@ -275,11 +290,18 @@ assert.deepEqual(arraysOfObjects, { a: [{ b: 'c' }] });

Some people use comma to join array, **qs** can parse it:
```javascript
var arraysOfObjects = qs.parse('a=b,c', { comma: true })
var arraysOfObjects = qs.parse('a=b,c', { arrayFormat: 'comma' })
assert.deepEqual(arraysOfObjects, { a: ['b', 'c'] })
```
(_this cannot convert nested objects, such as `a={b:1},{c:d}`_)

And you may write function for parse arrays with custom keys
(function should replace your custom format with default brackets format):
```javascript
var arraysOfObjects = qs.parse('a-0=b&a-1=c', { arrayFormat: function (key) { return key.replace(/-([^-[]+)/g, '[$1]'); } })
assert.deepEqual(arraysOfObjects, { a: ['b', 'c'] })
```

### Stringifying

[](#preventEval)
Expand Down Expand Up @@ -381,6 +403,8 @@ qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' })
// 'a=b&a=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'comma' })
// 'a=b,c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: function customFormat(prefix, key) { return prefix + '-' + key; } })
// 'a-0=b&a-1=c'
```

When objects are stringified, by default they use bracket notation:
Expand All @@ -390,11 +414,17 @@ qs.stringify({ a: { b: { c: 'd', e: 'f' } } });
// 'a[b][c]=d&a[b][e]=f'
```

You may override this to use dot notation by setting the `allowDots` option to `true`:
You may use the `objectFormat` option to specify the format of the output object:

```javascript
qs.stringify({ a: { b: { c: 'd', e: 'f' } } }, { allowDots: true });
qs.stringify({ a: { b: { c: 'd', e: 'f' } } }, { arrayFormat: 'dots' });
// 'a.b.c=d&a.b.e=f'
qs.stringify({ a: { b: { c: 'd', e: 'f' } } }, { arrayFormat: 'brackets' });
// 'a[b][c]=d&a[b][e]=f'
qs.stringify({ a: { b: { c: 'd', e: 'f' } } }, { arrayFormat: 'curly' });
// 'a{b}{c}=d&a{b}{e}=f'
Comment on lines +424 to +425
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly here

Suggested change
qs.stringify({ a: { b: { c: 'd', e: 'f' } } }, { arrayFormat: 'curly' });
// 'a{b}{c}=d&a{b}{e}=f'

qs.stringify({ a: { b: { c: 'd', e: 'f' } } }, { arrayFormat: function customFormat(prefix, key) { return prefix + '/' + key; } });
// 'a/b/c=d&a/b/e=f'
```

Empty strings and null values will omit the value, but the equals sign (=) remains in place:
Expand Down
49 changes: 46 additions & 3 deletions lib/parse.js
Expand Up @@ -5,18 +5,40 @@ var utils = require('./utils');
var has = Object.prototype.hasOwnProperty;
var isArray = Array.isArray;

var arrayKeyReplacer = {
brackets: function brackets(key) {
return key;
}
};

var objectKeyReplacer = {
brackets: function brackets(key) {
return key;
},
curly: function curly(key) {
return key.replace(/\{([^{}[]+)\}/g, '[$1]');
},
Comment on lines +18 to +20
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
curly: function curly(key) {
return key.replace(/\{([^{}[]+)\}/g, '[$1]');
},

dots: function dots(key) {
return key.replace(/\.([^.[]+)/g, '[$1]');
}
};

var defaults = {
// deprecated
allowDots: false,
allowPrototypes: false,
arrayFormat: 'brackets',
arrayLimit: 20,
charset: 'utf-8',
charsetSentinel: false,
// deprecated
comma: false,
decoder: utils.decode,
delimiter: '&',
depth: 5,
ignoreQueryPrefix: false,
interpretNumericEntities: false,
objectFormat: 'brackets',
parameterLimit: 1000,
parseArrays: true,
plainObjects: false,
Expand All @@ -30,7 +52,7 @@ var interpretNumericEntities = function (str) {
};

var parseArrayValue = function (val, options) {
if (val && typeof val === 'string' && options.comma && val.indexOf(',') > -1) {
if (val && typeof val === 'string' && options.arrayFormat === 'comma' && val.indexOf(',') > -1) {
return val.split(',');
}

Expand Down Expand Up @@ -162,8 +184,18 @@ var parseKeys = function parseQueryStringKeys(givenKey, val, options, valuesPars
return;
}

// Transform dot notation to bracket notation
var key = options.allowDots ? givenKey.replace(/\.([^.[]+)/g, '[$1]') : givenKey;
// Transforms key to bracket notation
var key = givenKey;
if (typeof options.arrayFormat === 'function') {
key = options.arrayFormat(key);
} else if (options.arrayFormat in arrayKeyReplacer) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else if (options.arrayFormat in arrayKeyReplacer) {
} else if (has.call(arrayKeyReplacer, options.arrayFormat)) {

key = arrayKeyReplacer[options.arrayFormat](key);
}
if (typeof options.objectFormat === 'function') {
key = options.objectFormat(key);
} else if (options.objectFormat in objectKeyReplacer) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else if (options.objectFormat in objectKeyReplacer) {
} else if (has.call(objectKeyReplacer, options.objectFormat)) {

key = objectKeyReplacer[options.objectFormat](key);
}

// The regex chunks

Expand Down Expand Up @@ -225,9 +257,19 @@ var normalizeParseOptions = function normalizeParseOptions(opts) {
}
var charset = typeof opts.charset === 'undefined' ? defaults.charset : opts.charset;

var arrayFormat = opts.arrayFormat;
if (typeof arrayFormat === 'undefined' && opts.comma) {
arrayFormat = 'comma';
}
var objectFormat = opts.objectFormat;
if (typeof objectFormat === 'undefined' && opts.allowDots) {
objectFormat = 'dots';
}

return {
allowDots: typeof opts.allowDots === 'undefined' ? defaults.allowDots : !!opts.allowDots,
allowPrototypes: typeof opts.allowPrototypes === 'boolean' ? opts.allowPrototypes : defaults.allowPrototypes,
arrayFormat: typeof arrayFormat === 'undefined' ? defaults.arrayFormat : arrayFormat,
arrayLimit: typeof opts.arrayLimit === 'number' ? opts.arrayLimit : defaults.arrayLimit,
charset: charset,
charsetSentinel: typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel,
Expand All @@ -238,6 +280,7 @@ var normalizeParseOptions = function normalizeParseOptions(opts) {
depth: (typeof opts.depth === 'number' || opts.depth === false) ? +opts.depth : defaults.depth,
ignoreQueryPrefix: opts.ignoreQueryPrefix === true,
interpretNumericEntities: typeof opts.interpretNumericEntities === 'boolean' ? opts.interpretNumericEntities : defaults.interpretNumericEntities,
objectFormat: typeof objectFormat === 'undefined' ? defaults.objectFormat : objectFormat,
parameterLimit: typeof opts.parameterLimit === 'number' ? opts.parameterLimit : defaults.parameterLimit,
parseArrays: opts.parseArrays !== false,
plainObjects: typeof opts.plainObjects === 'boolean' ? opts.plainObjects : defaults.plainObjects,
Expand Down
46 changes: 35 additions & 11 deletions lib/stringify.js
Expand Up @@ -17,6 +17,18 @@ var arrayPrefixGenerators = {
}
};

var objectPrefixGenerators = {
brackets: function brackets(prefix, key) {
return prefix + '[' + key + ']';
},
curly: function curly(prefix, key) {
return prefix + '{' + key + '}';
},
dots: function dots(prefix, key) {
return prefix + '.' + key;
}
};

var isArray = Array.isArray;
var push = Array.prototype.push;
var pushToArray = function (arr, valueOrArray) {
Expand All @@ -28,6 +40,7 @@ var toISO = Date.prototype.toISOString;
var defaultFormat = formats['default'];
var defaults = {
addQueryPrefix: false,
// deprecated
allowDots: false,
charset: 'utf-8',
charsetSentinel: false,
Expand Down Expand Up @@ -58,12 +71,12 @@ var stringify = function stringify(
object,
prefix,
generateArrayPrefix,
generateObjectPrefix,
strictNullHandling,
skipNulls,
encoder,
filter,
sort,
allowDots,
serializeDate,
formatter,
encodeValuesOnly,
Expand Down Expand Up @@ -120,12 +133,12 @@ var stringify = function stringify(
obj[key],
typeof generateArrayPrefix === 'function' ? generateArrayPrefix(prefix, key) : prefix,
generateArrayPrefix,
generateObjectPrefix,
strictNullHandling,
skipNulls,
encoder,
filter,
sort,
allowDots,
serializeDate,
formatter,
encodeValuesOnly,
Expand All @@ -134,14 +147,14 @@ var stringify = function stringify(
} else {
pushToArray(values, stringify(
obj[key],
prefix + (allowDots ? '.' + key : '[' + key + ']'),
generateObjectPrefix(prefix, key),
generateArrayPrefix,
generateObjectPrefix,
strictNullHandling,
skipNulls,
encoder,
filter,
sort,
allowDots,
serializeDate,
formatter,
encodeValuesOnly,
Expand Down Expand Up @@ -220,16 +233,27 @@ module.exports = function (object, opts) {
return '';
}

var arrayFormat;
if (opts && opts.arrayFormat in arrayPrefixGenerators) {
arrayFormat = opts.arrayFormat;
var generateArrayPrefix;
if (opts && typeof opts.arrayFormat === 'function') {
generateArrayPrefix = opts.arrayFormat;
} else if (opts && opts.arrayFormat in arrayPrefixGenerators) {
generateArrayPrefix = arrayPrefixGenerators[opts.arrayFormat];
} else if (opts && 'indices' in opts) {
arrayFormat = opts.indices ? 'indices' : 'repeat';
generateArrayPrefix = arrayPrefixGenerators[opts.indices ? 'indices' : 'repeat'];
} else {
arrayFormat = 'indices';
generateArrayPrefix = arrayPrefixGenerators.indices;
}

var generateArrayPrefix = arrayPrefixGenerators[arrayFormat];
var generateObjectPrefix;
if (opts && typeof opts.objectFormat === 'function') {
generateObjectPrefix = opts.objectFormat;
} else if (opts && opts.objectFormat in objectPrefixGenerators) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else if (opts && opts.objectFormat in objectPrefixGenerators) {
} else if (opts && has.call(objectPrefixGenerators, opts.objectFormat)) {

generateObjectPrefix = objectPrefixGenerators[opts.objectFormat];
} else if (opts && opts.allowDots) {
generateObjectPrefix = objectPrefixGenerators.dots;
} else {
generateObjectPrefix = objectPrefixGenerators.brackets;
}
Comment on lines +252 to +256
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems like it should be handled by the option normalization and the default?


if (!objKeys) {
objKeys = Object.keys(obj);
Expand All @@ -249,12 +273,12 @@ module.exports = function (object, opts) {
obj[key],
key,
generateArrayPrefix,
generateObjectPrefix,
options.strictNullHandling,
options.skipNulls,
options.encode ? options.encoder : null,
options.filter,
options.sort,
options.allowDots,
options.serializeDate,
options.formatter,
options.encodeValuesOnly,
Expand Down
53 changes: 49 additions & 4 deletions test/parse.js
Expand Up @@ -40,36 +40,81 @@ test('parse()', function (t) {
st.end();
});

t.test('arrayFormat: indices allows only indexed arrays', function (st) {
t.test('arrayFormat: indices allows indexed arrays', function (st) {
st.deepEqual(qs.parse('a[]=b&a[]=c', { arrayFormat: 'indices' }), { a: ['b', 'c'] });
st.deepEqual(qs.parse('a[0]=b&a[1]=c', { arrayFormat: 'indices' }), { a: ['b', 'c'] });
st.deepEqual(qs.parse('a=b,c', { arrayFormat: 'indices' }), { a: 'b,c' });
st.deepEqual(qs.parse('a=b&a=c', { arrayFormat: 'indices' }), { a: ['b', 'c'] });
st.end();
});

t.test('arrayFormat: comma allows only comma-separated arrays', function (st) {
t.test('arrayFormat: comma allows comma-separated arrays', function (st) {
st.deepEqual(qs.parse('a[]=b&a[]=c', { arrayFormat: 'comma' }), { a: ['b', 'c'] });
st.deepEqual(qs.parse('a[0]=b&a[1]=c', { arrayFormat: 'comma' }), { a: ['b', 'c'] });
st.deepEqual(qs.parse('a=b,c', { arrayFormat: 'comma' }), { a: 'b,c' });
st.deepEqual(qs.parse('a=b,c', { arrayFormat: 'comma' }), { a: ['b', 'c'] });
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this shouldn't change, or else it's a breaking change

st.deepEqual(qs.parse('a=b&a=c', { arrayFormat: 'comma' }), { a: ['b', 'c'] });
st.end();
});

t.test('arrayFormat: repeat allows only repeated values', function (st) {
t.test('arrayFormat: repeat allows repeated values', function (st) {
st.deepEqual(qs.parse('a[]=b&a[]=c', { arrayFormat: 'repeat' }), { a: ['b', 'c'] });
st.deepEqual(qs.parse('a[0]=b&a[1]=c', { arrayFormat: 'repeat' }), { a: ['b', 'c'] });
st.deepEqual(qs.parse('a=b,c', { arrayFormat: 'repeat' }), { a: 'b,c' });
st.deepEqual(qs.parse('a=b&a=c', { arrayFormat: 'repeat' }), { a: ['b', 'c'] });
st.end();
});

t.test('arrayFormat: function(){} allows custom keys for arrays', function (st) {
st.deepEqual(qs.parse('a[]=b&a[]=c', { arrayFormat: function (key) { return key.replace(/-([^-[]+)/g, '[$1]'); } }), { a: ['b', 'c'] });
st.deepEqual(qs.parse('a[0]=b&a[1]=c', { arrayFormat: function (key) { return key.replace(/-([^-[]+)/g, '[$1]'); } }), { a: ['b', 'c'] });
st.deepEqual(qs.parse('a=b,c', { arrayFormat: function (key) { return key.replace(/-([^-[]+)/g, '[$1]'); } }), { a: 'b,c' });
st.deepEqual(qs.parse('a=b&a=c', { arrayFormat: function (key) { return key.replace(/-([^-[]+)/g, '[$1]'); } }), { a: ['b', 'c'] });
st.deepEqual(qs.parse('a-0=b&a-1=c', { arrayFormat: function (key) { return key.replace(/-([^-[]+)/g, '[$1]'); } }), { a: ['b', 'c'] });
st.end();
});

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('objectFormat: brackets allows keys with brackets', function (st) {
st.deepEqual(qs.parse('a[b]=c&a[d]=e'), { a: { b: 'c', d: 'e' } });
st.deepEqual(qs.parse('a[b]=c&a[d]=e', { objectFormat: 'brackets' }), { a: { b: 'c', d: 'e' } });
st.deepEqual(qs.parse('a.b=c&a.d=e', { objectFormat: 'brackets' }), { 'a.b': 'c', 'a.d': 'e' });
st.deepEqual(qs.parse('a{b}=c&a{d}=e', { objectFormat: 'brackets' }), { 'a{b}': 'c', 'a{d}': 'e' });
st.deepEqual(qs.parse('a/b=c&a/d=e', { objectFormat: 'brackets' }), { 'a/b': 'c', 'a/d': 'e' });
st.end();
});

t.test('objectFormat: dots allows keys with dots', function (st) {
st.deepEqual(qs.parse('a.b=c&a.d=e'), { 'a.b': 'c', 'a.d': 'e' });
st.deepEqual(qs.parse('a[b]=c&a[d]=e', { objectFormat: 'dots' }), { a: { b: 'c', d: 'e' } });
st.deepEqual(qs.parse('a.b=c&a.d=e', { objectFormat: 'dots' }), { a: { b: 'c', d: 'e' } });
st.deepEqual(qs.parse('a{b}=c&a{d}=e', { objectFormat: 'dots' }), { 'a{b}': 'c', 'a{d}': 'e' });
st.deepEqual(qs.parse('a/b=c&a/d=e', { objectFormat: 'dots' }), { 'a/b': 'c', 'a/d': 'e' });
st.end();
});

t.test('objectFormat: curly allows keys with curly brackets', function (st) {
st.deepEqual(qs.parse('a{b}=c&a{d}=e'), { 'a{b}': 'c', 'a{d}': 'e' });
st.deepEqual(qs.parse('a[b]=c&a[d]=e', { objectFormat: 'curly' }), { a: { b: 'c', d: 'e' } });
st.deepEqual(qs.parse('a.b=c&a.d=e', { objectFormat: 'curly' }), { 'a.b': 'c', 'a.d': 'e' });
st.deepEqual(qs.parse('a{b}=c&a{d}=e', { objectFormat: 'curly' }), { a: { b: 'c', d: 'e' } });
st.deepEqual(qs.parse('a/b=c&a/d=e', { objectFormat: 'curly' }), { 'a/b': 'c', 'a/d': 'e' });
st.end();
});

t.test('objectFormat: function allows keys with custom format', function (st) {
st.deepEqual(qs.parse('a/b=c&a/d=e'), { 'a/b': 'c', 'a/d': 'e' });
st.deepEqual(qs.parse('a[b]=c&a[d]=e', { objectFormat: function (key) { return key.replace(/\/([^.[]+)/g, '[$1]'); } }), { a: { b: 'c', d: 'e' } });
st.deepEqual(qs.parse('a.b=c&a.d=e', { objectFormat: function (key) { return key.replace(/\/([^.[]+)/g, '[$1]'); } }), { 'a.b': 'c', 'a.d': 'e' });
st.deepEqual(qs.parse('a{b}=c&a{d}=e', { objectFormat: function (key) { return key.replace(/\/([^.[]+)/g, '[$1]'); } }), { 'a{b}': 'c', 'a{d}': 'e' });
st.deepEqual(qs.parse('a/b=c&a/d=e', { objectFormat: function (key) { return key.replace(/\/([^.[]+)/g, '[$1]'); } }), { a: { b: 'c', d: 'e' } });
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