Rewrote QueryString.parse to make it smaller and more effective.
authorDmitry Baranovskiy <Dmitry@Baranovskiy.com>
Tue, 29 Jun 2010 08:24:43 +0000 (18:24 +1000)
committerRyan Dahl <ry@tinyclouds.org>
Wed, 30 Jun 2010 06:52:42 +0000 (23:52 -0700)
Also added ability to parse foo.bar=4 equal to foo[bar]=4
Added tests for this as well

lib/querystring.js
test/simple/test-querystring.js

index e3b75ba..efd2969 100644 (file)
@@ -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;
@@ -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.
 
@@ -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, "");
+}
\ No newline at end of file
index 45d2c35..f3b3301 100644 (file)
@@ -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" }]