From 36b3d69f4681271e2e5e24909bb8c4fcbeb5b491 Mon Sep 17 00:00:00 2001 From: Kir_Antipov Date: Tue, 7 Jun 2022 15:58:13 +0300 Subject: [PATCH] Implemented `mapEnumInput`, so I can extract enums from options --- src/utils/input-utils.ts | 69 ++++++++++++++++++++++++++++++++++++---- test/input-utils.test.ts | 58 ++++++++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 8 deletions(-) diff --git a/src/utils/input-utils.ts b/src/utils/input-utils.ts index 18ab1bf..ec51bf1 100644 --- a/src/utils/input-utils.ts +++ b/src/utils/input-utils.ts @@ -1,5 +1,9 @@ import process from "process"; +interface EnumLike { + [i: string | number | symbol]: T | string; +} + // eslint-disable-next-line @typescript-eslint/no-empty-interface interface InputObject extends Record { } @@ -34,11 +38,11 @@ function init(root: InputObject, path: string[], value: string): void { } export function mapStringInput(value: any, defaultValue = ""): string { - return mapInput(value, defaultValue ?? ""); + return mapInput(value, defaultValue ?? "", null, "string"); } export function mapObjectInput(value: any, defaultValue: object = null): object { - return mapInput(value, defaultValue ?? null); + return mapInput(value, defaultValue ?? null, null, "object"); } export function mapNumberInput(value: any, defaultValue = 0): number { @@ -47,7 +51,7 @@ export function mapNumberInput(value: any, defaultValue = 0): number { const num = +x; return isNaN(num) ? undefined : num; } - }); + }, "number"); } export function mapBooleanInput(value: any, defaultValue = false): boolean { @@ -60,22 +64,73 @@ export function mapBooleanInput(value: any, defaultValue = false): boolean { undefined ); } - }); + }, "boolean"); } -export function mapInput(value: any, fallbackValue: T, mappers?: Record T | undefined>): T { +function findEnumValueByName, U>(enumClass: T, name: string): U | undefined { + if (typeof enumClass[+name] === "string") { + return +name; + } + + if (enumClass[name] !== undefined) { + return enumClass[name]; + } + + const entries = Object.entries(enumClass); + for (const [key, value] of entries) { + if (key.localeCompare(name, undefined, { sensitivity: "base" }) === 0) { + return value; + } + } + for (const [key, value] of entries) { + if (key.trim().replace(/[-_]/g, "").localeCompare(name.trim().replace(/[-_]/g, ""), undefined, { sensitivity: "base" }) === 0) { + return value; + } + } + return undefined; +} + +export function mapEnumInput, U>(value: any, enumClass: T, defaultValue: U = null): U | null { + return mapInput(value, defaultValue, { + string: (x: string) => { + let result: U = undefined; + + let i = 0; + while (i < x.length) { + let separatorIndex = x.indexOf("|", i); + if (separatorIndex === -1) { + separatorIndex = x.length; + } + + const currentValue = findEnumValueByName(enumClass, x.substring(i, separatorIndex)); + if (result === undefined || currentValue !== undefined && typeof currentValue !== "number") { + result = currentValue; + } else { + result = (result | currentValue); + } + + i = separatorIndex + 1; + } + + return result; + } + }, "number"); +} + +export function mapInput(value: any, fallbackValue?: T, mappers?: Record T | undefined>, valueType?: string): T { if (value === undefinedValue || value === undefined || value === null) { return fallbackValue; } - if (typeof value === typeof fallbackValue) { + valueType ??= typeof fallbackValue; + if (typeof value === valueType) { return value; } const mapper = mappers?.[typeof value]; if (mapper) { const mappedValue = mapper(value); - if (typeof mappedValue === typeof fallbackValue) { + if (typeof mappedValue === valueType) { return mappedValue; } } diff --git a/test/input-utils.test.ts b/test/input-utils.test.ts index ccd5dc9..ce66fbc 100644 --- a/test/input-utils.test.ts +++ b/test/input-utils.test.ts @@ -1,6 +1,6 @@ import { describe, test, expect, beforeAll, afterAll } from "@jest/globals"; import { setupInput, unsetInput } from "./utils/input-utils"; -import { getInputAsObject, mapStringInput, mapObjectInput, mapNumberInput, mapBooleanInput } from "../src/utils/input-utils"; +import { getInputAsObject, mapStringInput, mapObjectInput, mapNumberInput, mapBooleanInput, mapEnumInput } from "../src/utils/input-utils"; const defaultInput = { "boolean": true, @@ -195,3 +195,59 @@ describe("mapBooleanInput", () => { expect(mapBooleanInput(input["booleanfalsestringuppercasewithwhitespace"], true)).toBe(false); }); }); + +describe("mapEnumInput", () => { + enum TestEnum { + None = 0, + + A = 1, + B = 2, + C = 4, + + A_B = 1 | 2, + A_C = 1 | 4, + B_C = 2 | 4, + + A_B_C = 1 | 2 | 4, + } + + beforeAll(() => setupInput({ + ...defaultInput, + enumAB: TestEnum.A_B, + enumABStringUpperCase: "A_B", + enumABStringLowerCase: "a_b", + enumABStringLowerCaseWithWhitespace: " a_b ", + enumABStringLowerCaseWithWhitespaceAndDifferentSeparator: " a-b ", + + enumABStringLowerCaseWithWhitespaceBitwise: " a | b ", + enumABCStringLowerCaseWithWhitespaceBitwise: " c | b | b | a ", + })); + afterAll(() => unsetInput()); + + test("returns default value if input cannot be casted to the given enum", () => { + const input = getInputAsObject(); + + expect(input["object"]).not.toBeUndefined(); + expect(mapEnumInput(input["object"], TestEnum)).toBeNull(); + + expect(input["boolean"]).not.toBeUndefined(); + expect(mapEnumInput(input["boolean"], TestEnum)).toBeNull(); + + expect(input["array"]).not.toBeUndefined(); + expect(mapEnumInput(input["array"], TestEnum)).toBeNull(); + + expect(input["undefined"]).toBeUndefined(); + expect(mapEnumInput(input["undefined"], TestEnum, TestEnum.A_B_C)).toBe(TestEnum.A_B_C); + }); + + test("maps values to the given enum", () => { + const input = getInputAsObject(); + expect(mapEnumInput(input["enumab"], TestEnum)).toBe(TestEnum.A_B); + expect(mapEnumInput(input["enumabstringuppercase"], TestEnum)).toBe(TestEnum.A_B); + expect(mapEnumInput(input["enumabstringlowercase"], TestEnum)).toBe(TestEnum.A_B); + expect(mapEnumInput(input["enumabstringlowercasewithwhitespace"], TestEnum)).toBe(TestEnum.A_B); + expect(mapEnumInput(input["enumabstringlowercasewithwhitespaceanddifferentseparator"], TestEnum)).toBe(TestEnum.A_B); + expect(mapEnumInput(input["enumabstringlowercasewithwhitespacebitwise"], TestEnum)).toBe(TestEnum.A_B); + expect(mapEnumInput(input["enumabcstringlowercasewithwhitespacebitwise"], TestEnum, TestEnum.A_B)).toBe(TestEnum.A_B_C); + }); +});