From 5a64661cd7a9d7c2a5ab93f2e97ac93722c4792c Mon Sep 17 00:00:00 2001 From: Justin Wind Date: Tue, 30 Apr 2019 10:29:04 -0700 Subject: [PATCH 1/1] initial commit --- .eslintrc.json | 82 ++++++++++++++++++++++++++++++++++++++++ .gitignore | 2 + README.md | 11 ++++++ index.js | 8 ++++ lib/parse.js | 73 ++++++++++++++++++++++++++++++++++++ package.json | 25 +++++++++++++ test/index.js | 100 +++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 301 insertions(+) create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 README.md create mode 100755 index.js create mode 100755 lib/parse.js create mode 100644 package.json create mode 100755 test/index.js diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..7602b33 --- /dev/null +++ b/.eslintrc.json @@ -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 index 0000000..16d4f51 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.nyc_output +node_modules diff --git a/README.md b/README.md new file mode 100644 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 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 index 0000000..c0c52a6 --- /dev/null +++ b/lib/parse.js @@ -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 index 0000000..e869990 --- /dev/null +++ b/package.json @@ -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 ", + "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 index 0000000..0541b54 --- /dev/null +++ b/test/index.js @@ -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 +}); + -- 2.43.2