From 1afbf2483e96d1ea7e6d4701dc87f097598f1ee1 Mon Sep 17 00:00:00 2001
From: Kir_Antipov <kp.antipov@gmail.com>
Date: Thu, 19 Jan 2023 15:51:18 +0000
Subject: [PATCH] Implemented enum descriptor instantiation logic

---
 src/utils/enum/descriptors/enum-descriptor.ts | 51 +++++++++++++++++++
 .../enum/descriptors/enum-descriptor.spec.ts  | 33 ++++++++++++
 2 files changed, 84 insertions(+)
 create mode 100644 tests/unit/utils/enum/descriptors/enum-descriptor.spec.ts

diff --git a/src/utils/enum/descriptors/enum-descriptor.ts b/src/utils/enum/descriptors/enum-descriptor.ts
index 7e60f69..c55f5c1 100644
--- a/src/utils/enum/descriptors/enum-descriptor.ts
+++ b/src/utils/enum/descriptors/enum-descriptor.ts
@@ -1,4 +1,8 @@
 import { NamedType, TypeOf, TypeOfResult } from "@/utils/types";
+import { BigIntDescriptor } from "./bigint-descriptor";
+import { BooleanDescriptor } from "./boolean-descriptor";
+import { NumberDescriptor } from "./number-descriptor";
+import { StringDescriptor } from "./string-descriptor";
 
 /**
  * Interface that defines operations that should be implemented by an underlying type of an `Enum`.
@@ -46,3 +50,50 @@ export interface EnumDescriptor<T> {
      */
     removeFlag(value: T, flag: T): T;
 }
+
+/**
+ * A map of known `EnumDescriptor`s, keyed by the string representation of their underlying type.
+ */
+const KNOWN_ENUM_DESCRIPTORS = new Map<TypeOfResult, EnumDescriptor<unknown>>([
+    ["bigint", new BigIntDescriptor()],
+    ["boolean", new BooleanDescriptor()],
+    ["number", new NumberDescriptor()],
+    ["string", new StringDescriptor()],
+]);
+
+/**
+ * Gets the {@link EnumDescriptor} for the provided type name.
+ *
+ * @template T - The type of the result to return
+ * @param type - The name of the type to get the descriptor for
+ *
+ * @returns The descriptor for the specified type, or `undefined` if there is no such descriptor.
+ */
+export function getEnumDescriptorByUnderlyingType<T extends TypeOfResult>(type: T): EnumDescriptor<NamedType<T>> | undefined {
+    return KNOWN_ENUM_DESCRIPTORS.get(type) as EnumDescriptor<NamedType<T>>;
+}
+
+/**
+ * Infers the descriptor for an enum based on its values.
+ *
+ * @template T - Type of the enum.
+ *
+ * @param values - The values of the enum.
+ *
+ * @returns The inferred descriptor for the enum.
+ *
+ * @throws An error if the enum contains objects of different types or an invalid underlying type.
+ */
+export function inferEnumDescriptorOrThrow<T>(values: readonly T[]): EnumDescriptor<T> | never {
+    if (!values.every((x, i, self) => i === 0 || typeof x === typeof self[i - 1])) {
+        throw new Error("The enum must contain objects of the same type.");
+    }
+
+    const underlyingType = values.length ? typeof values[0] : "number";
+    const descriptor = getEnumDescriptorByUnderlyingType(underlyingType) as EnumDescriptor<T>;
+    if (!descriptor) {
+        throw new Error(`'${underlyingType}' is not an acceptable enum type.`);
+    }
+
+    return descriptor;
+}
diff --git a/tests/unit/utils/enum/descriptors/enum-descriptor.spec.ts b/tests/unit/utils/enum/descriptors/enum-descriptor.spec.ts
new file mode 100644
index 0000000..460e289
--- /dev/null
+++ b/tests/unit/utils/enum/descriptors/enum-descriptor.spec.ts
@@ -0,0 +1,33 @@
+import { getEnumDescriptorByUnderlyingType, inferEnumDescriptorOrThrow } from "@/utils/enum/descriptors/enum-descriptor";
+
+describe("getEnumDescriptorByUnderlyingType", () => {
+    test("returns the correct descriptor for a known type", () => {
+        const knownTypes = ["number", "bigint", "boolean", "string"] as const;
+        for (const knowType of knownTypes) {
+            expect(getEnumDescriptorByUnderlyingType(knowType)?.name).toBe(knowType);
+        }
+    });
+
+    test("returns undefined for an unknown type", () => {
+        expect(getEnumDescriptorByUnderlyingType("unknownType" as "string")).toBeUndefined();
+    });
+});
+
+describe("inferEnumDescriptorOrThrow", () => {
+    test("infers correct descriptor based on enum values", () => {
+        expect(inferEnumDescriptorOrThrow([1, 2, 3]).name).toBe("number");
+        expect(inferEnumDescriptorOrThrow(["a", "b", "c"]).name).toBe("string");
+    });
+
+    test("throws error if enum contains objects of different types", () => {
+        expect(() => inferEnumDescriptorOrThrow([1, "b", 3])).toThrow("The enum must contain objects of the same type.");
+    });
+
+    test("throws error if enum has an invalid underlying type", () => {
+        expect(() => inferEnumDescriptorOrThrow([{}])).toThrow("'object' is not an acceptable enum type.");
+    });
+
+    test("returns a number descriptor for an empty array", () => {
+        expect(inferEnumDescriptorOrThrow([]).name).toBe("number");
+    });
+});