diff --git a/src/compare.ts b/src/compare.ts new file mode 100644 index 0000000000..e4629d240b --- /dev/null +++ b/src/compare.ts @@ -0,0 +1,128 @@ +import { NoTransformConfigurationError } from "./transformers/NoTransformConfigurationError"; + +/* ----------------------------------------------------------- + COMPARE FUNCTIONS +----------------------------------------------------------- */ +/** + * Compare namespace for value comparison operations. + * + * @author Jeongho Nam - https://github.com/samchon + */ +export namespace compare { + /** + * Tests whether `x` covers `y`. + * + * Tests a parametric value type and returns whether `x` covers `y` or not. + * If the parametric value `x` covers `y`, `true` value will be returned. + * Otherwise, `false` value will be returned. + * + * @template T Type of the input values + * @param x First value to be compared + * @param y Second value to be compared + * @returns Whether the parametric value `x` covers `y` or not + * + * @author Jeongho Nam - https://github.com/samchon + */ + export function covers(x: T, y: T): boolean; + + /** + * Tests whether `x` covers `y`. + * + * Tests a parametric value type and returns whether `x` covers `y` or not. + * If the parametric value `x` covers `y`, `true` value will be returned. + * Otherwise, `false` value will be returned. + * + * @template T Type of the input values + * @param x First value to be compared + * @param y Second value to be compared + * @returns Whether the parametric value `x` covers `y` or not + * + * @author Jeongho Nam - https://github.com/samchon + */ + export function covers(x: unknown, y: unknown): boolean; + + /** + * @internal + */ + export function covers(): never { + NoTransformConfigurationError("compare.covers"); + } + + /** + * Tests equality between two values. + * + * Tests a parametric value type and returns whether `x` and `y` are equal or not. + * If the parametric values are deeply equal, `true` value will be returned. + * Otherwise, `false` value will be returned. + * + * @template T Type of the input values + * @param x First value to be compared + * @param y Second value to be compared + * @returns Whether the parametric values are equal or not + * + * @author Jeongho Nam - https://github.com/samchon + */ + export function equals(x: T, y: T): boolean; + + /** + * Tests equality between two values. + * + * Tests a parametric value type and returns whether `x` and `y` are equal or not. + * If the parametric values are deeply equal, `true` value will be returned. + * Otherwise, `false` value will be returned. + * + * @template T Type of the input values + * @param x First value to be compared + * @param y Second value to be compared + * @returns Whether the parametric values are equal or not + * + * @author Jeongho Nam - https://github.com/samchon + */ + export function equals(x: unknown, y: unknown): boolean; + + /** + * @internal + */ + export function equals(): never { + NoTransformConfigurationError("compare.equals"); + } + + /** + * Tests whether `x` is less than `y`. + * + * Tests a parametric value type and returns whether `x` is less than `y` or not. + * If the parametric value `x` is less than `y`, `true` value will be returned. + * Otherwise, `false` value will be returned. + * + * @template T Type of the input values + * @param x First value to be compared + * @param y Second value to be compared + * @returns Whether the parametric value `x` is less than `y` or not + * + * @author Jeongho Nam - https://github.com/samchon + */ + export function less(x: T, y: T): boolean; + + /** + * Tests whether `x` is less than `y`. + * + * Tests a parametric value type and returns whether `x` is less than `y` or not. + * If the parametric value `x` is less than `y`, `true` value will be returned. + * Otherwise, `false` value will be returned. + * + * @template T Type of the input values + * @param x First value to be compared + * @param y Second value to be compared + * @returns Whether the parametric value `x` is less than `y` or not + * + * @author Jeongho Nam - https://github.com/samchon + */ + export function less(x: unknown, y: unknown): boolean; + + /** + * @internal + */ + export function less(): never { + NoTransformConfigurationError("compare.less"); + } +} \ No newline at end of file diff --git a/src/module.ts b/src/module.ts index 679ad5a842..9ce96ed613 100644 --- a/src/module.ts +++ b/src/module.ts @@ -17,6 +17,7 @@ export * as notations from "./notations"; export * as protobuf from "./protobuf"; export * as reflect from "./reflect"; export * as tags from "./tags"; +export * as compare from "./compare"; export * from "./schemas/metadata/IJsDocTagInfo"; export * from "./schemas/json/IJsonApplication"; diff --git a/src/programmers/CompareProgrammer.ts b/src/programmers/CompareProgrammer.ts new file mode 100644 index 0000000000..bcf6774477 --- /dev/null +++ b/src/programmers/CompareProgrammer.ts @@ -0,0 +1,306 @@ +import ts from "typescript"; + +import { MetadataCollection } from "../factories/MetadataCollection"; +import { MetadataFactory } from "../factories/MetadataFactory"; + +import { Metadata } from "../schemas/metadata/Metadata"; + +import { IProgrammerProps } from "../transformers/IProgrammerProps"; +import { ITypiaContext } from "../transformers/ITypiaContext"; + +import { CheckerProgrammer } from "./CheckerProgrammer"; +import { FeatureProgrammer } from "./FeatureProgrammer"; +import { FunctionProgrammer } from "./helpers/FunctionProgrammer"; + +export namespace CompareProgrammer { + /* ----------------------------------------------------------- + INTERFACES + ----------------------------------------------------------- */ + export interface IConfig { + equals: boolean; + covers?: boolean; + less?: boolean; + } + + export interface IProps extends IProgrammerProps { + config: IConfig; + } + + /* ----------------------------------------------------------- + MAIN FUNCTIONS + ----------------------------------------------------------- */ + export const decompose = (props: { + context: ITypiaContext; + functor: FunctionProgrammer; + config: IConfig; + type: ts.Type; + name: string | undefined; + }): FeatureProgrammer.IDecomposed => { + // CONFIGURATION - simplified for compare + const config: CheckerProgrammer.IConfig = { + prefix: "_c", + equals: false, + trace: false, + path: false, + numeric: true, + atomist: ({ entry }) => entry.expression ?? ts.factory.createTrue(), + combiner: () => ts.factory.createTrue(), + joiner: { + object: () => ts.factory.createTrue(), + array: () => ts.factory.createTrue(), + failure: () => ts.factory.createFalse(), + }, + success: ts.factory.createTrue(), + }; + + // COMPOSITION + const composed: FeatureProgrammer.IComposed = CheckerProgrammer.compose({ + ...props, + config, + }); + return { + functions: composed.functions, + statements: composed.statements, + arrow: writeComparison(props), + }; + }; + + export const write = (props: IProps): ts.Expression => { + const functor: FunctionProgrammer = new FunctionProgrammer( + props.config.equals ? "compare.equals" : + props.config.covers ? "compare.covers" : "compare.less" + ); + + const result: FeatureProgrammer.IDecomposed = decompose({ + ...props, + functor, + }); + + return FeatureProgrammer.writeDecomposed({ + modulo: props.modulo, + functor, + result, + }); + }; + + /* ----------------------------------------------------------- + COMPARISON WRITER + ----------------------------------------------------------- */ + const writeComparison = (props: { + context: ITypiaContext; + config: IConfig; + type: ts.Type; + }): ts.ArrowFunction => { + // Generate metadata for the type + const collection: MetadataCollection = new MetadataCollection({ + replace: MetadataCollection.replace, + }); + + const result = MetadataFactory.analyze({ + checker: props.context.checker, + transformer: props.context.transformer, + options: { + absorb: true, + functional: false, + escape: false, + constant: true, + }, + collection, + type: props.type, + }); + + const metadata = result.success ? result.data : (() => { + throw new Error("Failed to analyze metadata"); + })(); + + // Generate comparison function parameters + const xParam = ts.factory.createIdentifier("x"); + const yParam = ts.factory.createIdentifier("y"); + + const comparison = write_comparison({ + context: props.context, + metadata, + config: props.config, + xExpr: xParam, + yExpr: yParam, + }); + + return ts.factory.createArrowFunction( + undefined, + undefined, + [ + ts.factory.createParameterDeclaration( + undefined, + undefined, + xParam, + undefined, + undefined, + undefined, + ), + ts.factory.createParameterDeclaration( + undefined, + undefined, + yParam, + undefined, + undefined, + undefined, + ), + ], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + comparison, + ); + }; + + /* ----------------------------------------------------------- + COMPARISON LOGIC + ----------------------------------------------------------- */ + const write_comparison = (props: { + context: ITypiaContext; + metadata: Metadata; + config: IConfig; + xExpr: ts.Expression; + yExpr: ts.Expression; + }): ts.Expression => { + // Handle null/undefined first + const nullCheck = write_null_check(props); + if (nullCheck) return nullCheck; + + // Handle primitive types + if (props.metadata.atomics.length || props.metadata.constants.length) { + return write_atomic_comparison(props); + } + + // Handle arrays + if (props.metadata.arrays.length) { + return write_array_comparison(props); + } + + // Handle objects + if (props.metadata.objects.length) { + return write_object_comparison(props); + } + + // Handle tuples + if (props.metadata.tuples.length) { + return write_tuple_comparison(props); + } + + // Default fallback + return ts.factory.createStrictEquality(props.xExpr, props.yExpr); + }; + + const write_null_check = (props: { + metadata: Metadata; + xExpr: ts.Expression; + yExpr: ts.Expression; + }): ts.Expression | null => { + const hasNull = props.metadata.nullable; + const hasUndefined = props.metadata.required === false; + + if (!hasNull && !hasUndefined) { + return null; + } + + let expr: ts.Expression | null = null; + + if (hasNull) { + const nullComparison = ts.factory.createConditionalExpression( + ts.factory.createStrictEquality( + props.xExpr, + ts.factory.createNull(), + ), + ts.factory.createToken(ts.SyntaxKind.QuestionToken), + ts.factory.createStrictEquality( + props.yExpr, + ts.factory.createNull(), + ), + ts.factory.createToken(ts.SyntaxKind.ColonToken), + ts.factory.createFalse(), + ); + expr = expr ? ts.factory.createLogicalOr(expr, nullComparison) : nullComparison; + } + + if (hasUndefined) { + const undefinedComparison = ts.factory.createConditionalExpression( + ts.factory.createStrictEquality( + props.xExpr, + ts.factory.createIdentifier("undefined"), + ), + ts.factory.createToken(ts.SyntaxKind.QuestionToken), + ts.factory.createStrictEquality( + props.yExpr, + ts.factory.createIdentifier("undefined"), + ), + ts.factory.createToken(ts.SyntaxKind.ColonToken), + ts.factory.createFalse(), + ); + expr = expr ? ts.factory.createLogicalOr(expr, undefinedComparison) : undefinedComparison; + } + + return expr; + }; + + const write_atomic_comparison = (props: { + xExpr: ts.Expression; + yExpr: ts.Expression; + }): ts.Expression => { + return ts.factory.createStrictEquality(props.xExpr, props.yExpr); + }; + + const write_array_comparison = (props: { + context: ITypiaContext; + metadata: Metadata; + config: IConfig; + xExpr: ts.Expression; + yExpr: ts.Expression; + }): ts.Expression => { + // Simple array comparison for now + return ts.factory.createLogicalAnd( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier("Array"), + "isArray", + ), + undefined, + [props.xExpr], + ), + ts.factory.createLogicalAnd( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier("Array"), + "isArray", + ), + undefined, + [props.yExpr], + ), + ts.factory.createStrictEquality( + ts.factory.createPropertyAccessExpression(props.xExpr, "length"), + ts.factory.createPropertyAccessExpression(props.yExpr, "length"), + ), + ), + ); + }; + + const write_object_comparison = (props: { + context: ITypiaContext; + metadata: Metadata; + config: IConfig; + xExpr: ts.Expression; + yExpr: ts.Expression; + }): ts.Expression => { + // Simple object comparison for now + return ts.factory.createStrictEquality(props.xExpr, props.yExpr); + }; + + const write_tuple_comparison = (props: { + context: ITypiaContext; + metadata: Metadata; + config: IConfig; + xExpr: ts.Expression; + yExpr: ts.Expression; + }): ts.Expression => { + // Simple tuple comparison for now + return ts.factory.createStrictEquality(props.xExpr, props.yExpr); + }; +} \ No newline at end of file diff --git a/src/transformers/CallExpressionTransformer.ts b/src/transformers/CallExpressionTransformer.ts index dc7fbd5929..972b7980af 100644 --- a/src/transformers/CallExpressionTransformer.ts +++ b/src/transformers/CallExpressionTransformer.ts @@ -17,6 +17,7 @@ import { NamingConvention } from "../utils/NamingConvention"; import { ITransformProps } from "./ITransformProps"; import { ITypiaContext } from "./ITypiaContext"; import { AssertTransformer } from "./features/AssertTransformer"; +import { CompareTransformer } from "./features/CompareTransformer"; import { CreateAssertTransformer } from "./features/CreateAssertTransformer"; import { CreateIsTransformer } from "./features/CreateIsTransformer"; import { CreateRandomTransformer } from "./features/CreateRandomTransformer"; @@ -212,6 +213,12 @@ const FUNCTORS: Record Task>> = { }), createRandom: () => CreateRandomTransformer.transform, }, + compare: { + // COMPARE FUNCTIONS + equals: () => CompareTransformer.transform({ equals: true }), + covers: () => CompareTransformer.transform({ equals: false, covers: true }), + less: () => CompareTransformer.transform({ equals: false, less: true }), + }, functional: { // ASSERTIONS assertFunction: () => diff --git a/src/transformers/features/CompareTransformer.ts b/src/transformers/features/CompareTransformer.ts new file mode 100644 index 0000000000..8fa430d69f --- /dev/null +++ b/src/transformers/features/CompareTransformer.ts @@ -0,0 +1,18 @@ +import { CompareProgrammer } from "../../programmers/CompareProgrammer"; + +import { ITransformProps } from "../ITransformProps"; +import { GenericTransformer } from "../internal/GenericTransformer"; + +export namespace CompareTransformer { + export const transform = + (config: CompareProgrammer.IConfig) => (props: ITransformProps) => + GenericTransformer.scalar({ + ...props, + method: config.equals ? "compare.equals" : config.covers ? "compare.covers" : "compare.less", + write: (x) => + CompareProgrammer.write({ + ...x, + config, + }), + }); +} \ No newline at end of file