From 64f717d1275457b05393a2b62bc2e2bfd9726054 Mon Sep 17 00:00:00 2001 From: Kir_Antipov Date: Sat, 21 Jan 2023 15:19:18 +0000 Subject: [PATCH] Implemented `Object`-like `Enum` interface --- src/utils/enum/enum.ts | 154 +++++++++++++++++++++++++++++ tests/unit/utils/enum/enum.spec.ts | 119 ++++++++++++++++++++++ 2 files changed, 273 insertions(+) create mode 100644 src/utils/enum/enum.ts create mode 100644 tests/unit/utils/enum/enum.spec.ts diff --git a/src/utils/enum/enum.ts b/src/utils/enum/enum.ts new file mode 100644 index 0000000..be8b48e --- /dev/null +++ b/src/utils/enum/enum.ts @@ -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(value: T, flag: T): boolean { + const descriptor = getEnumDescriptorByUnderlyingType(typeof flag) as EnumDescriptor; + 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(enumFactory: () => T, options?: EnumOptions): ConstructedEnum; + +/** + * 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(enumFactory: () => T, options: EnumOptions, methods: U): ConstructedEnum & Readonly; + +/** + * 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(underlyingEnum: T, options?: EnumOptions): ConstructedEnum; + +/** + * 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(underlyingEnum: T, options: EnumOptions, methods: U): ConstructedEnum & Readonly; + +/** + * 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(e: T | (() => T), options?: EnumOptions, methods?: unknown): ConstructedEnum { + 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 = { + [K in keyof T]: K extends "size" | "underlyingType" | typeof Symbol.toStringTag + ? never + : T[K] extends (...args: unknown[]) => unknown + ? never + : T[K]; +}[keyof T]; diff --git a/tests/unit/utils/enum/enum.spec.ts b/tests/unit/utils/enum/enum.spec.ts new file mode 100644 index 0000000..dcb4d9f --- /dev/null +++ b/tests/unit/utils/enum/enum.spec.ts @@ -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); + }); + }); +});