source/http/QueryString.js
import typeDetect from '../utils/typeDetect'
/**
* Query-string creator and parser.
*
* @example <caption>Basics - build a querystring</caption>
* const querystring = new QueryString();
* querystring.set('name', 'Peter');
* querystring.setIterable('tags', ['person', 'male']);
* const encodedQuerystring = querystring.urlencode();
* // encodedQuerystring === 'name=Peter&tags=person&tags=male' // order may vary
*
* @example <caption>Parse a querystring</caption>
* const querystring = new QueryString('name=Peter&tags=person&tags=male');
* const name = querystring.get('name');
* const tags = querystring.getArray('tags');
* const firstTag = querystring.get('tags');
*
* @example <caption>Parse a querystring from window.location.search</caption>
* // window.location.search == "?name=test&age=12"
* const querystring = new QueryString(window.location.search);
* const name = querystring.get('name');
* const age = querystring.get('age');
*
* @example <caption>Parse and modify a querystring</caption>
* const querystring = new QueryString('name=Peter&tags=person&tags=male');
* querystring.set('name', 'John');
* querystring.append('tags', 'important');
* // querystring.urlencode() === 'name=John&tags=person&tags=male&tags=important'
* querystring.setIterable('tags', ['male']);
* // querystring.urlencode() === 'name=John&tags=male'
*/
export default class QueryString {
/**
*
* @param {string} querystring Optional input querystring to parse.
*/
constructor(querystring='') {
this._queryStringMap = new Map();
if(querystring) {
if(typeof querystring !== 'string') {
throw new TypeError('The querystring argument must be a string.')
}
this._parseQueryString(querystring);
}
}
/**
* Create a deep copy of this QueryString object.
*
* @return The copy.
*/
deepCopy () {
let copy = Object.assign(Object.create(this), this)
copy._queryStringMap = new Map(this._queryStringMap)
return copy
}
/**
* Returns ``true`` if the querystring is empty, otherwise ``false``.
*
* @returns {boolean}
*/
isEmpty() {
return this._queryStringMap.size === 0;
}
/**
* Remove all keys and values from the QueryString.
*/
clear() {
this._queryStringMap.clear();
}
_parseQueryStringItem(querystringItem) {
const splitPair = querystringItem.split('=');
const key = decodeURIComponent(splitPair[0]);
const value = decodeURIComponent(splitPair[1]);
this.append(key, value);
}
_parseQueryString(querystring) {
if(querystring.substring(0, 1) == '?') {
querystring = querystring.substring(1);
}
const splitQueryString = querystring.split('&');
for(const querystringItem of splitQueryString) {
this._parseQueryStringItem(querystringItem);
}
}
_addToKey(key, value) {
this._queryStringMap.get(key).push(value);
}
_setKeyToEmptyArray(key) {
this._queryStringMap.set(key, []);
}
/**
* Set values from a querystring, like window.location.search.
*
* Overwrites any key/value pairs currently in this object with keys in the
* provided ``querystring``.
*
* @example
* const querystring = new QueryString();
* querystring.set('name', 'oldname');
* querystring.addValuesFromQueryString('name=newname&age=33');
* // querystring.get('name') == 'newname'
* // querystring.get('age') == '33'
*
* @param {string} querystring A querystring, like the one in window.location.search.
* Examples: ``"?a=10"``, ``"a=10"``, ``"a=10&s=test"``.
*/
setValuesFromQueryString(querystring) {
this.merge(new this.constructor(querystring));
}
/**
* Set values from an Object.
*
* Overwrites any key/value pairs currently in this QueryString
* with key/value pairs in the provided ``object``.
*
* Uses {@link QueryString#setSmart} to set the values, so
* the values of the map can be both simple types and
* iterables like arrays and sets.
*
* @example
* const querystring = new QueryString();
* querystring.set('name', 'oldname');
* querystring.addValuesFromObject({
* name: 'newname',
* age: 33,
* tags: ['tag1', 'tag2']
* });
* // querystring.get('name') == 'newname'
* // querystring.get('age') == 33
* // querystring.getArray('tags') == ['tag1', 'tag2']
*
* @param {Object} object An Object.
*/
setValuesFromObject(object) {
for(let key of Object.keys(object)) {
this.setSmart(key, object[key]);
}
}
/**
* Set values from a Map.
*
* Overwrites any key/value pairs currently in this QueryString
* with key/value pairs in the provided ``map``.
*
* Uses {@link QueryString#setSmart} to set the values, so
* the values of the map can be both simple types and
* iterables like arrays and sets.
*
* @example
* const querystring = new QueryString();
* querystring.set('name', 'oldname');
* querystring.addValuesFromMap(new Map([
* ['name', 'newname'],
* ['age', 33],
* ['tags', ['tag1', 'tag2']]
* ]));
* // querystring.get('name') == 'newname'
* // querystring.get('age') == 33
* // querystring.getArray('tags') == ['tag1', 'tag2']
*
* @param {Map} map A map.
*/
setValuesFromMap(map) {
for(let [key, value] of map.entries()) {
this.setSmart(key, value);
}
}
/**
* Merge {@link QueryString} objects into with this object.
*
* Overwrites any key/value pairs currently in this object with keys in the
* provided queryStringObjects in provided order, with the last
* one overwriting any preceding values.
*
* @example
* const querystring = new QueryString('name=oldname');
* querystring.merge(
* new QueryString('name=newname1&age=33'),
* new QueryString('name=newname2&size=large'));
* // querystring.get('name') == 'newname2'
* // querystring.get('age') == '33'
* // querystring.get('size') == 'large'
*
* @param queryStringObjects Zero or more {@link QueryString} objects.
*/
merge(...queryStringObjects) {
for(let queryStringObject of queryStringObjects) {
for (let [key, value] of queryStringObject._queryStringMap) {
this._queryStringMap.set(key, value);
}
}
}
/**
* Set value from an iterable.
*
* @param {string} key The key to set.
* @param iterable Something that can be iterated with a
* ``for(const value of iterable)`` loop.
* All the values in the iterable must be strings.
* If the iterable is empty the key will be removed
* from the QueryString.
*
* @example
* const querystring = QueryString();
* querystring.setIterable('names', ['Peter', 'Jane']);
*/
setIterable(key, iterable) {
this._setKeyToEmptyArray(key);
for(const value of iterable) {
this._addToKey(key, value);
}
if(this._queryStringMap.get(key).length === 0) {
this.remove(key);
}
}
/**
* Set a value.
*
* @param {string} key The key to store the value as.
* @param {string} value The value to set.
*
* @example
* const querystring = QueryString();
* querystring.set('name', 'Peter');
*/
set(key, value) {
this.setIterable(key, [value]);
}
/**
* Calls {@link QueryString#set} or {@link QueryString#setIterable} depending
* on the type of the provided value.
*
* @param {string} key The key to store the value as.
* @param {string|number|boolean|array|Set} value The value to set using
* {@link QueryString#set} or {@link QueryString#setIterable} depending
* on the type.
*/
setSmart(key, value) {
const valueType = typeDetect(value)
if(valueType === 'string' || valueType === 'number' || valueType === 'boolean') {
this.set(key, value)
} else if(valueType === 'array' || valueType === 'set') {
this.setIterable(key, value)
} else {
throw new Error(`Unsupporter value type: ${valueType}`)
}
}
/**
* Get a value.
*
* @param {string} key The key to get the value for.
* @param {string} fallback An optional fallback value if the key is
* not in the QueryString. Defaults to ``undefined``.
*/
get(key, fallback) {
const value = this._queryStringMap.get(key);
if(typeof value === 'undefined') {
return fallback;
} else {
return value[0];
}
}
/**
* Append a value to a key.
*
* @param {string} key The key to append a value to.
* @param {string} value The value to append.
*
* @example
* const querystring = QueryString();
* querystring.append('names', 'Jane');
* querystring.append('names', 'Joe');
* // querystring.urlencode() === 'names=Jane&names=Joe'
*/
append(key, value) {
if (!this._queryStringMap.has(key)) {
this._setKeyToEmptyArray(key);
}
this._addToKey(key, value);
}
/**
* Get the values for the specified key as an array.
*
* Always returns an array, even if the value was set
* with {@link QueryString#set}.
*
* @param {string} key The key to get the values for.
* @param {Array} fallback An optional fallback value if they
* key is not in the QueryString. Defaults to an empty array.
* @returns {Array}
*/
getArray(key, fallback) {
if (this._queryStringMap.has(key)) {
const valueArray = this._queryStringMap.get(key);
return Array.from(valueArray);
}
if(typeof falback !== 'undefined') {
return [];
}
return fallback;
}
/**
* Remove the specified key from the QueryString.
*
* @param {string} key The key to remove.
*/
remove(key) {
this._queryStringMap.delete(key);
}
/**
* Check if the QueryString contains the given key.
*
* @param {string} key The key to check for.
* @returns {boolean}
*/
has(key) {
return this._queryStringMap.has(key);
}
_encodeKeyValue(key, value) {
key = `${key}`;
value = `${value}`;
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
}
/**
* Get the QueryString object as a string in query-string format.
*
* @example
* const querystring = QueryString();
* querystring.set('next', '/a&b/');
* querystring.set('name', 'john');
* let urlEncodedQuerystring = querystring.urlencode();
* // urlEncodedQuerystring === 'name=john&next=%2Fa%26b%2F' // order may vary
*
* @example <caption>Sort keys</caption>
* const querystring = QueryString();
* querystring.set('name', 'john');
* querystring.set('age', 33);
* let urlEncodedQuerystring = querystring.urlencode({sortKeys: true});
* // urlEncodedQuerystring === 'age=33&name=john'
*
* @example <caption>Sort values</caption>
* const querystring = QueryString();
* querystring.setIterable('name', ['john', 'amy', 'xion']);
* let urlEncodedQuerystring = querystring.urlencode();
* // urlEncodedQuerystring === 'name=amy&name=john&name=xion'
*
*
* @param {Object} options Options. All are optional
* @param {boolean} options.sortKeys Sort the keys using Array.sort? ``false`` by default.
* @param {boolean} options.sortValues Sort the values using Array.sort? ``false`` by default.
* This only makes sense if you have keys with multiple values.
* @param {boolean} options.skipEmptyValues Skip empty values? ``false`` by default.
*/
urlencode(options={}) {
const {sortKeys, sortValues, skipEmptyValues} = options
let keys = this._queryStringMap.keys()
if(sortKeys) {
keys = Array.from(keys)
keys.sort()
}
let urlEncodedArray = [];
for(let key of keys) {
let valueArray = this._queryStringMap.get(key);
if(sortValues) {
valueArray = Array.from(valueArray)
valueArray.sort()
}
for(const value of valueArray) {
if(skipEmptyValues && `${value}` === '') {
continue
}
urlEncodedArray.push(this._encodeKeyValue(key, value));
}
}
return urlEncodedArray.join('&');
}
}