Implemented DynamicEnum

This commit is contained in:
Kir_Antipov 2023-01-21 13:47:56 +00:00
parent 8e382c9ed5
commit 5fe6c0ba36
2 changed files with 762 additions and 0 deletions

View file

@ -0,0 +1,446 @@
import { $i } from "@/utils/collections";
import { EqualityComparer, ORDINAL_EQUALITY_COMPARER } from "@/utils/comparison";
import { toType } from "@/utils/convert";
import { split, toPascalCase } from "@/utils/string-utils";
import { TypeOf } from "@/utils/types";
import { EnumDescriptor, inferEnumDescriptorOrThrow } from "./descriptors";
import { EnumEntry, enumEntries } from "./enum-entry";
import { EnumKey } from "./enum-key";
import { DEFAULT_ENUM_SEPARATOR, ENUM_SEPARATORS } from "./enum-separators";
import { EnumValue } from "./enum-value";
/**
* Options for constructing a dynamic enum.
*/
export interface DynamicEnumOptions {
/**
* Specifies whether the enum should be treated as a set of flags.
*/
hasFlags?: boolean;
/**
* The equality comparer used to compare enum keys.
*/
comparer?: EqualityComparer<string>;
/**
* An iterable of tuples containing enum keys and their corresponding display names.
*
* The display names can be used for more human-readable representations of the enum keys.
*/
names?: Iterable<readonly [string, string]>;
}
/**
* A dynamic enum implementation that allows you to create an enum at runtime.
*
* @template T - The type of the enum.
*/
export class DynamicEnum<T> implements ReadonlyMap<EnumKey<T>, EnumValue<T>> {
/**
* An array of enum keys.
*/
private readonly _keys: readonly EnumKey<T>[];
/**
* An array of enum values.
*/
private readonly _values: readonly EnumValue<T>[];
/**
* A map containing the enum keys and their corresponding display names.
*/
private readonly _names: ReadonlyMap<string, string>;
/**
* The enum descriptor.
*/
private readonly _descriptor: EnumDescriptor<EnumValue<T>>;
/**
* A boolean indicating whether the enum should be treated as a set of flags.
*/
private readonly _hasFlags: boolean;
/**
* The equality comparer used to compare enum keys.
*/
private readonly _comparer: EqualityComparer<string>;
/**
* Constructs a new {@link DynamicEnum} instance.
*
* @param entries - An array of key-value pairs representing the entries of the enum.
* @param options - An object containing options for the `DynamicEnum` instance, such as whether the enum is a flags enum.
*/
private constructor(entries: readonly EnumEntry<T>[], options?: DynamicEnumOptions) {
this._keys = entries.map(([key]) => key);
this._values = entries.map(([, value]) => value);
this._names = new Map(options?.names || []);
this._descriptor = inferEnumDescriptorOrThrow(this._values);
this._hasFlags = options?.hasFlags ?? false;
this._comparer = options?.comparer || ORDINAL_EQUALITY_COMPARER;
const properties = $i(entries).map(([key, value]) => [key, { value, enumerable: true }] as const).toRecord();
Object.defineProperties(this, properties);
}
/**
* Creates a dynamic enum from an existing enum object.
*
* @param underlyingEnum - The underlying enum object.
* @param options - The options to use when creating the new enum.
*
* @returns A new dynamic enum.
*/
static create<TEnum>(underlyingEnum: TEnum, options?: DynamicEnumOptions): ConstructedEnum<TEnum> {
const entries = enumEntries(underlyingEnum);
return new DynamicEnum(entries, options) as ConstructedEnum<TEnum>;
}
/**
* Returns a string representation of this object.
*/
get [Symbol.toStringTag](): string {
return "Enum";
}
/**
* The number of values in the enum.
*/
get size(): number {
return this._keys.length;
}
/**
* The underlying type of the enum.
*/
get underlyingType(): TypeOf<EnumValue<T>> {
return this._descriptor.name;
}
/**
* Determines whether the given `value` contains the specified `flag`.
*
* @param value - The value to check for the presence of the flag.
* @param flag - The flag to check for.
*
* @returns `true` if the value has the flag; otherwise, `false`.
*/
hasFlag(value: EnumValue<T>, flag: EnumValue<T>): boolean {
return this._descriptor.hasFlag(value, flag);
}
/**
* Gets the enum value associated with the specified key.
*
* @param key - The key to look up.
*
* @returns The enum value associated with the key, or `undefined` if the key is not found.
*/
get(key: EnumKey<T> | string): EnumValue<T> | undefined {
// Attempt to retrieve the value from this object's properties.
const value = (this as unknown as T)[key as EnumKey<T>];
if (typeof value === this.underlyingType || this._comparer === ORDINAL_EQUALITY_COMPARER) {
return value;
}
// Apply the custom comparer.
const comparer = this._comparer;
const keys = this._keys;
const values = this._values;
for (let i = 0; i < keys.length; ++i) {
if (comparer(key, keys[i])) {
return values[i];
}
}
// Nothing we can do about it.
return undefined;
}
/**
* Returns the key of the first occurrence of a value in the enum.
*
* @param value - The value to locate in the enum.
*
* @returns The key of the first occurrence of a value in the enum, or `undefined` if it is not present.
*/
keyOf(value: EnumValue<T>): EnumKey<T> | undefined {
const i = this._values.indexOf(value);
return i >= 0 ? this._keys[i] : undefined;
}
/**
* Returns the friendly name of the key of the first occurrence of a value in the enum.
*
* @param value - The value to locate in the enum.
*
* @returns The friendly name of the key of the first occurrence of a value in the enum, or `undefined` if it is not present.
*/
friendlyNameOf(value: EnumValue<T>): string | undefined {
const key = this.keyOf(value);
if (key === undefined) {
return undefined;
}
const friendlyName = this._names.get(key) ?? toPascalCase(key);
return friendlyName;
}
/**
* Returns the first element in the enum that satisfies the provided `predicate`.
*
* @param predicate - A function to test each key/value pair in the enum. It should return `true` to indicate a match; otherwise, `false`.
* @param thisArg - An optional object to use as `this` when executing the `predicate`.
*
* @returns The first element in the enum that satisfies the provided `predicate`, or `undefined` if no value satisfies the function.
*/
find(predicate: (value: EnumValue<T>, key: EnumKey<T>, e: ConstructedEnum<T>) => boolean, thisArg?: unknown): EnumValue<T> | undefined {
const key = this.findKey(predicate, thisArg);
return key === undefined ? undefined : this.get(key);
}
/**
* Returns the key for the first element in the enum that satisfies the provided `predicate`.
*
* @param predicate - A function to test each key/value pair in the enum. It should return `true` to indicate a match; otherwise, `false`.
* @param thisArg - An optional object to use as `this` when executing the `predicate`.
*
* @returns The key of the first element in the enum that satisfies the provided `predicate`, or `undefined` if no key satisfies the function.
*/
findKey(predicate: (value: EnumValue<T>, key: EnumKey<T>, e: ConstructedEnum<T>) => boolean, thisArg?: unknown): EnumKey<T> | undefined {
predicate = thisArg === undefined ? predicate : predicate.bind(thisArg);
const keys = this._keys;
const values = this._values;
for (let i = 0; i < values.length; ++i) {
if (predicate(values[i], keys[i], this as unknown as ConstructedEnum<T>)) {
return keys[i];
}
}
return undefined;
}
/**
* Checks whether the specified key exists in the enum.
*
* @param key - The key to check.
*
* @returns `true` if the key exists in the enum; otherwise, `false`.
*/
has(key: EnumKey<T> | string): boolean {
return this.get(key) !== undefined;
}
/**
* Checks whether the specified value exists in the enum.
*
* @param value - The enum value to check.
*
* @returns `true` if the enum value exists in the enum; otherwise, `false`.
*/
includes(value: EnumValue<T>): boolean {
return this._values.includes(value);
}
/**
* Returns an iterator that yields the keys of the enum.
*/
keys(): IterableIterator<EnumKey<T>> {
return this._keys[Symbol.iterator]();
}
/**
* Returns an iterator that yields the values of the enum.
*/
values(): IterableIterator<EnumValue<T>> {
return this._values[Symbol.iterator]();
}
/**
* Returns an iterator that yields the key/value pairs for every entry in the enum.
*/
*entries(): IterableIterator<EnumEntry<T>> {
const keys = this._keys;
const values = this._values;
for (let i = 0; i < keys.length; ++i) {
yield [keys[i], values[i]];
}
}
/**
* Returns an iterator that yields the key/value pairs for every entry in the enum.
*/
[Symbol.iterator](): IterableIterator<EnumEntry<T>> {
return this.entries();
}
/**
* Executes a provided function once per each key/value pair in the enum, in definition order.
*
* @param callbackFn - The function to call for each element in the enum.
* @param thisArg - The value to use as `this` when calling `callbackFn`.
*/
forEach(callbackFn: (value: EnumValue<T>, key: EnumKey<T>, e: ConstructedEnum<T>) => void, thisArg?: unknown): void {
callbackFn = thisArg === undefined ? callbackFn : callbackFn.bind(thisArg);
const keys = this._keys;
const values = this._values;
for (let i = 0; i < keys.length; ++i) {
callbackFn(values[i], keys[i], this as unknown as ConstructedEnum<T>);
}
}
/**
* Formats the given value as a string.
*
* @param value - The value to format.
*
* @returns The formatted string, or `undefined` if the value does not belong to the enum.
*/
format(value: EnumValue<T>): string | undefined {
// Unsupported value cannot be formatted.
if (typeof value !== this.underlyingType) {
return undefined;
}
// Attempt to find an existing key for the provided value.
const existingKey = this.keyOf(value);
if (existingKey !== undefined) {
return existingKey;
}
// In case values in this enum are not flags,
// and we did not find a key for the `value` during the previous step,
// just return its string representation.
//
// Note: we don't return `undefine` or throw error,
// because the `value` has the same type as other enum members.
// E.g., `42` is considered a valid value for any number enum,
// even if it was not directly specified.
if (!this._hasFlags) {
return String(value);
}
// Retrieve the keys, values, and descriptor,
// so we won't need to directly access them every time it's necessary.
const keys = this._keys;
const values = this._values;
const descriptor = this._descriptor;
// Prepare for generating the string representation.
let name = "";
let remainingValue = value;
// Iterate over each flag value in reverse order.
// (because the flags with higher values are likely to be
// more significant than the flags with lower values)
for (let i = values.length - 1; i >= 0; --i) {
const flag = values[i];
// If the current flag is not present in the remaining value,
// or is the default value (e.g., `0` for number enums), skip to the next flag.
const isZero = flag === descriptor.defaultValue;
const isFlagPresent = descriptor.hasFlag(remainingValue, flag);
if (isZero || !isFlagPresent) {
continue;
}
// If this is not the first flag to be added to the name, add a separator to the current name.
name = name ? `${keys[i]}${DEFAULT_ENUM_SEPARATOR} ${name}` : keys[i];
// Remove the current flag from the remaining value to ensure that
// we won't add aliases of the same value to the result string.
remainingValue = descriptor.removeFlag(remainingValue, flag);
}
// If the remaining value is equal to the default value for the descriptor
// (e.g., `0` for number enums), return the generated name.
//
// Otherwise, it means there were some flags, which aren't specified in the enum,
// so just return the string representation of the provided value.
return remainingValue === descriptor.defaultValue && name ? name : String(value);
}
/**
* Parses the specified string and returns the corresponding enum value.
*
* @param key - The string to parse.
*
* @returns The corresponding enum value, or `undefined` if the string could not be parsed.
*/
parse(key: string): EnumValue<T> {
// Attempt to find an existing value for the provided key.
const existingValue = this.findOrParseValue(key);
if (existingValue !== undefined) {
return existingValue;
}
// In case values in this enum are not flags,
// and we did not find a value for the `key` during the previous step,
// return `undefined`, since the key is not valid for this enum.
if (!this._hasFlags) {
return undefined;
}
// Otherwise, we need to parse the key into individual flags and combine them into a single value.
const formattedFlags = split(key, ENUM_SEPARATORS, { trimEntries: true, removeEmptyEntries: true });
const descriptor = this._descriptor;
// Start with the default value for the enum.
let result = descriptor.defaultValue;
for (const formattedFlag of formattedFlags) {
// Try to find the value for the current string representation of flag.
const flag = this.findOrParseValue(formattedFlag);
// If the value is not found, return `undefined`.
// In this case a single failure makes the whole input invalid.
if (flag === undefined) {
return undefined;
}
// Otherwise, combine it with the result.
result = descriptor.addFlag(result, flag);
}
// Return the final combined value.
return result;
}
/**
* Finds the enum value for the given key.
*
* @param key - The key of the enum value to find.
*
* @returns The enum value with the given key, or `undefined` if no element with that key exists.
*/
private findOrParseValue(key: string): EnumValue<T> | undefined {
// If the value was found, return it as is.
const value = this.get(key as EnumKey<T>);
if (value !== undefined) {
return value;
}
// If the key couldn't be found in the enumeration, try to parse it as a value.
// E.g., `42` is considered a valid value for any number enum,
// even if it was not directly specified.
const keyAsValue = toType(key, this.underlyingType) as unknown as EnumValue<T>;
if (keyAsValue !== undefined) {
return keyAsValue;
}
// If the key couldn't be found in the enum and it couldn't be parsed as a value,
// there's not much we can do about it, so just return `undefined`.
return undefined;
}
}
/**
* A type of the constructed enum, which has all the methods and properties of `DynamicEnum`
* and all the entries of the underlying enum `TEnum`.
*
* @template TEnum - Type of underlying enum used to construct the `DynamicEnum` instance.
*/
export type ConstructedEnum<TEnum> = DynamicEnum<TEnum> & Readonly<TEnum>;

View file

@ -0,0 +1,316 @@
import { IGNORE_CASE_EQUALITY_COMPARER } from "@/utils/comparison/string-equality-comparer";
import { DynamicEnum } from "@/utils/enum/dynamic-enum";
describe("DynamicEnum", () => {
enum TestEnum {
FOO = 1,
BAR = 2,
BAZ = 4,
QUX = 8,
}
describe("create", () => {
test("creates a dynamic enum based on the given enum value container", () => {
const e = DynamicEnum.create(TestEnum);
expect(e).toBeInstanceOf(DynamicEnum);
expect(e.FOO).toBe(TestEnum.FOO);
expect(e.BAR).toBe(TestEnum.BAR);
expect(e.BAZ).toBe(TestEnum.BAZ);
expect(e.QUX).toBe(TestEnum.QUX);
});
test("handles enums without flags", () => {
const e = DynamicEnum.create({ A: "a", B: "b" }, { hasFlags: false });
expect(e.hasFlag("a", "b")).toBe(false);
});
test("handles enums with flags", () => {
const e = DynamicEnum.create(TestEnum, { hasFlags: true });
expect(e.hasFlag(TestEnum.BAR | TestEnum.BAZ, TestEnum.BAZ)).toBe(true);
});
test("handles enums with a custom comparer", () => {
const e = DynamicEnum.create(TestEnum, { comparer: IGNORE_CASE_EQUALITY_COMPARER, hasFlags: true });
expect(e.parse("FOO")).toBe(TestEnum.FOO);
expect(e.parse("Foo")).toBe(TestEnum.FOO);
expect(e.parse("foo")).toBe(TestEnum.FOO);
expect(e.parse("foo, baz")).toBe(TestEnum.FOO | TestEnum.BAZ);
expect(e.parse("FOO, baz")).toBe(TestEnum.FOO | TestEnum.BAZ);
expect(e.parse("foo, Baz")).toBe(TestEnum.FOO | TestEnum.BAZ);
expect(e.parse("foo | Baz")).toBe(TestEnum.FOO | TestEnum.BAZ);
expect(e.parse("foo|Baz")).toBe(TestEnum.FOO | TestEnum.BAZ);
});
test("handles enums with custom display names", () => {
const e = DynamicEnum.create(TestEnum, { names: [["FOO", "1"]] });
expect(e.friendlyNameOf(TestEnum.FOO)).toBe("1");
});
});
describe("size", () => {
test("returns the correct size of the enum", () => {
expect(DynamicEnum.create({}).size).toBe(0);
expect(DynamicEnum.create({ A: "a" }).size).toBe(1);
expect(DynamicEnum.create({ A: "a", B: "b" }).size).toBe(2);
expect(DynamicEnum.create(TestEnum).size).toBe(4);
});
});
describe("underlyingType", () => {
test("returns the correct underlying type of the enum", () => {
expect(DynamicEnum.create({ A: "a" }).underlyingType).toBe("string");
expect(DynamicEnum.create({ A: 1n }).underlyingType).toBe("bigint");
expect(DynamicEnum.create({ TRUE: true }).underlyingType).toBe("boolean");
expect(DynamicEnum.create(TestEnum).underlyingType).toBe("number");
});
test("returns 'number' if the enum is empty", () => {
expect(DynamicEnum.create({}).underlyingType).toBe("number");
});
});
describe("hasFlag", () => {
test("returns true if a flag is set", () => {
const e = DynamicEnum.create(TestEnum, { hasFlags: true });
expect(e.hasFlag(TestEnum.BAR, TestEnum.BAR)).toBe(true);
expect(e.hasFlag(TestEnum.BAR | TestEnum.QUX, TestEnum.QUX)).toBe(true);
expect(e.hasFlag(TestEnum.BAR | TestEnum.QUX | TestEnum.FOO, TestEnum.FOO)).toBe(true);
});
test("returns false if a flag is not set", () => {
const e = DynamicEnum.create(TestEnum);
expect(e.hasFlag(TestEnum.BAR, TestEnum.QUX)).toBe(false);
expect(e.hasFlag(TestEnum.BAR | TestEnum.QUX, TestEnum.FOO)).toBe(false);
expect(e.hasFlag(TestEnum.BAR | TestEnum.QUX | TestEnum.FOO, TestEnum.BAZ)).toBe(false);
});
});
describe("get", () => {
test("retrieves the correct enum value", () => {
expect(DynamicEnum.create(TestEnum).get("FOO")).toBe(TestEnum.FOO);
});
test("returns undefined if the given key does not exist in the enum", () => {
expect(DynamicEnum.create(TestEnum).get("Foo")).toBeUndefined();
expect(DynamicEnum.create({}).get("A")).toBeUndefined();
});
});
describe("keyOf", () => {
test("retrieves the correct enum key for the value", () => {
expect(DynamicEnum.create(TestEnum).keyOf(TestEnum.FOO)).toBe("FOO");
});
test("returns undefined if the given value does not exist in the enum", () => {
expect(DynamicEnum.create(TestEnum).keyOf(16 as TestEnum)).toBeUndefined();
expect(DynamicEnum.create({}).keyOf("A" as never)).toBeUndefined();
});
});
describe("friendlyNameOf", () => {
test("retrieves the friendly name of a value", () => {
expect(DynamicEnum.create(TestEnum).friendlyNameOf(TestEnum.FOO)).toBe("Foo");
});
test("returns undefined if the given value does not exist in the enum", () => {
expect(DynamicEnum.create(TestEnum).friendlyNameOf(16 as TestEnum)).toBeUndefined();
expect(DynamicEnum.create({}).friendlyNameOf("A" as never)).toBeUndefined();
});
});
describe("find", () => {
test("finds the first value that satisfies the provided testing function", () => {
const predicate = jest.fn().mockImplementation(x => x === TestEnum.QUX);
const e = DynamicEnum.create(TestEnum);
expect(e.find(predicate)).toBe(TestEnum.QUX);
expect(predicate).toHaveBeenCalledTimes(4);
expect(predicate).toHaveBeenNthCalledWith(1, TestEnum.FOO, "FOO", e);
expect(predicate).toHaveBeenNthCalledWith(2, TestEnum.BAR, "BAR", e);
expect(predicate).toHaveBeenNthCalledWith(3, TestEnum.BAZ, "BAZ", e);
expect(predicate).toHaveBeenNthCalledWith(4, TestEnum.QUX, "QUX", e);
});
test("returns undefined if no value satisfies the given predicate", () => {
expect(DynamicEnum.create({}).find(() => false)).toBeUndefined();
});
});
describe("findKey", () => {
test("finds the first key that satisfies the provided testing function", () => {
const predicate = jest.fn().mockImplementation(x => x === TestEnum.QUX);
const e = DynamicEnum.create(TestEnum);
expect(e.findKey(predicate)).toBe("QUX");
expect(predicate).toHaveBeenCalledTimes(4);
expect(predicate).toHaveBeenNthCalledWith(1, TestEnum.FOO, "FOO", e);
expect(predicate).toHaveBeenNthCalledWith(2, TestEnum.BAR, "BAR", e);
expect(predicate).toHaveBeenNthCalledWith(3, TestEnum.BAZ, "BAZ", e);
expect(predicate).toHaveBeenNthCalledWith(4, TestEnum.QUX, "QUX", e);
});
test("returns undefined if no key satisfies the given predicate", () => {
expect(DynamicEnum.create({}).findKey(() => false)).toBeUndefined();
});
});
describe("has", () => {
test("returns true if a key exists in the enum", () => {
expect(DynamicEnum.create(TestEnum).has("FOO")).toBe(true);
});
test("returns false if a key does not exist in the enum", () => {
expect(DynamicEnum.create({}).has("A")).toBe(false);
});
});
describe("includes", () => {
test("returns true if a value exists in the enum", () => {
expect(DynamicEnum.create(TestEnum).includes(TestEnum.QUX)).toBe(true);
});
test("returns false if a value does not exist in the enum", () => {
expect(DynamicEnum.create({}).includes(1 as never)).toBe(false);
});
});
describe("keys", () => {
test("returns an iterable of keys in the enum", () => {
const e = DynamicEnum.create(TestEnum);
const keys = Array.from(e.keys());
expect(keys).toEqual(["FOO", "BAR", "BAZ", "QUX"]);
});
test("returns an empty iterable if the enum is empty", () => {
expect(Array.from(DynamicEnum.create({}).keys())).toEqual([]);
});
});
describe("values", () => {
test("returns an iterable of values in the enum", () => {
const e = DynamicEnum.create(TestEnum);
const values = Array.from(e.values());
expect(values).toEqual([TestEnum.FOO, TestEnum.BAR, TestEnum.BAZ, TestEnum.QUX]);
});
test("returns an empty iterable if the enum is empty", () => {
expect(Array.from(DynamicEnum.create({}).values())).toEqual([]);
});
});
describe("entries", () => {
test("returns an iterable of entries in the enum", () => {
const e = DynamicEnum.create(TestEnum);
const entries = Array.from(e.entries());
expect(entries).toEqual([["FOO", TestEnum.FOO], ["BAR", TestEnum.BAR], ["BAZ", TestEnum.BAZ], ["QUX", TestEnum.QUX]]);
});
test("returns an empty iterable if the enum is empty", () => {
expect(Array.from(DynamicEnum.create({}).entries())).toEqual([]);
});
});
describe("[Symbol.iterator]", () => {
test("returns an iterable of entries in the enum", () => {
const e = DynamicEnum.create(TestEnum);
const entries = Array.from(e.entries());
expect(entries).toEqual([["FOO", TestEnum.FOO], ["BAR", TestEnum.BAR], ["BAZ", TestEnum.BAZ], ["QUX", TestEnum.QUX]]);
});
test("returns an empty iterable if the enum is empty", () => {
expect(Array.from(DynamicEnum.create({}).entries())).toEqual([]);
});
});
describe("[Symbol.toStringTag]", () => {
test("returns 'Enum'", () => {
expect(DynamicEnum.create(TestEnum)[Symbol.toStringTag]).toBe("Enum");
expect(DynamicEnum.create({})[Symbol.toStringTag]).toBe("Enum");
});
});
describe("forEach", () => {
test("executes a provided function once for each enum entry", () => {
const callback = jest.fn();
const e = DynamicEnum.create(TestEnum);
e.forEach(callback);
expect(callback).toHaveBeenCalledTimes(4);
expect(callback).toHaveBeenNthCalledWith(1, TestEnum.FOO, "FOO", e);
expect(callback).toHaveBeenNthCalledWith(2, TestEnum.BAR, "BAR", e);
expect(callback).toHaveBeenNthCalledWith(3, TestEnum.BAZ, "BAZ", e);
expect(callback).toHaveBeenNthCalledWith(4, TestEnum.QUX, "QUX", e);
});
});
describe("format", () => {
test("formats a single enum value correctly", () => {
const e = DynamicEnum.create(TestEnum, { hasFlags: true });
expect(e.format(TestEnum.FOO)).toBe("FOO");
expect(e.format(TestEnum.BAR)).toBe("BAR");
expect(e.format(TestEnum.BAZ)).toBe("BAZ");
expect(e.format(TestEnum.QUX)).toBe("QUX");
});
test("formats enum values with flags correctly", () => {
const e = DynamicEnum.create(TestEnum, { hasFlags: true });
expect(e.format(TestEnum.FOO)).toBe("FOO");
expect(e.format(TestEnum.BAR)).toBe("BAR");
expect(e.format(TestEnum.BAZ)).toBe("BAZ");
expect(e.format(TestEnum.QUX)).toBe("QUX");
expect(e.format(TestEnum.FOO | TestEnum.BAZ)).toBe("FOO, BAZ");
expect(e.format(TestEnum.FOO | TestEnum.BAZ | TestEnum.QUX)).toBe("FOO, BAZ, QUX");
});
test("returns invalid enum values as is", () => {
const e = DynamicEnum.create(TestEnum, { hasFlags: true });
expect(e.format(0 as TestEnum)).toBe("0");
expect(e.format(16 as TestEnum)).toBe("16");
});
});
describe("parse", () => {
test("parses a single enum value correctly", () => {
const e = DynamicEnum.create(TestEnum, { hasFlags: true });
expect(e.parse("FOO")).toBe(TestEnum.FOO);
expect(e.parse("BAR")).toBe(TestEnum.BAR);
expect(e.parse("BAZ")).toBe(TestEnum.BAZ);
expect(e.parse("QUX")).toBe(TestEnum.QUX);
});
test("parses enum values with flags correctly", () => {
const e = DynamicEnum.create(TestEnum, { hasFlags: true });
expect(e.parse("FOO, BAZ")).toBe(TestEnum.FOO | TestEnum.BAZ);
expect(e.parse("FOO, BAZ, QUX")).toBe(TestEnum.FOO | TestEnum.BAZ | TestEnum.QUX);
expect(e.parse("FOO|BAZ|QUX")).toBe(TestEnum.FOO | TestEnum.BAZ | TestEnum.QUX);
});
test("returns undefined for an invalid enum value", () => {
const e = DynamicEnum.create(TestEnum, { hasFlags: true });
expect(e.parse("QUUX")).toBeUndefined();
expect(e.parse("FOO, Huh")).toBeUndefined();
expect(e.parse("FOO|Huh")).toBeUndefined();
});
});
});