Skip to content

Commit

Permalink
Rewrote QueryString.parse to make it smaller and more effective.
Browse files Browse the repository at this point in the history
Also added ability to parse foo.bar=4 equal to foo[bar]=4
Added tests for this as well
  • Loading branch information
DmitryBaranovskiy authored and ry committed Jun 30, 2010
1 parent 0a8bd34 commit f8ca6b3
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 99 deletions.
166 changes: 67 additions & 99 deletions lib/querystring.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Query String Utilities

var QueryString = exports;
var urlDecode = process.binding('http_parser').urlDecode;
var urlDecode = process.binding("http_parser").urlDecode;

// a safe fast alternative to decodeURIComponent
QueryString.unescape = urlDecode;
Expand All @@ -26,24 +26,24 @@ var stack = [];
* @static
*/
QueryString.stringify = QueryString.encode = function (obj, sep, eq, munge, name) {
munge = typeof(munge) == "undefined" || munge;
munge = typeof munge == "undefined" || munge;
sep = sep || "&";
eq = eq || "=";
if (obj == null || typeof(obj) === 'function') {
return name ? QueryString.escape(name) + eq : '';
if (obj == null || typeof obj == "function") {
return name ? QueryString.escape(name) + eq : "";
}

if (isBool(obj)) obj = +obj;
if (isBool(obj)) {
obj = +obj;
}
if (isNumber(obj) || isString(obj)) {
return QueryString.escape(name) + eq + QueryString.escape(obj);
}
if (isA(obj, [])) {
var s = [];
name = name+(munge ? '[]' : '');
for (var i = 0, l = obj.length; i < l; i ++) {
s.push( QueryString.stringify(obj[i], sep, eq, munge, name) );
}
return s.join(sep);
name = name + (munge ? "[]" : "");
return obj.map(function (item) {
return QueryString.stringify(item, sep, eq, munge, name);
}).join(sep);
}
// now we know it's an object.

Expand All @@ -54,107 +54,75 @@ QueryString.stringify = QueryString.encode = function (obj, sep, eq, munge, name

stack.push(obj);

var s = [];
var begin = name ? name + '[' : '';
var end = name ? ']' : '';
var keys = Object.keys(obj);
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
var n = begin + key + end;
s.push(QueryString.stringify(obj[key], sep, eq, munge, n));
}
var begin = name ? name + "[" : "",
end = name ? "]" : "",
keys = Object.keys(obj),
n,
s = Object.keys(obj).map(function (key) {
n = begin + key + end;
return QueryString.stringify(obj[key], sep, eq, munge, n);
}).join(sep);

stack.pop();

s = s.join(sep);
if (!s && name) return name + "=";
if (!s && name) {
return name + "=";
}
return s;
};

QueryString.parse = QueryString.decode = function (qs, sep, eq) {
return (qs || '')
.split(sep||"&")
.map(pieceParser(eq||"="))
.reduce(mergeParams);
};

// matches .xxxxx or [xxxxx] or ['xxxxx'] or ["xxxxx"] with optional [] at the end
var chunks = /(?:(?:^|\.)([^\[\(\.]+)(?=\[|\.|$|\()|\[([^"'][^\]]*?)\]|\["([^\]"]*?)"\]|\['([^\]']*?)'\])(\[\])?/g;
// Parse a key=val string.
// These can get pretty hairy
// example flow:
// parse(foo[bar][][bla]=baz)
// return parse(foo[bar][][bla],"baz")
// return parse(foo[bar][], {bla : "baz"})
// return parse(foo[bar], [{bla:"baz"}])
// return parse(foo, {bar:[{bla:"baz"}]})
// return {foo:{bar:[{bla:"baz"}]}}
var slicerPattern = /(.*)\[([^\]]*)\]$/;
var pieceParser = function (eq) {
return function parsePiece (key, val) {
if (arguments.length !== 2) {
// key=val, called from the map/reduce
key = key.split(eq);
return parsePiece(QueryString.unescape(key.shift(), true),
QueryString.unescape(key.join(eq), true));
}
var sliced = slicerPattern.exec(key);
if (!sliced) {
var ret = {};
if (key) ret[key] = val;
return ret;
}
// ["foo[][bar][][baz]", "foo[][bar][]", "baz"]
var tail = sliced[2], head = sliced[1];

// array: key[]=val
if (!tail) return parsePiece(head, [val]);

// obj: key[subkey]=val
var ret = {};
ret[tail] = val;
return parsePiece(head, ret);
};
QueryString.parse = QueryString.decode = function (qs, sep, eq) {
var obj = {};
String(qs).split(sep || "&").map(function (keyValue) {
var res = obj,
next,
kv = keyValue.split(eq || "="),
key = QueryString.unescape(kv.shift(), true),
value = QueryString.unescape(kv.join(eq || "="), true);
key.replace(chunks, function (all, name, nameInBrackets, nameIn2Quotes, nameIn1Quotes, isArray, offset) {
var end = offset + all.length == key.length;
name = name || nameInBrackets || nameIn2Quotes || nameIn1Quotes;
next = end ? value : {};
next = next && (+next == next ? +next : next);
if (Array.isArray(res[name])) {
res[name].push(next);
res = next;
} else {
if (name in res) {
if (isArray || end) {
res = (res[name] = [res[name], next])[1];
} else {
res = res[name];
}
} else {
if (isArray) {
res = (res[name] = [next])[0];
} else {
res = res[name] = next;
}
}
}
});
});
return obj;
};

// the reducer function that merges each query piece together into one set of params
function mergeParams (params, addition) {
return (
// if it's uncontested, then just return the addition.
(!params) ? addition
// if the existing value is an array, then concat it.
: (isA(params, [])) ? params.concat(addition)
// if the existing value is not an array, and either are not objects, arrayify it.
: (!isA(params, {}) || !isA(addition, {})) ? [params].concat(addition)
// else merge them as objects, which is a little more complex
: mergeObjects(params, addition)
);
}

// Merge two *objects* together. If this is called, we've already ruled
// out the simple cases, and need to do a loop.
function mergeObjects (params, addition) {
var keys = Object.keys(addition);
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
if (key) {
params[key] = mergeParams(params[key], addition[key]);
}
}
return params;
}

function isA (thing, canon) {
// special case for null and undefined
if (thing == null || canon == null) {
return thing === canon;
}
return Object.getPrototypeOf(Object(thing)) == Object.getPrototypeOf(Object(canon));
// special case for null and undefined
if (thing == null || canon == null) {
return thing === canon;
}
return Object.getPrototypeOf(Object(thing)) == Object.getPrototypeOf(Object(canon));
}
function isBool (thing) {
return isA(thing, true);
return isA(thing, true);
}
function isNumber (thing) {
return isA(thing, 0) && isFinite(thing);
return isA(thing, 0) && isFinite(thing);
}
function isString (thing) {
return isA(thing, "");
}
return isA(thing, "");
}
9 changes: 9 additions & 0 deletions test/simple/test-querystring.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ var qsTestCases = [
["foo[bar][bla]=baz&foo[bar][bla]=blo", "foo%5Bbar%5D%5Bbla%5D%5B%5D=baz&foo%5Bbar%5D%5Bbla%5D%5B%5D=blo", {"foo":{"bar":{"bla":["baz","blo"]}}}],
["foo[bar][][bla]=baz&foo[bar][][bla]=blo", "foo%5Bbar%5D%5B%5D%5Bbla%5D=baz&foo%5Bbar%5D%5B%5D%5Bbla%5D=blo", {"foo":{"bar":[{"bla":"baz"},{"bla":"blo"}]}}],
["foo[bar][bla][]=baz&foo[bar][bla][]=blo", "foo%5Bbar%5D%5Bbla%5D%5B%5D=baz&foo%5Bbar%5D%5Bbla%5D%5B%5D=blo", {"foo":{"bar":{"bla":["baz","blo"]}}}],

["foo.bar.bla=baz&foo.bar.bla=blo", "foo%5Bbar%5D%5Bbla%5D%5B%5D=baz&foo%5Bbar%5D%5Bbla%5D%5B%5D=blo", {"foo":{"bar":{"bla":["baz","blo"]}}}],
["foo.bar[].bla=baz&foo[bar][][bla]=blo", "foo%5Bbar%5D%5B%5D%5Bbla%5D=baz&foo%5Bbar%5D%5B%5D%5Bbla%5D=blo", {"foo":{"bar":[{"bla":"baz"},{"bla":"blo"}]}}],
["foo[bar].bla[]=baz&foo.bar[bla][]=blo", "foo%5Bbar%5D%5Bbla%5D%5B%5D=baz&foo%5Bbar%5D%5Bbla%5D%5B%5D=blo", {"foo":{"bar":{"bla":["baz","blo"]}}}],

["foo['bar']['bla']=baz&foo[\"bar\"][\"bla\"]=blo", "foo%5Bbar%5D%5Bbla%5D%5B%5D=baz&foo%5Bbar%5D%5Bbla%5D%5B%5D=blo", {"foo":{"bar":{"bla":["baz","blo"]}}}],
["foo['bar'][]['bla']=baz&foo['bar'][][\"bla\"]=blo", "foo%5Bbar%5D%5B%5D%5Bbla%5D=baz&foo%5Bbar%5D%5B%5D%5Bbla%5D=blo", {"foo":{"bar":[{"bla":"baz"},{"bla":"blo"}]}}],
["foo[bar][\"bla\"][]=baz&foo[\"bar\"][bla][]=blo", "foo%5Bbar%5D%5Bbla%5D%5B%5D=baz&foo%5Bbar%5D%5Bbla%5D%5B%5D=blo", {"foo":{"bar":{"bla":["baz","blo"]}}}],

[" foo = bar ", "%20foo%20=%20bar%20", {" foo ":" bar "}],
["foo=%zx", "foo=%25zx", {"foo":"%zx"}],
["foo=%EF%BF%BD", "foo=%EF%BF%BD", {"foo" : "\ufffd" }]
Expand Down

0 comments on commit f8ca6b3

Please sign in to comment.