Implemented mapEnumInput, so I can extract enums from options

This commit is contained in:
Kir_Antipov 2022-06-07 15:58:13 +03:00
parent 44e418ffe7
commit 36b3d69f46
2 changed files with 119 additions and 8 deletions

View file

@ -1,5 +1,9 @@
import process from "process";
interface EnumLike<T = number> {
[i: string | number | symbol]: T | string;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface InputObject extends Record<string, string | InputObject> { }
@ -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<T>(value: any, fallbackValue: T, mappers?: Record<string, (x: any) => T | undefined>): T {
function findEnumValueByName<T extends EnumLike<U>, U>(enumClass: T, name: string): U | undefined {
if (typeof enumClass[+name] === "string") {
return <U><unknown>+name;
}
if (enumClass[name] !== undefined) {
return <U>enumClass[name];
}
const entries = Object.entries(enumClass);
for (const [key, value] of entries) {
if (key.localeCompare(name, undefined, { sensitivity: "base" }) === 0) {
return <U>value;
}
}
for (const [key, value] of entries) {
if (key.trim().replace(/[-_]/g, "").localeCompare(name.trim().replace(/[-_]/g, ""), undefined, { sensitivity: "base" }) === 0) {
return <U>value;
}
}
return undefined;
}
export function mapEnumInput<T extends EnumLike<U>, 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<T, U>(enumClass, x.substring(i, separatorIndex));
if (result === undefined || currentValue !== undefined && typeof currentValue !== "number") {
result = currentValue;
} else {
result = <U><unknown>(<number><unknown>result | <number><unknown>currentValue);
}
i = separatorIndex + 1;
}
return result;
}
}, "number");
}
export function mapInput<T>(value: any, fallbackValue?: T, mappers?: Record<string, (x: any) => 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;
}
}

View file

@ -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);
});
});