initial commit master v1.0.0
authorJustin Wind <justin.wind+git@gmail.com>
Tue, 30 Apr 2019 17:29:04 +0000 (10:29 -0700)
committerJustin Wind <justin.wind+git@gmail.com>
Mon, 21 Sep 2020 22:37:39 +0000 (15:37 -0700)
.eslintrc.json [new file with mode: 0644]
.gitignore [new file with mode: 0644]
README.md [new file with mode: 0644]
index.js [new file with mode: 0755]
lib/parse.js [new file with mode: 0755]
package.json [new file with mode: 0644]
test/index.js [new file with mode: 0755]

diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644 (file)
index 0000000..7602b33
--- /dev/null
@@ -0,0 +1,82 @@
+{
+  "env": {
+    "browser": false,
+    "es6": true,
+    "node": true
+  },
+  "extends": [
+    "eslint:recommended",
+    "plugin:node/recommended",
+    "plugin:security/recommended",
+    "plugin:sonarjs/recommended"
+  ],
+  "parserOptions": {
+    "ecmaVersion": 2018
+  },
+  "plugins": [
+    "node",
+    "security",
+    "sonarjs"
+  ],
+  "rules": {
+    "array-element-newline": [
+      "error",
+      "consistent"
+    ],
+    "arrow-parens": [
+      "error",
+      "always"
+    ],
+    "arrow-spacing": [
+      "error",
+      {
+        "after": true,
+        "before": true
+      }
+    ],
+    "block-scoped-var": "error",
+    "block-spacing": "error",
+    "brace-style": "error",
+    "callback-return": "error",
+    "camelcase": "error",
+    "capitalized-comments": "warn",
+    "class-methods-use-this": "error",
+    "comma-dangle": [
+      "error",
+      "always-multiline"
+    ],
+    "comma-spacing": [
+      "error",
+      {
+        "after": true,
+        "before": false
+      }
+    ],
+    "comma-style": [
+      "error",
+      "last"
+    ],
+    "sonarjs/cognitive-complexity": "warn",
+    "keyword-spacing": "error",
+    "linebreak-style": [
+      "error",
+      "unix"
+    ],
+    "no-unused-vars": [
+      "error", {
+        "varsIgnorePattern": "^_"
+      }
+    ],
+    "object-curly-spacing": [
+      "error",
+      "always"
+    ],
+    "prefer-const": "error",
+    "quotes": [
+      "error",
+      "single"
+    ],
+    "strict": "error",
+    "vars-on-top": "error"
+  }
+}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..16d4f51
--- /dev/null
@@ -0,0 +1,2 @@
+.nyc_output
+node_modules
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..927404e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,11 @@
+# Another in-house query string parser
+
+This exists due to needing to support parsing a certain legacy format, which the built-in parser cannot quite handle meaningfully.
+
+Primary differences:
+ * recognize both & and ; as parameter separators
+ * for valueless keys, value is set to null, not ''
+ * empty parameters are included as a '' key
+
+These behaviors are admittedly idiosyncratic.
+
diff --git a/index.js b/index.js
new file mode 100755 (executable)
index 0000000..8e1ea90
--- /dev/null
+++ b/index.js
@@ -0,0 +1,8 @@
+'use strict';
+
+const { parse } = require('./lib/parse');
+
+module.exports = {
+  parse: parse,
+};
+
diff --git a/lib/parse.js b/lib/parse.js
new file mode 100755 (executable)
index 0000000..c0c52a6
--- /dev/null
@@ -0,0 +1,73 @@
+/* eslint-disable security/detect-object-injection */
+'use strict';
+
+const SPACES = /\+/ug;
+const DELIM = /[&;]/u;
+
+/**
+ * Separate a string into decoded key and value.
+ * @param {string} param
+ * @returns {object} .key
+ *                   .val
+ */
+const splitKeyValue = (param) => {
+  const idx = param.indexOf('=');
+  let key, val;
+  if (idx >= 0) {
+    key = decodeURIComponent(param.slice(0, idx));
+    val = decodeURIComponent(param.slice(idx + 1));
+  } else {
+    key = decodeURIComponent(param);
+    val = null;
+  }
+  return {
+    key,
+    val,
+  };
+};
+
+/**
+ * Parse a raw querystring (as provided by url.parse) into a map of
+ * parameter values.
+ * This is nearly the same as the node-provided querystring.parse,
+ * but is more accepting of legacy conventions and (subjectively) more convenient in output.
+ * Primary differences:
+ *   for valueless keys, value is set to null, not ''
+ *   ; is a valid parameter separator
+ *   empty parameters are included as a '' key
+ * 
+ * @param {string} querystring - raw query string
+ * @param {boolean} arraySuffix - allow param[] to explicitly treat param as array, à la PHP
+ * @returns {object} - parsed data
+ */
+function parse(querystring, arraySuffix=false) {
+  const params = {};
+  if (querystring) {
+    const queries = querystring.replace(SPACES, '%20').split(DELIM);
+    queries.forEach((query) => {
+      // eslint-disable-next-line prefer-const
+      let { key, val } = splitKeyValue(query);
+
+      if (arraySuffix
+      &&  key.endsWith('[]')) {
+        key = key.slice(0, key.length - 2);
+        if (!Object.prototype.hasOwnProperty.call(params, key)) {
+          params[key] = Array();
+        }
+      }
+
+      if (!Object.prototype.hasOwnProperty.call(params, key)) {
+        params[key] = val;
+      } else if (Array.isArray(params[key])) {
+        params[key].push(val);
+      } else {
+        params[key] = Array(params[key], val);
+      }
+    });
+  }
+  return params;
+}
+
+module.exports = {
+  parse,
+};
diff --git a/package.json b/package.json
new file mode 100644 (file)
index 0000000..e869990
--- /dev/null
@@ -0,0 +1,25 @@
+{
+  "name": "@squeep/querystring",
+  "version": "1.0.0",
+  "description": "An in-house query string parser.",
+  "repository": {
+    "type": "git",
+    "url": "https://git.squeep.com/squeep-querystring/"
+  },
+  "main": "index.js",
+  "scripts": {
+    "test": "mocha",
+    "coverage": "nyc mocha",
+    "eslint": "eslint index.js lib"
+  },
+  "author": "Justin Wind <jwind-npm@squeep.com>",
+  "license": "ISC",
+  "devDependencies": {
+    "eslint": "^7.9.0",
+    "eslint-plugin-node": "^11.1.0",
+    "eslint-plugin-security": "^1.4.0",
+    "eslint-plugin-sonarjs": "^0.5.0",
+    "mocha": "^8.1.3",
+    "nyc": "^15.1.0"
+  }
+}
diff --git a/test/index.js b/test/index.js
new file mode 100755 (executable)
index 0000000..0541b54
--- /dev/null
@@ -0,0 +1,100 @@
+/* eslint-disable capitalized-comments */
+/* eslint-env mocha */
+'use strict';
+
+
+const assert = require('assert');
+const qs = require('../index.js');
+
+describe('querystring', function () {
+  describe('parse', function () {
+
+    it('should return empty object from no input', function () {
+      assert.deepStrictEqual(
+        qs.parse(undefined),
+        {},
+      );
+    });
+
+    it('should return empty object from empty string', function () {
+      assert.deepStrictEqual(
+        qs.parse(''),
+        {},
+      );
+    });
+
+    it('should return simple mapping', function () {
+      assert.deepStrictEqual(
+        qs.parse('foo=1&bar=2&baz=quux'),
+        {
+          foo: '1',
+          bar: '2',
+          baz: 'quux',
+        },
+      );
+    });
+
+    it('should return array for repeated keys', function () {
+      assert.deepStrictEqual(
+        qs.parse('foo=1&foo=2&foo=quux'),
+        {
+          foo: [ '1', '2', 'quux' ],
+        },
+      );
+    });
+
+    it('should return null values for bare keys', function () {
+      assert.deepStrictEqual(
+        qs.parse('foo&foo&bar&quux'),
+        {
+          foo: [ null, null ],
+          bar: null,
+          quux: null,
+        },
+      );
+    });
+
+    it('should handle space encodings and equals in values', function () {
+      assert.deepStrictEqual(
+        qs.parse('this+key=+spacey+string+;&foo=equally=string&'),
+        {
+          '': [ null, null ],
+          'this key': ' spacey string ',
+          foo: 'equally=string',
+        },
+      );
+    });
+
+    it('should handle raw spaces and encoded spaces', function () {
+      assert.deepStrictEqual(
+        qs.parse('this+has actual+spaces=but okay%20whatever'),
+        {
+          'this has actual spaces': 'but okay whatever',
+        },
+      );
+    });
+
+    it('should handle empty values', function () {
+      assert.deepStrictEqual(
+        qs.parse(';;;;;;;+;;;;'),
+        {
+          '': [ null, null, null, null, null, null, null, null, null, null, null ],
+          ' ': null,
+        },
+      );
+    });
+
+    it('should handle PHP style array declarations', function () {
+      assert.deepStrictEqual(
+        qs.parse('foo%5B%5D&bar[]=1&bar[]=2&baz', true),
+        {
+          'foo': [null],
+          bar: ['1', '2'],
+          baz: null,
+        },
+      )
+    });
+
+  }); // parse
+});
+