Skip to content

Instantly share code, notes, and snippets.

@robertvanhoesel
Created June 7, 2022 16:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save robertvanhoesel/059ab8792d30b328418a5743cc43443b to your computer and use it in GitHub Desktop.
Save robertvanhoesel/059ab8792d30b328418a5743cc43443b to your computer and use it in GitHub Desktop.
Format and parse objects in a single url query param
export type JsonParamPrimitiveValue = string | number | boolean | null;
export type JsonParamObject = Record<string, JsonParamPrimitiveValue | JsonParamPrimitiveValue[]>;
export type JsonParamValue = JsonParamPrimitiveValue | JsonParamValue[] | JsonParamObject;
export type JsonParam = Record<string, JsonParamValue>;
function formatValue(value: JsonParamValue): string {
if (typeof value == 'number') return `${value}`;
else if (value === null) return 'null';
else if (typeof value == 'boolean' && value) return undefined;
else if (typeof value == 'boolean') return 'false';
else if (value === 'true' || value === 'false' || !isNaN(+value)) return `'${formatString(value as string)}'`;
else if (Array.isArray(value)) return wrap(formatArray(value));
else if (typeof value == 'object') return wrap(formatObject(value));
else return formatString(value);
}
function parseValue(value: string | undefined, depth: number): JsonParamValue | JsonParam {
if (value == undefined) return true;
else if (value.startsWith('(') && value.endsWith(')'))
return (value.includes(':') ? parseObject : parseArray)(value.slice(1, -1), depth);
else if (value == 'null') return null;
else if (value === 'true') return true;
else if (value === 'false') return false;
else if (value.match(/^-?\d+\.?\d+$/)) return +value;
else return parseString(value);
}
const formatString = (str: string) => encodeURIComponent(str).replace(/\(/g, '%28').replace(/\)/g, '%29').replace(`'`, '%27');
const parseString = (raw: string): string => decodeURIComponent(raw.startsWith(`'`) && raw.endsWith(`'`) ? raw.slice(1, -1) : raw);
const formatArray = (array: JsonParamValue[]) => array.map(formatValue).join(',');
const parseArray = (raw: string) => raw.split(',').map(parseValue) as JsonParamValue[];
const formatObject = (object: JsonParam) =>
Object.entries(object)
.map(([key, value]) => {
const formatted = formatValue(value);
if (formatted === undefined) return key;
else return `${key}:${formatted}`;
})
.join(',');
const parseObject = (raw: string, depth = 0) => {
const initialDepth = depth;
const object: Record<string, JsonParamValue> = {};
let substringbegin = 0;
for (const [i, ch] of [...raw].entries()) {
const end = i === raw.length - 1;
if (ch === '(') depth++;
if (ch === ')') depth--;
if ((ch === ',' || end) && depth == initialDepth) {
const substr = raw.slice(substringbegin, end ? undefined : i);
const [key, value] = parseKeyValue(substr, initialDepth);
object[key] = value;
substringbegin = i + 1;
}
if (depth < 0) throw new Error(`Unexpected closing parantheses at pos ${i} in ${raw}`);
if (end && depth > 0) throw new Error(`Unexpected end of string at pos ${i} in ${raw}, expected closing parantheses`);
}
return object;
};
function parseKeyValue(raw: string, depth: number): [string, any] {
const separator = raw.indexOf(':');
if (separator == -1) return [raw, parseValue(undefined, depth)];
const key = raw.slice(0, separator);
const value = raw.slice(separator + 1);
return [key, parseValue(value, depth)];
}
const wrap = (value: string) => `(${value})`;
export const jsonParam = {
format: formatObject,
parse: parseObject,
formatValue,
parseValue,
formatObject,
parseObject,
};
export const formatJsonParam = formatObject;
export const parseJsonParam = parseObject;
export default jsonParam;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment