From 650ca179f4b88b0237989f0e385f354818e1bcfe Mon Sep 17 00:00:00 2001 From: Kir_Antipov Date: Tue, 16 May 2023 16:03:22 +0000 Subject: [PATCH] Enhanced conversion logic --- src/utils/convert.ts | 58 +++++++++++++------------- tests/unit/utils/convert.spec.ts | 71 ++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 29 deletions(-) diff --git a/src/utils/convert.ts b/src/utils/convert.ts index cfee667..70d7e68 100644 --- a/src/utils/convert.ts +++ b/src/utils/convert.ts @@ -1,7 +1,8 @@ -import { stringEquals } from "@/utils/string-utils"; -import { TypeOfResult, NamedType } from "@/utils/types/type-of"; import { $i } from "@/utils/collections/iterable"; -import { getAllNames } from "@/utils/reflection/object-reflector"; +import { Func } from "@/utils/functions/func"; +import { getAllNames, getSafe } from "@/utils/reflection/object-reflector"; +import { stringEquals } from "@/utils/string-utils"; +import { NamedType, TypeOfResult } from "@/utils/types"; /** * Represents a function that converts a value to some target type. @@ -295,38 +296,31 @@ type ParsableGlobalThisMember = ParseMethod(obj: unknown): Convert | undefined { - // Attempt to retrieve a `Converter` function from the object using the conversion method prefixes. - const converter = getParseLikeFunction(obj, CONVERT_METHOD_PREFIXES) as Convert; +function getConverter(obj: unknown, prioritizeParsing?: boolean): Convert | undefined { + const strategies = [ + [CONVERT_METHOD_PREFIXES], + [PARSE_METHOD_PREFIXES, (parser: Func) => (x: unknown) => typeof x === "string" ? parser(x) : undefined], + ] as const; - // If a `Converter` function was found, return it. - if (converter) { - return converter; + const resolvedStrategies = prioritizeParsing ? [...strategies].reverse() : strategies; + + for (const [prefixes, mapper] of resolvedStrategies) { + const parseLike = getParseLikeFunction(obj, prefixes); + if (!parseLike) { + continue; + } + + const mapped = mapper ? mapper(parseLike) : parseLike; + return mapped as Convert; } - // Otherwise, attempt to retrieve a `Parser` function from the object and create a `Converter` function that uses it. - const parser = getParser(obj); - if (parser) { - return x => typeof x === "string" ? parser(x) : undefined; - } - - // If neither a `Converter` nor a `Parser` function was found, return undefined. return undefined; } -/** - * Retrieves a `Parser` function from the given object, if one is defined. - * - * @param obj - The object to retrieve the `Parser` function from. - * @returns A `Parser` function that can parse a string to the target type `T`, or `undefined` if none was found. - */ -function getParser(obj: unknown): Parse | undefined { - // Attempt to retrieve a `Parser` function from the object using the parsing method prefixes. - return getParseLikeFunction(obj, PARSE_METHOD_PREFIXES) as Parse; -} - /** * Attempts to retrieve a parsing method from the given object using the specified prefixes. * @@ -341,9 +335,15 @@ function getParseLikeFunction(obj: unknown, prefixes: readonly string[]): (obj: return undefined; } + // If the object has a method named exactly like one of the given prefix, we should use it. + const prioritizedParseMethodName = $i(prefixes).first(x => typeof getSafe(obj, x) === "function"); + if (prioritizedParseMethodName) { + return x => obj[prioritizedParseMethodName](x); + } + // Find all method names on the object that start with one of the specified prefixes. const propertyNames = getAllNames(obj); - const parseMethodNames = $i(propertyNames).filter(x => typeof obj[x] === "function" && prefixes.some(p => x.startsWith(p))); + const parseMethodNames = $i(propertyNames).filter(x => prefixes.some(p => x.startsWith(p) && typeof getSafe(obj, x) === "function")); // Determine the first parse-like method name by sorting them based on prefix precedence and taking the first result. const firstParseMethodName = $i(parseMethodNames).min( @@ -509,7 +509,7 @@ export function toType(obj: unknown, target: unknown): unknown { try { // Attempt to retrieve a converter function from the target type. - const converter = getConverter(target); + const converter = getConverter(target, typeof obj === "string"); // If the converter function was found, use it to convert the input object. if (converter !== undefined) { diff --git a/tests/unit/utils/convert.spec.ts b/tests/unit/utils/convert.spec.ts index 11f865b..33a3724 100644 --- a/tests/unit/utils/convert.spec.ts +++ b/tests/unit/utils/convert.spec.ts @@ -397,6 +397,19 @@ describe("toType", () => { }); describe("from convertible object", () => { + test("converts a value via the standard 'convert' function in a class", () => { + const convert = jest.fn().mockImplementation(o => String(o)); + class Convertible { + static convert(n: number): string { + return convert(n); + } + } + + expect(toType(123, Convertible)).toBe("123"); + expect(convert).toBeCalledTimes(1); + expect(convert).toBeCalledWith(123); + }); + test("converts a value via the standard 'convert' function", () => { const convertible = { convert: jest.fn().mockImplementation(o => String(o)), @@ -417,6 +430,64 @@ describe("toType", () => { expect(convertible.convertObjectToNumber).toBeCalledWith(123); }); + test("converts a value via the prioritized 'convert' function", () => { + const convertible = { + convert: jest.fn().mockImplementation(o => String(o)), + convertObjectToNumber: jest.fn(), + from: jest.fn(), + fromObjectTonNumber: jest.fn(), + parse: jest.fn(), + parseToNumber: jest.fn(), + }; + + expect(toType(123, convertible)).toBe("123"); + expect(convertible.convert).toBeCalledTimes(1); + expect(convertible.convert).toBeCalledWith(123); + expect(convertible.convertObjectToNumber).not.toHaveBeenCalled(); + expect(convertible.from).not.toHaveBeenCalled(); + expect(convertible.fromObjectTonNumber).not.toHaveBeenCalled(); + expect(convertible.parse).not.toHaveBeenCalled(); + expect(convertible.parseToNumber).not.toHaveBeenCalled(); + }); + + test("converts a value via the prioritized 'from' function, if 'convert' is not present", () => { + const convertible = { + convertObjectToNumber: jest.fn(), + from: jest.fn().mockImplementation(o => String(o)), + fromObjectTonNumber: jest.fn(), + parse: jest.fn(), + parseToNumber: jest.fn(), + }; + + expect(toType(123, convertible)).toBe("123"); + expect(convertible.from).toBeCalledTimes(1); + expect(convertible.from).toBeCalledWith(123); + expect(convertible.convertObjectToNumber).not.toHaveBeenCalled(); + expect(convertible.fromObjectTonNumber).not.toHaveBeenCalled(); + expect(convertible.parse).not.toHaveBeenCalled(); + expect(convertible.parseToNumber).not.toHaveBeenCalled(); + }); + + test("parses a string via the prioritized 'parse' function", () => { + const convertible = { + convert: jest.fn(), + convertObjectToNumber: jest.fn(), + from: jest.fn(), + fromObjectTonNumber: jest.fn(), + parse: jest.fn().mockImplementation(x => +x), + parseToNumber: jest.fn(), + }; + + expect(toType("123", convertible)).toBe(123); + expect(convertible.parse).toBeCalledTimes(1); + expect(convertible.parse).toBeCalledWith("123"); + expect(convertible.parseToNumber).not.toHaveBeenCalled(); + expect(convertible.convert).not.toHaveBeenCalled(); + expect(convertible.convertObjectToNumber).not.toHaveBeenCalled(); + expect(convertible.from).not.toHaveBeenCalled(); + expect(convertible.fromObjectTonNumber).not.toHaveBeenCalled(); + }); + test("returns undefined when conversion is not possible", () => { expect(toType(123, {})).toBeUndefined(); expect(toType(123, { notConvertFunction: () => 42 })).toBeUndefined();