// Licensed under the MIT License
// https://github.com/craigahobbs/schema-markdown-js/blob/main/LICENSE
/** @module lib/encode */
/**
* Encode an object as a query/hash string. Dictionaries, lists, and tuples are recursed. Each member key is expressed
* in fully-qualified form. List keys are the index into the list, and are in order.
*
* @param {Object} obj - The parameters object
* @returns {string}
*/
export function encodeQueryString(obj) {
return encodeQueryStringHelper(obj).join('&');
}
// Helper function for encodeQueryString
function encodeQueryStringHelper(obj, memberFqn = null, keyValues = []) {
const objType = typeof obj;
if (Array.isArray(obj)) {
if (obj.length === 0) {
keyValues.push(memberFqn !== null ? `${memberFqn}=` : '');
} else {
for (let ix = 0; ix < obj.length; ix++) {
encodeQueryStringHelper(obj[ix], memberFqn !== null ? `${memberFqn}.${ix}` : `${ix}`, keyValues);
}
}
} else if (obj instanceof Date) {
const objEncoded = encodeURIComponent(obj.toISOString());
keyValues.push(memberFqn !== null ? `${memberFqn}=${objEncoded}` : `${objEncoded}`);
} else if (obj !== null && typeof obj === 'object') {
const keys = Object.keys(obj).sort();
if (keys.length === 0) {
keyValues.push(memberFqn !== null ? `${memberFqn}=` : '');
} else {
for (const key of keys) {
const keyEncoded = encodeQueryStringHelper(key);
encodeQueryStringHelper(obj[key], memberFqn !== null ? `${memberFqn}.${keyEncoded}` : `${keyEncoded}`, keyValues);
}
}
} else if (obj === null || objType === 'boolean' || objType === 'number') {
keyValues.push(memberFqn !== null ? `${memberFqn}=${obj}` : `${obj}`);
} else {
const objEncoded = encodeURIComponent(obj);
keyValues.push(memberFqn !== null ? `${memberFqn}=${objEncoded}` : `${objEncoded}`);
}
return keyValues;
}
/**
* Decode an object from a query/hash string. Each member key of the query string is expressed in fully-qualified
* form. List keys are the index into the list, must be in order.
*
* @param {string} paramStr - The parameters string
* @returns {Object}
*/
export function decodeQueryString(paramStr) {
// Decode the parameter string key/values
const result = [null];
const keyValues = paramStr.split('&');
keyValues.forEach((keyValue, ixKeyValue) => {
const [keyFqn, valueEncoded = null] = keyValue.split('=', 2);
if (valueEncoded === null) {
// Ignore hash IDs
if (ixKeyValue === keyValues.length - 1) {
return;
}
throw new Error(`Invalid key/value pair '${keyValue.slice(0, 100)}'`);
}
const value = decodeURIComponent(valueEncoded);
// Find/create the object on which to set the value
let parent = result;
let keyParent = 0;
const keys = keyFqn.split('.');
for (const keyEncoded of keys) {
let key = decodeURIComponent(keyEncoded);
let obj = parent[keyParent];
// Array key? First "key" of an array must start with "0".
if (Array.isArray(obj) || (obj === null && key === '0')) {
// Create this key's container, if necessary
if (obj === null) {
obj = [];
parent[keyParent] = obj;
}
// Parse the key as an integer
if (isNaN(key)) {
throw new Error(`Invalid array index '${key.slice(0, 100)}' in key '${keyFqn.slice(0, 100)}'`);
}
const keyOrig = key;
key = parseInt(key, 10);
// Append the value placeholder null
if (key === obj.length) {
obj.push(null);
} else if (key < 0 || key > obj.length) {
throw new Error(`Invalid array index '${keyOrig.slice(0, 100)}' in key '${keyFqn.slice(0, 100)}'`);
}
} else {
// Create this key's container, if necessary
if (obj === null) {
obj = {};
parent[keyParent] = obj;
}
// Create the index for this key
if (!(key in obj)) {
obj[key] = null;
}
}
// Update the parent object and key
parent = obj;
keyParent = key;
}
// Set the value
if (parent[keyParent] !== null) {
throw new Error(`Duplicate key '${keyFqn.slice(0, 100)}'`);
}
parent[keyParent] = value;
});
return result[0] !== null ? result[0] : {};
}
/**
* JSON-stringify an object with sorted keys
*
* @param {Object} obj - The object to stringify
* @returns {string}
*/
export function jsonStringifySortKeys(value, space) {
// JSON-stringify non-objects without creating a key set
if (value === null || typeof value !== 'object') {
return JSON.stringify(value, null, space);
}
// JSON-stringify with sorted keys
const keySet = new Set();
getObjectKeys(value, keySet);
const sortedKeys = Array.from(keySet.values()).sort();
return JSON.stringify(value, sortedKeys, space);
}
// Helper function to get an object keys (deep)
function getObjectKeys(value, keySet) {
if (value !== null && typeof value === 'object') {
if (Array.isArray(value)) {
for (const subValue of value) {
getObjectKeys(subValue, keySet);
}
} else {
for (const [subKey, subValue] of Object.entries(value)) {
keySet.add(subKey);
getObjectKeys(subValue, keySet);
}
}
}
}