diff --git a/src/utils/enum/dynamic-enum.ts b/src/utils/enum/dynamic-enum.ts new file mode 100644 index 0000000..6081791 --- /dev/null +++ b/src/utils/enum/dynamic-enum.ts @@ -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; + + /** + * 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; +} + +/** + * A dynamic enum implementation that allows you to create an enum at runtime. + * + * @template T - The type of the enum. + */ +export class DynamicEnum implements ReadonlyMap, EnumValue> { + /** + * An array of enum keys. + */ + private readonly _keys: readonly EnumKey[]; + + /** + * An array of enum values. + */ + private readonly _values: readonly EnumValue[]; + + /** + * A map containing the enum keys and their corresponding display names. + */ + private readonly _names: ReadonlyMap; + + /** + * The enum descriptor. + */ + private readonly _descriptor: EnumDescriptor>; + + /** + * 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; + + /** + * 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[], 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(underlyingEnum: TEnum, options?: DynamicEnumOptions): ConstructedEnum { + const entries = enumEntries(underlyingEnum); + return new DynamicEnum(entries, options) as ConstructedEnum; + } + + /** + * 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> { + 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, flag: EnumValue): 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 | string): EnumValue | undefined { + // Attempt to retrieve the value from this object's properties. + const value = (this as unknown as T)[key as EnumKey]; + 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): EnumKey | 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): 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, key: EnumKey, e: ConstructedEnum) => boolean, thisArg?: unknown): EnumValue | 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, key: EnumKey, e: ConstructedEnum) => boolean, thisArg?: unknown): EnumKey | 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)) { + 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 | 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): boolean { + return this._values.includes(value); + } + + /** + * Returns an iterator that yields the keys of the enum. + */ + keys(): IterableIterator> { + return this._keys[Symbol.iterator](); + } + + /** + * Returns an iterator that yields the values of the enum. + */ + values(): IterableIterator> { + return this._values[Symbol.iterator](); + } + + /** + * Returns an iterator that yields the key/value pairs for every entry in the enum. + */ + *entries(): IterableIterator> { + 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> { + 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, key: EnumKey, e: ConstructedEnum) => 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); + } + } + + /** + * 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): 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 { + // 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 | undefined { + // If the value was found, return it as is. + const value = this.get(key as EnumKey); + 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; + 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 = DynamicEnum & Readonly; diff --git a/tests/unit/utils/enum/dynamic-enum.spec.ts b/tests/unit/utils/enum/dynamic-enum.spec.ts new file mode 100644 index 0000000..ac032e3 --- /dev/null +++ b/tests/unit/utils/enum/dynamic-enum.spec.ts @@ -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(); + }); + }); +});