mirror of
https://github.com/Kir-Antipov/mc-publish.git
synced 2024-11-22 00:11:02 -05:00
Implemented DynamicEnum
This commit is contained in:
parent
8e382c9ed5
commit
5fe6c0ba36
2 changed files with 762 additions and 0 deletions
446
src/utils/enum/dynamic-enum.ts
Normal file
446
src/utils/enum/dynamic-enum.ts
Normal 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>;
|
316
tests/unit/utils/enum/dynamic-enum.spec.ts
Normal file
316
tests/unit/utils/enum/dynamic-enum.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue