UNPKG

@decentra/enums

Version:

An enums helper written in JS 2022

412 lines (373 loc) 10.5 kB
import {inspect} from "node:util"; export class Enum { /** * @readonly * @public * @type {string} */ name; /** * @readonly * @public * @type {number|string} */ /** * @readonly * @public * @type {number} */ asNumber; /** * @readonly * @public * @type {string} */ asString; /** * @public * @readonly * @type {boolean} */ isNumber; /** * * @param {string} name * @param {?number|string} value */ constructor(name, value) { this.name = name; this.value = value; if (this.value === undefined) { this.value = this.name; } this.isNumber = typeof (this.value) === 'number' && !isNaN(this.value); this.asString = this.value.toString(); this.asNumber = this.isNumber ? this.value : NaN; } [Symbol.toStringTag]() { return this.name; } toString() { return this.name; } [Symbol.toPrimitive]() { return this.value; } valueOf() { return this.value; } } /** * @template {T extends Enum} T * @typedef {{ name:string, new (name: string, value?:(number|string)):T}} EnumConstructor<T> */ /** * @template {T extends Enum} T * @template {extends object} TValues */ export class EnumResolver { /** * @template {T extends Enum} T * @template TValues * @type {EnumerationValues<T, TValues>} */ #valuesBag; /** * @template T {T extends Enum} * @type {Map<string, T>} */ #byName = new Map(); /** * * @type {Map<(keyof TValues)|string, T>} */ #byKey = new Map(); /** * * @type {Map<(string|number), T>} */ #byValue = new Map(); /** * * @type {T[]|ReadonlyArray<T>} */ #valuesList = []; /** * @type {EnumConstructor<T>} */ #ctr; /** * @param {EnumConstructor<T>} ctr * @param {EnumerationValues<T, TValues>} valuesMap */ constructor(ctr,valuesMap) { this.#ctr = ctr; this.#valuesBag = valuesMap; for(let [key, entry] of Object.entries(valuesMap)) { this.#byName.set(entry.name, entry); this.#byValue.set(entry.value, entry); this.#byKey.set(key, entry); this.#valuesList.push(entry); } } /** * * @return {EnumerationValues<T, TValues>} */ get values() { return this.#valuesBag; } /** * * @param {string} name * @param {boolean} [optional] * @return {T} */ byName(name, optional= true) { let value = this.#byName.get(name); if (value === undefined && optional === false) { throw new Error(`Unknown enum name ${name}`); } return value; } /** * * @param {number} val * @param {boolean} [optional] * @return {T} **/ byNumericValue(val, optional = true) { let value = this.#byValue.get(val); if (value === undefined && optional === false) { throw new Error(`Unknown enum value ${val}`); } return value; } /** * * @param {string} nameOrValue * @param {boolean} [optional] * @return {T} **/ byNameOrValue(nameOrValue, optional) { let value = this.#byValue.get(nameOrValue); if (value === undefined) { value = this.#byName.get(nameOrValue); } if (value === undefined && optional === false) { throw new Error(`Unknown enum value ${value}`); } return value; } /** * * @param {keyof TValues|any} key * @param {boolean} [optional] */ byKey(key, optional = false) { let value = this.#byKey.get(key); if (value === undefined && optional === false) { throw new Error(`Unknown enum value ${value}`); } return value; } /** * * @param {T|keyof TValues|string|number|any|*} entryKeyNameOrValue * @param {?boolean} [optional] * @return {T} */ resolve(entryKeyNameOrValue, optional = false) { if (entryKeyNameOrValue instanceof this.#ctr) { if (this.#valuesList.includes(entryKeyNameOrValue)) { return entryKeyNameOrValue; } else { throw new Error(`Unknown ${this.#ctr.name || this.#ctr.constructor.name} value ${entryKeyNameOrValue}`); } } else { let value = this.#byValue.get(entryKeyNameOrValue); if (value === undefined) { value = this.#byKey.get(entryKeyNameOrValue); } if (value === undefined) { value = this.byName(entryKeyNameOrValue); } if (value === undefined && optional === false) { throw new Error(`Unknown reference ${entryKeyNameOrValue} to ${this.#ctr.name || this.#ctr.constructor.name} enum`) } return value; } } /** * * @return {ReadonlyArray<T>} */ enumerateValues() { return this.#valuesList; } } /** * @template {T extends Enum} T * @template TValues * @typedef {{ * [name in keyof TValues]:T * }} EnumerationValues<T, TValues> */ export const INCREMENT = Symbol('Increment value of enum'); /** * @template {T extends Enum} T * @template TValues * @callback EnumResolverFunc<T, TValues> * @param {(T|keyof TValues)|string|Symbol|number|any} key * @return {T} */ /** * @template {T extends Enum} T * @template TValues * @param {EnumConstructor<T>} ctr * @param values * @return {EnumerationValues<T, TValues>} */ function buildEnumValues(ctr, values) { /** * @template {T extends Enum} T * @template TValues * @type {EnumerationValues<T, TValues>} */ let resultValues = {}; let pos = 0; for(let [key, val] of Object.entries(values)) { /** * @template {T extends Enum} T * @type {T|ctr} */ let entry; switch (typeof val) { case "undefined": case "boolean": entry = new ctr(key, pos); break; case "symbol": if (val === INCREMENT) { entry = new ctr(key, pos); } else { entry = new ctr(key, String(val)); } break; case "number": case "string": entry = new ctr(key, val); break; case "bigint": entry = new ctr(key, Number(val)); break; case "function": continue; case "object": if (val instanceof Date) { entry = new ctr(key, val.toISOString()); } else if (Array.isArray(val)) { throw new Error(`Array values not supported for enums`); } else { if (val instanceof ctr) { entry = val; } else { throw new Error(`Unsupported value for ${ctr.name} enum ${inspect(val, false, 5, false)}`); } } } resultValues[key] = entry; pos++; } return resultValues; } /** * @template {T extends Enum} T * @template TValues * @param {ctr:EnumConstructor<T>} ctr * @param {TValues} values * @return {[EnumerationValues<T>, EnumResolverFunc<T, TValues>, ReadonlyArray<T>]} */ export function makeEnumeration(ctr, values) { let result = buildEnumValues(ctr, values); /** * @template {T extends Enum} T * @type {Map<string, T>} */ const byName = new Map(); /** * @template {T extends Enum} T * @type {Map<string, T>} */ const byProp = new Map(); /** * @template {T extends Enum} T * @type {Map<(string|number), T>} */ const byValue = new Map(); /** * @template {T extends Enum} T * @template TValues * @type {EnumerationValues<T, TValues>} */ let ret = {}; let pos = -1; /** * @template {T extends Enum} T * @type {T[]} */ let list = []; for(let [key, entry] of Object.entries(result)) { byProp.set(key, entry); byName.set(entry.name, entry); byValue.set(entry.value, entry); ret[key] = entry; list.push(entry); pos++; } /** * @template {T extends Enum} T * @template TValues * @callback * @param {keyof TValues||string|number|T|any} val * @return {T} */ let resolver = function (val) { if (typeof val === "object" && val instanceof ctr) { return val; } if (typeof val === 'string') { let ret = byName.get(val); if (ret === undefined) { ret = byValue.get(val); if (ret === undefined) { ret = byProp.get(val); } } if (ret === undefined || ret === null) { throw new Error(`Unknown ${val} name/value of ${ctr.name}`); } return ret; } if (typeof val === "number") { let ret = byValue.get(val); if (ret === undefined || ret === null) { throw new Error(`Unknown ${val} value of ${ctr.name}`); } return ret; } throw new Error(`Unknown ${String(val)} of ${ctr.name}`); } return [Object.freeze(result), resolver, Object.freeze(list)]; } /** * @template {T extends Enum} T * @template TValues * @param {ctr:EnumConstructor<T>} ctr * @param {TValues} values * @return {[EnumerationValues<T, TValues>, EnumResolver<T, TValues>]} */ export function produceEnumeration(ctr, values) { let enumValues = buildEnumValues(ctr, values); return [Object.freeze(enumValues), new EnumResolver(ctr, enumValues)]; }