Enhanced conversion logic

This commit is contained in:
Kir_Antipov 2023-05-16 16:03:22 +00:00
parent 149430bbe9
commit 650ca179f4
2 changed files with 100 additions and 29 deletions

View file

@ -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 { $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. * Represents a function that converts a value to some target type.
@ -295,38 +296,31 @@ type ParsableGlobalThisMember<T extends keyof GlobalThis> = ParseMethod<GlobalTh
* Retrieves a `Converter` function from the given object, if one is defined. * Retrieves a `Converter` function from the given object, if one is defined.
* *
* @param obj - The object to retrieve the `Converter` function from. * @param obj - The object to retrieve the `Converter` function from.
* @param prioritizeParsing - Indicates wether the parsing should be prioritized.
*
* @returns A `Converter` function that can convert an unknown value to the target type `T`, or `undefined` if none was found. * @returns A `Converter` function that can convert an unknown value to the target type `T`, or `undefined` if none was found.
*/ */
function getConverter<T>(obj: unknown): Convert<T> | undefined { function getConverter<T>(obj: unknown, prioritizeParsing?: boolean): Convert<T> | undefined {
// Attempt to retrieve a `Converter` function from the object using the conversion method prefixes. const strategies = [
const converter = getParseLikeFunction(obj, CONVERT_METHOD_PREFIXES) as Convert<T>; [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. const resolvedStrategies = prioritizeParsing ? [...strategies].reverse() : strategies;
if (converter) {
return converter; for (const [prefixes, mapper] of resolvedStrategies) {
const parseLike = getParseLikeFunction(obj, prefixes);
if (!parseLike) {
continue;
}
const mapped = mapper ? mapper(parseLike) : parseLike;
return mapped as Convert<T>;
} }
// Otherwise, attempt to retrieve a `Parser` function from the object and create a `Converter` function that uses it.
const parser = getParser<T>(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; 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<T>(obj: unknown): Parse<T> | undefined {
// Attempt to retrieve a `Parser` function from the object using the parsing method prefixes.
return getParseLikeFunction(obj, PARSE_METHOD_PREFIXES) as Parse<T>;
}
/** /**
* Attempts to retrieve a parsing method from the given object using the specified prefixes. * 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; 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. // Find all method names on the object that start with one of the specified prefixes.
const propertyNames = getAllNames(obj); 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. // Determine the first parse-like method name by sorting them based on prefix precedence and taking the first result.
const firstParseMethodName = $i(parseMethodNames).min( const firstParseMethodName = $i(parseMethodNames).min(
@ -509,7 +509,7 @@ export function toType(obj: unknown, target: unknown): unknown {
try { try {
// Attempt to retrieve a converter function from the target type. // 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 the converter function was found, use it to convert the input object.
if (converter !== undefined) { if (converter !== undefined) {

View file

@ -397,6 +397,19 @@ describe("toType", () => {
}); });
describe("from convertible object", () => { 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", () => { test("converts a value via the standard 'convert' function", () => {
const convertible = { const convertible = {
convert: jest.fn().mockImplementation(o => String(o)), convert: jest.fn().mockImplementation(o => String(o)),
@ -417,6 +430,64 @@ describe("toType", () => {
expect(convertible.convertObjectToNumber).toBeCalledWith(123); 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", () => { test("returns undefined when conversion is not possible", () => {
expect(toType(123, {})).toBeUndefined(); expect(toType(123, {})).toBeUndefined();
expect(toType(123, { notConvertFunction: () => 42 })).toBeUndefined(); expect(toType(123, { notConvertFunction: () => 42 })).toBeUndefined();