diff --git a/examples.js b/examples.js index 6fe9f47..42baa15 100644 --- a/examples.js +++ b/examples.js @@ -6,34 +6,34 @@ var qs = require('./'); var obj = qs.parse('foo'); -require('inspect')(obj) +require('util').inspect(obj) var obj = qs.parse('foo=bar=baz'); -require('inspect')(obj) +require('util').inspect(obj) var obj = qs.parse('users[]'); -require('inspect')(obj) +require('util').inspect(obj) var obj = qs.parse('name=tj&email=tj@vision-media.ca'); -require('inspect')(obj) +require('util').inspect(obj) var obj = qs.parse('users[]=tj&users[]=tobi&users[]=jane'); -require('inspect')(obj) +require('util').inspect(obj) var obj = qs.parse('user[name][first]=tj&user[name][last]=holowaychuk'); -require('inspect')(obj) +require('util').inspect(obj) var obj = qs.parse('users[][name][first]=tj&users[][name][last]=holowaychuk'); -require('inspect')(obj) +require('util').inspect(obj) var obj = qs.parse('a=a&a=b&a=c'); -require('inspect')(obj) +require('util').inspect(obj) var obj = qs.parse('user[tj]=tj&user[tj]=TJ'); -require('inspect')(obj) +require('util').inspect(obj) var obj = qs.parse('user[names]=tj&user[names]=TJ&user[names]=Tyler'); -require('inspect')(obj) +require('util').inspect(obj) var obj = qs.parse('user[name][first]=tj&user[name][first]=TJ'); -require('inspect')(obj) \ No newline at end of file +require('util').inspect(obj) diff --git a/lib/querystring.js b/lib/querystring.js index 5fb3441..836e643 100644 --- a/lib/querystring.js +++ b/lib/querystring.js @@ -38,6 +38,7 @@ exports.parse = function(str) { // nested if (~key.indexOf(']')) { + var parts = key.split('[') , len = parts.length , last = len - 1; @@ -55,15 +56,51 @@ exports.parse = function(str) { parent[key] = [parent[key], val]; } // array - } else if (']' == part) { - obj = parent[key] = Array.isArray(parent[key]) - ? parent[key] - : []; - if ('' != val) obj.push(val); + } else if (']' == part || ('0]' == part && JSON.stringify(obj) == '{}')) { + // existing array, append to it + if (Array.isArray(parent[key])) { + obj = parent[key]; + } else { + obj = []; + // was an object, but parser found out it was actually an array! + // preserve any named indices if we can + if (parent[key]) { + for(var field in parent[key]) { + obj[field] = parent[key][field]; + } + } + } + + parent[key] = obj; + + // if there are more parts, we need to continue parsing + if (parts.length) { + // if obj is an empty array, push a new object into in + if (!obj.length || 'object' != typeof(obj[obj.length - 1])) obj.push({}); + + // the new obj is the last one in this array + parse(obj[obj.length - 1], parts, obj, part); + } else if ('' != val) { + obj.push(val); + } + // prop } else if (~part.indexOf(']')) { part = part.substr(0, part.length - 1); + + // does object already have property AND parent is a nested array + if (obj[part] && Array.isArray(parent)) { + var left = parts.length; + + // are there no more parts OR + // is the last part not a '[]' AND the next part already exists as a previously parsed property + if (!left || (parts[left - 1] != ']' && obj[part][parts[0].slice(0, -1)] !== undefined)) { + parent.push(obj = {}); + } + } + parse(obj[part] = obj[part] || {}, parts, obj, part); + // key } else { parse(obj[part] = obj[part] || {}, parts, obj, part); diff --git a/test/querystring.test.js b/test/querystring.test.js index a4ca6db..61c570a 100644 --- a/test/querystring.test.js +++ b/test/querystring.test.js @@ -117,17 +117,97 @@ module.exports = { qs.parse('').should.eql({}); qs.parse(undefined).should.eql({}); qs.parse(null).should.eql({}); + }, + + 'test complex': function(){ + // if no arrays are EVER specified, parser will create arrays for you + qs.parse('users[name][first]=tj&users[name][first]=tobi') + .should.eql({ + users: { name: { first: [ 'tj', 'tobi' ] } } + }); + + // once an array is specified, objects that are parsed are PUSHED onto the array + qs.parse('users[][name][first]=tj&users[][name][first]=tobi') + .should.eql({ + users: [ { name: { first: 'tj' } }, { name: { first: 'tobi' } } ] + }); + + // arrays can be specified within objects + qs.parse('users[name][][first]=tj&users[name][][first]=tobi') + .should.eql({ + users: { name: [ { first: 'tj' }, { first: 'tobi' } ] } + }); + + // if you specify an array but want a deeper object to be pushed into a deeper array, you can specify that with [] + qs.parse('users[][name][first][]=tj&users[][name][first][]=tobi') + .should.eql({ + users: [ { name: { first: [ 'tj', 'tobi' ] } } ] + }); + + // you can continue adding to a deep object object + qs.parse('users[][name][first]=tj&users[][name][last]=holowaychuk') + .should.eql({ + users: [ { name: { first: 'tj', last: 'holowaychuk' } } ] + }); + + // order matters, once a new object is pushed, you can't add anything back into the youngest object + qs.parse('users[][name][first]=tobi&users[][name][first]=tj&users[][name][last]=holowaychuk') + .should.eql({ + users: [ { name: { first: 'tobi' } }, { name: { first: 'tj', last: 'holowaychuk' } } ] + }); + }, + + 'test deep': function(){ + // deep objects should work + qs.parse('users[][name][first][nickname]=tj&users[][name][first][nickname]=tobi') + .should.eql({ + users: [ { name: { first: { nickname: 'tj' } } }, { name: { first: { nickname: 'tobi' } } } ] + }); + + // deep objects should work + qs.parse('users[][name][first][nickname][]=tj&users[][name][first][nickname][]=tobi') + .should.eql({ + users: [ { name: { first: { nickname: [ 'tj', 'tobi' ] } } } ] + }); + }, + + 'test deep complex': function() { + // deep objects should work + qs.parse('a[b][][c]=1&a[b][]=2&a[b][]=3&a[b][][d]=4') + .should.eql({ + a: { b : [ { c: 1 }, 2, 3, { d: 4 } ] } + }); + }, + + 'indice types': function() { + // numbered indice starting with 0 or [] means we want an array + qs.parse('a[0]=1') + .should.eql({ + a: [1] + }).obj.a.should.be.an.instanceof(Array); + + // without [0] or [], means we want an object + qs.parse('a[1]=2') + .should.eql({ + a: { 1: 2 } + }).obj.a.should.be.a('object'); + + // [] triggers the array creation, no matter what, it will push from last indice that it can + qs.parse('a[8]=1&a[]=2&a[2]=3&a[500]=4') + .should.eql({ + a: {2:3, 8:1, 9:2, 500:4} + }).obj.a.should.be.an.instanceof(Array); + + // arrays can have named indices!? + qs.parse('a[foo]=bar&a[]=2') + .should.eql({ + a: { foo: 'bar', 0: 2} + }).obj.a.should.be.an.instanceof(Array); + + // no [] or [0], then we have an object, no matter what + qs.parse('a[foo]=bar&a[1]=2') + .should.eql({ + a: { foo: 'bar', 1: 2} + }).obj.a.should.be.a('object'); } - - // 'test complex': function(){ - // qs.parse('users[][name][first]=tj&users[foo]=bar') - // .should.eql({ - // users: [ { name: 'tj' }, { name: 'tobi' }, { foo: 'bar' }] - // }); - // - // qs.parse('users[][name][first]=tj&users[][name][first]=tobi') - // .should.eql({ - // users: [ { name: 'tj' }, { name: 'tobi' }] - // }); - // } };