util_propMap.js

import Pagination from "../domain/pagination.js";
import SortDescriptor from "../domain/sortDescriptor.js";
import Enum from "./enum.js";

/**
 * A basic map-like object.
 *
 * <p>This object includes some utility functions that make it well suited to using
 * as an API query object. For example, the {@link module:util~PropMap#toUriEncoding}
 * method provides a way to serialize this object into URL query parameters.</p>
 *
 * @alias module:util~PropMap
 */
class PropMap {
	/**
	 * Constructor.
	 * @param {PropMap|object} props the initial properties; if a `PropMap` instance is provided, the properties
	 *                               of that object will be copied into this one; otherwise the object will be
	 *                               used directly to hold property values
	 */
	constructor(props) {
		/**
		 * The object that all properties are stored on.
		 * @member {object}
		 */
		this.props =
			props instanceof PropMap ? props.properties() : typeof props === "object" ? props : {};
	}

	/**
	 * Get, set, or remove a property value.
	 *
	 * @param {string} key the key to get or set the value for
	 * @param {*} [newValue] if defined, the new value to set for the given `key`;
	 *                       if `null` then the `key` property will be removed
	 * @returns {*} if called as a getter, the associated value for the given `key`,
	 *              otherwise this object
	 */
	prop(key, newValue) {
		if (arguments.length === 1) {
			return this.props[key];
		}
		if (newValue === null) {
			delete this.props[key];
		} else {
			this.props[key] = newValue;
		}
		return this;
	}

	/**
	 * Get, set, or remove multiple properties.
	 *
	 * @param {object} [newProps] the new values to set; if any value is `null` that property
	 *                            will be deleted
	 * @returns {object} if called as a getter, all properties of this object copied into a
	 *                   simple object; otherwise this object
	 */
	properties(newProps) {
		if (newProps) {
			for (const k of Object.keys(newProps)) {
				this.prop(k, newProps[k]);
			}
			return this;
		}
		return Object.assign({}, this.props);
	}

	/**
	 * Get this object as a standard URI encoded (query parameters) string value.
	 *
	 * All enumerable properties of the <code>props</code> property will be added to the
	 * result. If any property value is an array, the values of the array will be joined
	 * by a comma. Any {@link module:util~Enum} values will have their `name` property used.
	 * Any value that has a `toUriEncoding()` function property will have that function
	 * invoked, passing the associated property name as the first argument, and the returned
	 * value will be used.
	 *
	 * @param {string} [propertyName] an optional object property prefix to add to all properties
	 * @param {function} [callbackFn] An optional function that will be called for each property.
	 *                   The function will be passed property name and value arguments, and must
	 *                   return either `null` to skip the property, a 2 or 3-element array with the
	 *                   property name and value to use, and an optional boolean to force array
	 *                   values to use mutliple parameter keys. Any other return value causes the
	 *                   property to be used as- is.
	 * @return {string} the URI encoded string
	 */
	toUriEncoding(propertyName, callbackFn) {
		let result = "";
		for (let k of Object.keys(this.props)) {
			if (result.length > 0) {
				result += "&";
			}
			let v = this.props[k];
			let forceMultiKey = false;
			if (callbackFn) {
				const kv = callbackFn(k, v);
				if (kv === null) {
					continue;
				} else if (Array.isArray(kv) && kv.length > 1) {
					k = kv[0];
					v = kv[1];
					if (kv.length > 2) {
						forceMultiKey = !!kv[2];
					}
				}
			}

			if (typeof v.toUriEncoding === "function") {
				result += v.toUriEncoding(
					propertyName ? encodeURIComponent(propertyName) + "." + k : k,
				);
				continue;
			}

			if (propertyName) {
				result += encodeURIComponent(propertyName) + ".";
			}
			result += encodeURIComponent(k) + "=";
			if (Array.isArray(v)) {
				v.forEach(function (e, i) {
					if (i > 0) {
						result += forceMultiKey ? "&" + encodeURIComponent(k) + "=" : ",";
					}
					if (e instanceof Enum) {
						e = e.name;
					}
					result += encodeURIComponent(e);
				});
			} else {
				if (v instanceof Enum) {
					v = v.name;
				}
				result += encodeURIComponent(v);
			}
		}
		return result;
	}

	/**
	 * Get this object as a standard URI encoded (query parameters) string value with
	 * sorting and pagination parameters.
	 *
	 * <p>This calls {@link module:util~PropMap#toUriEncoding} first, then encodes
	 * the `sorts` and `pagination` parameters, if provided.
	 *
	 * @param {module:domain~SortDescriptor[]} [sorts] optional sort settings to use
	 * @param {module:domain~Pagination} [pagination] optional pagination settings to use
	 * @param {string} [propertyName] an optional object property prefix to add to all properties
	 * @param {function} [callbackFn] An optional function that will be called for each property.
	 *                   The function will be passed property name and value arguments, and must
	 *                   return either `null` to skip the property, a 2-element array with the property
	 *                   name and value to use, or anything else to use the property as- is.
	 * @return {string} the URI encoded string
	 */
	toUriEncodingWithSorting(sorts, pagination, propertyName, callbackFn) {
		let params = this.toUriEncoding(propertyName, callbackFn);
		if (Array.isArray(sorts)) {
			sorts.forEach((sort, i) => {
				if (sort instanceof SortDescriptor) {
					if (params.length > 0) {
						params += "&";
					}
					params += sort.toUriEncoding(i);
				}
			});
		}
		if (pagination instanceof Pagination) {
			const paginationParams = pagination.toUriEncoding();
			if (paginationParams) {
				if (params.length > 0) {
					params += "&";
				}
				params += paginationParams;
			}
		}
		return params;
	}
}

export default PropMap;