Implemented Object-like Enum interface

This commit is contained in:
Kir_Antipov 2023-01-21 15:19:18 +00:00
parent 5fe6c0ba36
commit 64f717d127
2 changed files with 273 additions and 0 deletions

154
src/utils/enum/enum.ts Normal file
View file

@ -0,0 +1,154 @@
import { IGNORE_CASE_AND_NON_WORD_CHARACTERS_EQUALITY_COMPARER, IGNORE_CASE_EQUALITY_COMPARER, IGNORE_NON_WORD_CHARACTERS_EQUALITY_COMPARER, ORDINAL_EQUALITY_COMPARER } from "@/utils/comparison";
import { EnumDescriptor, getEnumDescriptorByUnderlyingType } from "./descriptors";
import { ConstructedEnum, DynamicEnum, DynamicEnumOptions } from "./dynamic-enum";
import { enumKeys } from "./enum-key";
import { enumValues } from "./enum-value";
import { enumEntries } from "./enum-entry";
/**
* The options to use when creating the new enum.
*/
export interface EnumOptions extends DynamicEnumOptions {
/**
* Indicates whether to ignore the case when comparing enum keys.
*/
ignoreCase?: boolean;
/**
* Indicates whether to ignore non-word characters when comparing enum keys.
*/
ignoreNonWordCharacters?: boolean;
}
/**
* Determines whether the given `value` contains the specified `flag`.
*
* @template T - Type of the enum.
*
* @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`.
*/
export function hasFlag<T>(value: T, flag: T): boolean {
const descriptor = getEnumDescriptorByUnderlyingType(typeof flag) as EnumDescriptor<T>;
return !!descriptor?.hasFlag(value, flag);
}
/**
* Creates a new enum object from the specified `enumFactory` with the specified `options`.
*
* @template T - Type of the enum.
*
* @param enumFactory - The enum factory to use for the new enum.
* @param options - The options to use when creating the new enum.
*
* @returns The constructed enum object.
*/
export function createEnum<T>(enumFactory: () => T, options?: EnumOptions): ConstructedEnum<T>;
/**
* Creates a new enum object from the specified `enumFactory` with the specified `options`.
*
* @template T - Type of the enum.
*
* @param enumFactory - The enum factory to use for the new enum.
* @param options - The options to use when creating the new enum.
*
* @returns The constructed enum object.
*/
export function createEnum<T, U>(enumFactory: () => T, options: EnumOptions, methods: U): ConstructedEnum<T> & Readonly<U>;
/**
* Creates a new enum object from the specified underlying enum with the specified `options`.
*
* @template T - Type of the enum.
*
* @param underlyingEnum - The underlying enum to use for the new enum.
* @param options - The options to use when creating the new enum.
*
* @returns The constructed enum object.
*/
export function createEnum<T>(underlyingEnum: T, options?: EnumOptions): ConstructedEnum<T>;
/**
* Creates a new enum object from the specified underlying enum with the specified `options`.
*
* @template T - Type of the enum.
*
* @param underlyingEnum - The underlying enum to use for the new enum.
* @param options - The options to use when creating the new enum.
*
* @returns The constructed enum object.
*/
export function createEnum<T, U>(underlyingEnum: T, options: EnumOptions, methods: U): ConstructedEnum<T> & Readonly<U>;
/**
* Creates a new enum object from the specified `enumFactory` or `underlyingEnum` with the specified `options`.
*
* @template T - Type of the enum.
*
* @param e - The enum factory or underlying enum to use for the new enum.
* @param options - The options to use when creating the new enum.
*
* @returns The constructed enum object.
*/
export function createEnum<T>(e: T | (() => T), options?: EnumOptions, methods?: unknown): ConstructedEnum<T> {
const underlyingEnum = typeof e === "function" ? (e as () => T)() : e;
const dynamicEnumOptions = toDynamicEnumOptions(options);
const dynamicEnum = DynamicEnum.create(underlyingEnum, dynamicEnumOptions);
if (methods) {
Object.assign(dynamicEnum, methods);
}
return dynamicEnum;
}
/**
* Converts specified `options` into an instance acceptable by the {@link DynamicEnum}'s constructor.
*
* @param options - The options to be converted.
*
* @returns The options acceptable by the {@link DynamicEnum}'s constructor.
*/
function toDynamicEnumOptions(options?: EnumOptions): DynamicEnumOptions {
if (!options || (options as DynamicEnumOptions).comparer) {
return options;
}
const o = options as EnumOptions;
const comparer = o.ignoreCase ? o.ignoreNonWordCharacters
? IGNORE_CASE_AND_NON_WORD_CHARACTERS_EQUALITY_COMPARER
: IGNORE_CASE_EQUALITY_COMPARER
: o.ignoreNonWordCharacters
? IGNORE_NON_WORD_CHARACTERS_EQUALITY_COMPARER
: ORDINAL_EQUALITY_COMPARER;
return { ...o, comparer };
}
/**
* An object that emulates the `Object` API for `Enum` objects.
*/
export const Enum = {
hasFlag,
create: createEnum,
keys: enumKeys,
values: enumValues,
entries: enumEntries,
} as const;
/**
* A type that extracts definition of the original enum
* from the dynamically created one.
*
* @template T - Type of the dynamically created enum.
*/
export type Enum<T> = {
[K in keyof T]: K extends "size" | "underlyingType" | typeof Symbol.toStringTag
? never
: T[K] extends (...args: unknown[]) => unknown
? never
: T[K];
}[keyof T];

View file

@ -0,0 +1,119 @@
import { enumEntries } from "@/utils/enum/enum-entry";
import { enumKeys } from "@/utils/enum/enum-key";
import { enumValues } from "@/utils/enum/enum-value";
import { DynamicEnum } from "@/utils/enum/dynamic-enum";
import { hasFlag, Enum, createEnum } from "@/utils/enum/enum";
describe("hasFlag", () => {
test("returns true if a flag is set", () => {
expect(hasFlag(3, 2)).toBe(true);
expect(hasFlag(3n, 2n)).toBe(true);
expect(hasFlag(true, true)).toBe(true);
expect(hasFlag("value1, value2", "value2")).toBe(true);
expect(hasFlag("value1 | value2", "value2")).toBe(true);
expect(hasFlag("value1|value2", "value2")).toBe(true);
});
test("returns false if a flag is not set", () => {
expect(hasFlag(3, 4)).toBe(false);
expect(hasFlag(3n, 4n)).toBe(false);
expect(hasFlag(false, true)).toBe(false);
expect(hasFlag("value1, value2", "value3")).toBe(false);
expect(hasFlag("value1 | value2", "value3")).toBe(false);
expect(hasFlag("value1|value2", "value3")).toBe(false);
});
test("returns false if a value cannot contain flags", () => {
expect(hasFlag({}, {})).toBe(false);
expect(hasFlag([], [])).toBe(false);
expect(hasFlag(Symbol("a"), Symbol("b"))).toBe(false);
});
});
describe("createEnum", () => {
test("creates an enum when a plain object is given", () => {
const e = createEnum({ A: "A", B: "B" });
expect(e).toBeInstanceOf(DynamicEnum);
expect(e.A).toBe("A");
expect(e.B).toBe("B");
});
test("creates an enum when a plain enum is given", () => {
enum TestEnum {
A = 1,
B = 2,
}
const e = createEnum(TestEnum);
expect(e).toBeInstanceOf(DynamicEnum);
expect(e.A).toBe(1);
expect(e.B).toBe(2);
});
test("creates an enum when a function returning an object is given", () => {
const e = createEnum(() => ({ A: "A", B: "B" }));
expect(e).toBeInstanceOf(DynamicEnum);
expect(e.A).toBe("A");
expect(e.B).toBe("B");
});
test("creates an enum with custom methods", () => {
const e = createEnum({ A: "A", B: "B" }, {}, { customMethod: () => "custom" });
expect(e.customMethod()).toBe("custom");
});
test("creates an enum with 'ignoreCase' option", () => {
const e = createEnum({ A: "A", B: "B", Foo: "Foo" }, { ignoreCase: true });
expect(e.get("A")).toBe("A");
expect(e.get("a")).toBe("A");
expect(e.get("B")).toBe("B");
expect(e.get("b")).toBe("B");
expect(e.get("Foo")).toBe("Foo");
expect(e.get("foo")).toBe("Foo");
expect(e.get("FOO")).toBe("Foo");
expect(e.get("FoO")).toBe("Foo");
});
test("creates an enum with 'ignoreNonWordCharacters' option", () => {
const e = createEnum({ "a-b": "a-b", "C_D": "C_D" }, { ignoreNonWordCharacters: true });
expect(e.get("ab")).toBe("a-b");
expect(e.get("CD")).toBe("C_D");
});
});
describe("Enum", () => {
describe("hasFlag", () => {
test("redirects the call to 'hasFlag'", () => {
expect(Enum.hasFlag).toBe(hasFlag);
});
});
describe("create", () => {
test("redirects the call to 'createEnum'", () => {
expect(Enum.create).toBe(createEnum);
});
});
describe("keys", () => {
test("redirects the call to 'enumKeys'", () => {
expect(Enum.keys).toBe(enumKeys);
});
});
describe("values", () => {
test("redirects the call to 'enumValues'", () => {
expect(Enum.values).toBe(enumValues);
});
});
describe("entries", () => {
test("redirects the call to 'enumEntries'", () => {
expect(Enum.entries).toBe(enumEntries);
});
});
});