@decentra/enums
Version:
An enums helper written in JS 2022
412 lines (373 loc) • 10.5 kB
JavaScript
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)];
}