blob: 129c975dd03ab884de215b1d694e8a1fb3a2eec7 [file] [log] [blame]
/* eslint-disable no-prototype-builtins */
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/** @module common-sk/modules/query
* @descripiton Utilities for working with URL query strings.
*/
import { HintableObject } from './hintable';
/** ParamSet mirrors //infra/go/paramtools ParamSet. */
export type ParamSet = { [key: string]: string[] };
/** fromParamSet encodes an object of the form:
* <pre>
* {
* a:["2", "4"],
* b:["3"]
* }
* </pre>
*
* to a query string like:
*
* <pre>
* "a=2&a=4&b=3"
* </pre>
*
* This function handles URI encoding of both keys and values.
*
* @param {Object} o The object to encode.
* @returns {string}
*/
export function fromParamSet(o: ParamSet): string {
if (!o) {
return '';
}
const ret: string[] = [];
const keys = Object.keys(o).sort();
keys.forEach((key) => {
o[key].forEach((value) => {
ret.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
});
});
return ret.join('&');
}
/** toParamSet parses a query string into an object with
* arrays of values for the values. I.e.
*
* <pre>
* "a=2&b=3&a=4"
* </pre>
*
* decodes to
*
* <pre>
* {
* a:["2", "4"],
* b:["3"],
* }
* </pre>
*
* This function handles URI decoding of both keys and values.
*
* @param {string} s The query string to decode.
* @returns {ParamSet}
*/
export function toParamSet(s: string): ParamSet {
s = s || '';
const ret: ParamSet = {};
const vars = s.split('&');
for (const v of vars) {
const pair = v.split('=', 2);
if (pair.length === 2) {
const key = decodeURIComponent(pair[0]);
const value = decodeURIComponent(pair[1]);
if (ret.hasOwnProperty(key)) {
ret[key].push(value);
} else {
ret[key] = [value];
}
}
}
return ret;
}
/** fromObject takes an object and encodes it into a query string.
*
* The reverse of this function is toObject.
*
* @param o - The object to encode.
*/
export function fromObject(o: HintableObject): string {
const ret: string[] = [];
Object.keys(o)
.sort()
.forEach((key) => {
const value = o[key];
if (Array.isArray(value)) {
value.forEach((v: string) => {
ret.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
});
} else if (typeof value === 'object') {
ret.push(
`${encodeURIComponent(key)}=${encodeURIComponent(fromObject(value))}`
);
} else {
ret.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
}
});
return ret.join('&');
}
/** toObject decodes a query string into an object.
*
* Uses the 'target' as a source for hinting on the types of the values.
* For example:
*
* <pre>
* "a=2&b=true"
* </pre>
*
* decodes to:
*
* <pre>
* {
* a: 2,
* b: true,
* }
* </pre>
*
* When given a target of:
*
* <pre>
* {
* a: 1.0,
* b: false,
* }
* </pre>
*
* Note that a target of {} would decode
* the same query string into:
*
* <pre>
* {
* a: "2",
* b: "true",
* }
* </pre>
*
* Only Number, String, Boolean, Object, and Array of String hints are supported.
*
* @param s - The query string.
* @param target - The object that contains the type hints.
*/
export function toObject(s: string, target: HintableObject): HintableObject {
target = target || {};
const ret: { [key: string]: any } = {};
const vars = s.split('&');
for (const v of vars) {
const pair = v.split('=', 2);
if (pair.length === 2) {
const key = decodeURIComponent(pair[0]);
const value = decodeURIComponent(pair[1]);
if (target.hasOwnProperty(key)) {
const targetValue = target[key];
switch (typeof targetValue) {
case 'boolean':
ret[key] = value === 'true';
break;
case 'number':
ret[key] = Number(value);
break;
case 'object': // Arrays report as 'object' to typeof.
if (Array.isArray(targetValue)) {
const r = ret[key] || [];
r.push(value);
ret[key] = r;
} else {
ret[key] = toObject(value, targetValue);
}
break;
case 'string':
ret[key] = value;
break;
default:
ret[key] = value;
}
} else {
ret[key] = value;
}
}
}
return ret;
}
/** splitAmp returns the given query string as a newline
* separated list of key value pairs. If sepator is not
* provided newline will be used.
*
* @param [queryStr=''] A query string.
* @param [separator='\n'] The separator to use when joining.
*/
export function splitAmp(queryStr = '', separator = '\n') {
return queryStr.split('&').join(separator);
}