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 1 commit
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,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 +392,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
46 changes: 35 additions & 11 deletions dist/qs.js
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,18 @@ var arrayPrefixGenerators = {
}
};

var objectPrefixGenerators = {
leninlin marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -340,6 +352,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 @@ -370,12 +383,12 @@ var stringify = function stringify(
object,
prefix,
generateArrayPrefix,
generateObjectPrefix,
strictNullHandling,
skipNulls,
encoder,
filter,
sort,
allowDots,
serializeDate,
formatter,
encodeValuesOnly,
Expand Down Expand Up @@ -432,12 +445,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 @@ -446,14 +459,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 @@ -532,16 +545,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) {
generateObjectPrefix = objectPrefixGenerators[opts.objectFormat];
} else if (opts && opts.allowDots) {
generateObjectPrefix = objectPrefixGenerators.dots;
} else {
generateObjectPrefix = objectPrefixGenerators.brackets;
}

if (!objKeys) {
objKeys = Object.keys(obj);
Expand All @@ -561,12 +585,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
46 changes: 35 additions & 11 deletions lib/stringify.js
Original file line number Diff line number Diff line change
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
75 changes: 75 additions & 0 deletions test/stringify.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,56 @@ test('stringify()', function (t) {
'default => indices'
);

st.equal(
qs.stringify(
{ a: [{ b: 'c' }] },
{ objectFormat: 'dots', encode: false, arrayFormat: 'indices' }
),
'a[0].b=c',
'indices => indices'
);
st.equal(
qs.stringify(
{ a: [{ b: 'c' }] },
{ objectFormat: 'dots', encode: false, arrayFormat: 'brackets' }
),
'a[].b=c',
'brackets => brackets'
);
st.equal(
qs.stringify(
{ a: [{ b: 'c' }] },
{ objectFormat: 'dots', encode: false }
),
'a[0].b=c',
'default => indices'
);

st.equal(
qs.stringify(
{ a: [{ b: { c: [1] } }] },
{ objectFormat: 'dots', encode: false, arrayFormat: 'indices' }
),
'a[0].b.c[0]=1',
'indices => indices'
);
st.equal(
qs.stringify(
{ a: [{ b: { c: [1] } }] },
{ objectFormat: 'dots', encode: false, arrayFormat: 'brackets' }
),
'a[].b.c[]=1',
'brackets => brackets'
);
st.equal(
qs.stringify(
{ a: [{ b: { c: [1] } }] },
{ objectFormat: 'dots', encode: false }
),
'a[0].b.c[0]=1',
'default => indices'
);

st.end();
});

Expand Down Expand Up @@ -317,11 +367,36 @@ test('stringify()', function (t) {
st.end();
});

t.test('uses custom function notation for arrays when no arrayFormat=function(){}', function (st) {
st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: function (prefix, key) { return prefix + '-' + key; } }), 'a-0=b&a-1=c');
st.end();
});

t.test('stringifies a complicated object', function (st) {
st.equal(qs.stringify({ a: { b: 'c', d: 'e' } }), 'a%5Bb%5D=c&a%5Bd%5D=e');
st.end();
});

t.test('uses brackets notation for objects when no objectFormat=brackets', function (st) {
st.equal(qs.stringify({ a: { b: 'c', d: 'e' } }, { objectFormat: 'brackets' }), 'a%5Bb%5D=c&a%5Bd%5D=e');
st.end();
});

t.test('uses dots notation for objects when no objectFormat=dots', function (st) {
st.equal(qs.stringify({ a: { b: 'c', d: 'e' } }, { objectFormat: 'dots' }), 'a.b=c&a.d=e');
st.end();
});

t.test('uses curly notation for objects when no objectFormat=curly', function (st) {
st.equal(qs.stringify({ a: { b: 'c', d: 'e' } }, { objectFormat: 'curly' }), 'a%7Bb%7D=c&a%7Bd%7D=e');
st.end();
});

t.test('uses custom function notation for objects when no objectFormat=function(){}', function (st) {
st.equal(qs.stringify({ a: { b: 'c', d: 'e' } }, { objectFormat: function (prefix, key) { return prefix + '/' + key; } }), 'a%2Fb=c&a%2Fd=e');
st.end();
});

t.test('stringifies an empty value', function (st) {
st.equal(qs.stringify({ a: '' }), 'a=');
st.equal(qs.stringify({ a: null }, { strictNullHandling: true }), 'a');
Expand Down