diff --git a/README.md b/README.md index 84f2534e..9b86f12a 100644 --- a/README.md +++ b/README.md @@ -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' } }); +``` + +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' } }); ``` @@ -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) @@ -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: @@ -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' +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: diff --git a/lib/parse.js b/lib/parse.js index 49d5c042..b9f3432c 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -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]'); + }, + 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, @@ -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(','); } @@ -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) { + key = arrayKeyReplacer[options.arrayFormat](key); + } + if (typeof options.objectFormat === 'function') { + key = options.objectFormat(key); + } else if (options.objectFormat in objectKeyReplacer) { + key = objectKeyReplacer[options.objectFormat](key); + } // The regex chunks @@ -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, @@ -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, diff --git a/lib/stringify.js b/lib/stringify.js index 72ded14a..d96c818d 100644 --- a/lib/stringify.js +++ b/lib/stringify.js @@ -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) { @@ -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, @@ -58,12 +71,12 @@ var stringify = function stringify( object, prefix, generateArrayPrefix, + generateObjectPrefix, strictNullHandling, skipNulls, encoder, filter, sort, - allowDots, serializeDate, formatter, encodeValuesOnly, @@ -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, @@ -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, @@ -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) { + generateObjectPrefix = objectPrefixGenerators[opts.objectFormat]; + } else if (opts && opts.allowDots) { + generateObjectPrefix = objectPrefixGenerators.dots; + } else { + generateObjectPrefix = objectPrefixGenerators.brackets; + } if (!objKeys) { objKeys = Object.keys(obj); @@ -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, diff --git a/test/parse.js b/test/parse.js index b6ec1b72..3175ea1c 100644 --- a/test/parse.js +++ b/test/parse.js @@ -40,7 +40,7 @@ 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' }); @@ -48,15 +48,15 @@ test('parse()', function (t) { 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'] }); 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' }); @@ -64,12 +64,57 @@ test('parse()', function (t) { 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( diff --git a/test/stringify.js b/test/stringify.js index 760d08cb..120ba8d4 100644 --- a/test/stringify.js +++ b/test/stringify.js @@ -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(); }); @@ -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');