diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 80e113a..016e683 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -166,33 +166,57 @@ const result = await generateCode({ - : Contains the core logic for converting TypeScript type nodes into TypeBox `Type` expressions. `getTypeBoxType` takes a `TypeNode` as input and returns a `ts.Node` representing the equivalent TypeBox schema. - : Generates and adds the `export type [TypeName] = Static` declaration to the output source file. This declaration is essential for enabling TypeScript's static type inference from the dynamically generated TypeBox schemas, ensuring type safety at compile time. - : Contains general utility functions that support the TypeBox code generation process, such as helper functions for string manipulation or AST node creation. -- : Responsible for parsing TypeScript source code and extracting relevant Abstract Syntax Tree (AST) information. It provides functions to navigate the AST and identify specific nodes like type aliases, interfaces, or enums. -- : Defines custom types and interfaces that represent the structured AST information extracted by `typescript-ast-parser.ts`, providing a consistent data model for further processing. ### Handlers Directory -This directory contains a collection of specialized handler modules, each responsible for converting a specific type of TypeScript AST node into its corresponding TypeBox schema. This modular approach allows for easy extension and maintenance of the type mapping logic. +This directory contains a collection of specialized handler modules, each responsible for converting a specific type of TypeScript AST node into its corresponding TypeBox schema. The handlers follow a hierarchical architecture with specialized base classes to reduce code duplication and ensure consistent behavior. + +#### Base Handler Classes + +- : The root abstract base class that defines the common interface for all type handlers. Provides the `canHandle` and `handle` methods, along with utility functions like `makeTypeCall` for creating TypeBox expressions. +- : Specialized base class for utility type handlers that work with TypeScript type references. Provides `validateTypeReference` and `extractTypeArguments` methods for consistent handling of generic utility types like `Partial`, `Pick`, etc. +- : Base class for handlers that process object-like structures (objects and interfaces). Provides `processProperties`, `extractProperties`, and `createObjectType` methods for consistent property handling and TypeBox object creation. +- : Base class for handlers that work with collections of types (arrays, tuples, unions, intersections). Provides `processTypeCollection`, `processSingleType`, and `validateNonEmptyCollection` methods for consistent type collection processing. + +#### Type Handler Implementations + +**Utility Type Handlers** (extend `TypeReferenceBaseHandler`): + +- : Handles TypeScript `Partial` utility types. +- : Handles TypeScript `Pick` utility types. +- : Handles TypeScript `Omit` utility types. +- : Handles TypeScript `Required` utility types. +- : Handles TypeScript `Record` utility types. + +**Object-Like Type Handlers** (extend `ObjectLikeBaseHandler`): + +- : Handles TypeScript object types and type literals. +- : Handles TypeScript interface declarations. + +**Collection Type Handlers** (extend `CollectionBaseHandler`): + +- : Handles TypeScript array types (e.g., `string[]`, `Array`). +- : Handles TypeScript tuple types. +- : Handles TypeScript union types (e.g., `string | number`). +- : Handles TypeScript intersection types (e.g., `TypeA & TypeB`). + +**Standalone Type Handlers** (extend `BaseTypeHandler`): -- : Handles TypeScript array types (e.g., `string[]`, `Array`). -- : Handles TypeScript indexed access types (e.g., `Type[Key]`). -- : Handles TypeScript intersection types (e.g., `TypeA & TypeB`). -- : Handles TypeScript literal types (e.g., `'hello'`, `123`, `true`). -- : Handles TypeScript object types and interfaces. -- : Handles TypeScript `Omit` utility types. -- : Handles TypeScript `Partial` utility types. -- : Handles TypeScript `Pick` utility types. -- : Handles TypeScript `Required` utility types. -- : Handles TypeScript `Record` utility types. - : Handles basic TypeScript types like `string`, `number`, `boolean`, `null`, `undefined`, `any`, `unknown`, `void`. +- : Handles TypeScript literal types (e.g., `'hello'`, `123`, `true`). - : Handles TypeScript function types and function declarations, including parameter types, optional parameters, and return types. - : Handles TypeScript template literal types (e.g., `` `hello-${string}` ``). Parses template literals into components, handling literal text, embedded types (string, number, unions), and string/numeric literals. - : Handles TypeScript `typeof` expressions for extracting types from values. -- : Handles TypeScript tuple types. -- : Handles TypeScript type operators like `keyof`, `typeof`. +- : Handles TypeScript `keyof` type operator for extracting object keys. +- : Handles TypeScript `readonly` type modifier for creating immutable types. +- : Fallback handler for other TypeScript type operators not covered by specific handlers. - : Handles references to other types (e.g., `MyType`). +- : Handles TypeScript indexed access types (e.g., `Type[Key]`). - : A generic handler for TypeBox types. -- : Orchestrates the use of the individual type handlers, acting as a dispatcher based on the type of AST node encountered. -- : Handles TypeScript union types (e.g., `string | number`). + +**Handler Orchestration**: + +- : Orchestrates the use of the individual type handlers, acting as a dispatcher based on the type of AST node encountered. Uses optimized lookup mechanisms for performance with O(1) syntax kind-based lookups and type reference name mappings. Includes specialized handlers for type operators (KeyOfTypeHandler, TypeofTypeHandler, ReadonlyTypeHandler) and maintains fallback handlers for edge cases requiring custom logic. ### Parsers Directory diff --git a/src/handlers/typebox/array-type-handler.ts b/src/handlers/typebox/array-type-handler.ts deleted file mode 100644 index 6c00dd3..0000000 --- a/src/handlers/typebox/array-type-handler.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' -import { Node, ts } from 'ts-morph' - -export class ArrayTypeHandler extends BaseTypeHandler { - constructor(getTypeBoxType: (typeNode?: Node) => ts.Expression) { - super(getTypeBoxType) - } - - canHandle(typeNode?: Node): boolean { - return Node.isArrayTypeNode(typeNode) - } - - handle(typeNode: Node): ts.Expression { - if (!Node.isArrayTypeNode(typeNode)) { - return makeTypeCall('Any') - } - const elementType = typeNode.getElementTypeNode() - const elementTypeBox: ts.Expression = this.getTypeBoxType(elementType) - return makeTypeCall('Array', [elementTypeBox]) - } -} diff --git a/src/handlers/typebox/base-type-handler.ts b/src/handlers/typebox/base-type-handler.ts index 340fdce..c9eaf39 100644 --- a/src/handlers/typebox/base-type-handler.ts +++ b/src/handlers/typebox/base-type-handler.ts @@ -1,12 +1,6 @@ import { Node, ts } from 'ts-morph' export abstract class BaseTypeHandler { - protected getTypeBoxType: (typeNode?: Node) => ts.Expression - - constructor(getTypeBoxType: (typeNode?: Node) => ts.Expression) { - this.getTypeBoxType = getTypeBoxType - } - - abstract canHandle(typeNode: Node | undefined): boolean - abstract handle(typeNode: Node | undefined): ts.Expression + abstract canHandle(node: Node): boolean + abstract handle(node: Node): ts.Expression } diff --git a/src/handlers/typebox/collection/array-type-handler.ts b/src/handlers/typebox/collection/array-type-handler.ts new file mode 100644 index 0000000..9993e71 --- /dev/null +++ b/src/handlers/typebox/collection/array-type-handler.ts @@ -0,0 +1,12 @@ +import { CollectionBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/collection/collection-base-handler' +import { ArrayTypeNode, Node, ts } from 'ts-morph' + +export class ArrayTypeHandler extends CollectionBaseHandler { + canHandle(node: Node): boolean { + return Node.isArrayTypeNode(node) + } + + handle(node: ArrayTypeNode): ts.Expression { + return this.processSingleType(node.getElementTypeNode(), 'Array') + } +} diff --git a/src/handlers/typebox/collection/collection-base-handler.ts b/src/handlers/typebox/collection/collection-base-handler.ts new file mode 100644 index 0000000..dfa634d --- /dev/null +++ b/src/handlers/typebox/collection/collection-base-handler.ts @@ -0,0 +1,23 @@ +import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' +import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' +import { Node, ts } from 'ts-morph' + +export abstract class CollectionBaseHandler extends BaseTypeHandler { + protected processTypeCollection(nodes: Node[], typeBoxFunction: string): ts.Expression { + const typeBoxTypes = nodes.map((node) => getTypeBoxType(node)) + const arrayLiteral = ts.factory.createArrayLiteralExpression(typeBoxTypes) + + return makeTypeCall(typeBoxFunction, [arrayLiteral]) + } + + protected processSingleType(node: Node, typeBoxFunction: string): ts.Expression { + return makeTypeCall(typeBoxFunction, [getTypeBoxType(node)]) + } + + protected validateNonEmptyCollection(nodes: Node[], typeName: string): void { + if (nodes.length === 0) { + throw new Error(`${typeName} must have at least one type`) + } + } +} diff --git a/src/handlers/typebox/collection/intersection-type-handler.ts b/src/handlers/typebox/collection/intersection-type-handler.ts new file mode 100644 index 0000000..ac23ce0 --- /dev/null +++ b/src/handlers/typebox/collection/intersection-type-handler.ts @@ -0,0 +1,12 @@ +import { CollectionBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/collection/collection-base-handler' +import { IntersectionTypeNode, Node, ts } from 'ts-morph' + +export class IntersectionTypeHandler extends CollectionBaseHandler { + canHandle(node: Node): boolean { + return Node.isIntersectionTypeNode(node) + } + + handle(node: IntersectionTypeNode): ts.Expression { + return this.processTypeCollection(node.getTypeNodes(), 'Intersect') + } +} diff --git a/src/handlers/typebox/collection/tuple-type-handler.ts b/src/handlers/typebox/collection/tuple-type-handler.ts new file mode 100644 index 0000000..ac981ca --- /dev/null +++ b/src/handlers/typebox/collection/tuple-type-handler.ts @@ -0,0 +1,12 @@ +import { CollectionBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/collection/collection-base-handler' +import { Node, ts, TupleTypeNode } from 'ts-morph' + +export class TupleTypeHandler extends CollectionBaseHandler { + canHandle(node: Node): boolean { + return Node.isTupleTypeNode(node) + } + + handle(node: TupleTypeNode): ts.Expression { + return this.processTypeCollection(node.getElements(), 'Tuple') + } +} diff --git a/src/handlers/typebox/collection/union-type-handler.ts b/src/handlers/typebox/collection/union-type-handler.ts new file mode 100644 index 0000000..d070c42 --- /dev/null +++ b/src/handlers/typebox/collection/union-type-handler.ts @@ -0,0 +1,12 @@ +import { CollectionBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/collection/collection-base-handler' +import { Node, ts, UnionTypeNode } from 'ts-morph' + +export class UnionTypeHandler extends CollectionBaseHandler { + canHandle(node: Node): boolean { + return Node.isUnionTypeNode(node) + } + + handle(node: UnionTypeNode): ts.Expression { + return this.processTypeCollection(node.getTypeNodes(), 'Union') + } +} diff --git a/src/handlers/typebox/function-type-handler.ts b/src/handlers/typebox/function-type-handler.ts index 4c961c0..99f4da1 100644 --- a/src/handlers/typebox/function-type-handler.ts +++ b/src/handlers/typebox/function-type-handler.ts @@ -1,28 +1,21 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' -import { Node, ts } from 'ts-morph' +import { FunctionTypeNode, Node, ts } from 'ts-morph' export class FunctionTypeHandler extends BaseTypeHandler { - constructor(getTypeBoxType: (typeNode?: Node) => ts.Expression) { - super(getTypeBoxType) + canHandle(node: Node): boolean { + return Node.isFunctionTypeNode(node) } - canHandle(typeNode?: Node): boolean { - return Node.isFunctionTypeNode(typeNode) - } - - handle(typeNode: Node): ts.Expression { - if (!Node.isFunctionTypeNode(typeNode)) { - return makeTypeCall('Any') - } - - const parameters = typeNode.getParameters() - const returnType = typeNode.getReturnTypeNode() + handle(node: FunctionTypeNode): ts.Expression { + const parameters = node.getParameters() + const returnType = node.getReturnTypeNode() // Convert parameters to TypeBox types const parameterTypes = parameters.map((param) => { const paramTypeNode = param.getTypeNode() - const paramType = this.getTypeBoxType(paramTypeNode) + const paramType = getTypeBoxType(paramTypeNode) // Check if parameter is optional if (param.hasQuestionToken()) { @@ -33,7 +26,7 @@ export class FunctionTypeHandler extends BaseTypeHandler { }) // Convert return type to TypeBox type - const returnTypeBox = this.getTypeBoxType(returnType) + const returnTypeBox = getTypeBoxType(returnType) // Create TypeBox Function call with parameters array and return type return makeTypeCall('Function', [ diff --git a/src/handlers/typebox/indexed-access-type-handler.ts b/src/handlers/typebox/indexed-access-type-handler.ts index 74b2408..e71546e 100644 --- a/src/handlers/typebox/indexed-access-type-handler.ts +++ b/src/handlers/typebox/indexed-access-type-handler.ts @@ -1,27 +1,27 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' import { IndexedAccessTypeNode, Node, ts } from 'ts-morph' export class IndexedAccessTypeHandler extends BaseTypeHandler { - canHandle(node: Node | undefined): boolean { - return node !== undefined && node.isKind(ts.SyntaxKind.IndexedAccessType) + canHandle(node: Node): boolean { + return node.isKind(ts.SyntaxKind.IndexedAccessType) } - handle(node: Node): ts.Expression { - const typeNode = node as IndexedAccessTypeNode - const objectType = typeNode.getObjectTypeNode() - const indexType = typeNode.getIndexTypeNode() + handle(node: IndexedAccessTypeNode): ts.Expression { + const objectType = node.getObjectTypeNode() + const indexType = node.getIndexTypeNode() // Handle special case: typeof A[number] where A is a readonly tuple if ( objectType?.isKind(ts.SyntaxKind.TypeQuery) && indexType?.isKind(ts.SyntaxKind.NumberKeyword) ) { - return this.handleTypeofArrayAccess(objectType, typeNode) + return this.handleTypeofArrayAccess(objectType, node) } - const typeboxObjectType = this.getTypeBoxType(objectType) - const typeboxIndexType = this.getTypeBoxType(indexType) + const typeboxObjectType = getTypeBoxType(objectType) + const typeboxIndexType = getTypeBoxType(indexType) return makeTypeCall('Index', [typeboxObjectType, typeboxIndexType]) } @@ -58,8 +58,8 @@ export class IndexedAccessTypeHandler extends BaseTypeHandler { } // Fallback to default Index behavior - const typeboxObjectType = this.getTypeBoxType(typeQuery) - const typeboxIndexType = this.getTypeBoxType(indexedAccessType.getIndexTypeNode()) + const typeboxObjectType = getTypeBoxType(typeQuery) + const typeboxIndexType = getTypeBoxType(indexedAccessType.getIndexTypeNode()) return makeTypeCall('Index', [typeboxObjectType, typeboxIndexType]) } diff --git a/src/handlers/typebox/interface-type-handler.ts b/src/handlers/typebox/interface-type-handler.ts deleted file mode 100644 index f382d00..0000000 --- a/src/handlers/typebox/interface-type-handler.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' -import { InterfaceDeclaration, Node, ts } from 'ts-morph' - -export class InterfaceTypeHandler extends BaseTypeHandler { - constructor(getTypeBoxType: (typeNode?: Node) => ts.Expression) { - super(getTypeBoxType) - } - - canHandle(typeNode?: Node): boolean { - return Node.isInterfaceDeclaration(typeNode) - } - - handle(typeNode: Node): ts.Expression { - if (!Node.isInterfaceDeclaration(typeNode)) { - return makeTypeCall('Any') - } - - const interfaceDecl = typeNode as InterfaceDeclaration - const properties: ts.PropertyAssignment[] = [] - - for (const prop of interfaceDecl.getProperties()) { - const propName = prop.getName() - const propTypeNode = prop.getTypeNode() - const valueExpr = this.getTypeBoxType(propTypeNode) - const isAlreadyOptional = - ts.isCallExpression(valueExpr) && - ts.isPropertyAccessExpression(valueExpr.expression) && - valueExpr.expression.name.text === 'Optional' - - const maybeOptional = - prop.hasQuestionToken() && !isAlreadyOptional - ? makeTypeCall('Optional', [valueExpr]) - : valueExpr - - const nameNode = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(propName) - ? ts.factory.createIdentifier(propName) - : ts.factory.createStringLiteral(propName) - - properties.push(ts.factory.createPropertyAssignment(nameNode, maybeOptional)) - } - - const objectLiteral = ts.factory.createObjectLiteralExpression(properties, true) - return makeTypeCall('Object', [objectLiteral]) - } -} diff --git a/src/handlers/typebox/intersection-type-handler.ts b/src/handlers/typebox/intersection-type-handler.ts deleted file mode 100644 index 6ea0902..0000000 --- a/src/handlers/typebox/intersection-type-handler.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' -import { Node, ts } from 'ts-morph' - -export class IntersectionTypeHandler extends BaseTypeHandler { - constructor(getTypeBoxType: (typeNode?: Node) => ts.Expression) { - super(getTypeBoxType) - } - - canHandle(typeNode?: Node): boolean { - return Node.isIntersectionTypeNode(typeNode) - } - - handle(typeNode: Node): ts.Expression { - if (!Node.isIntersectionTypeNode(typeNode)) { - return makeTypeCall('Any') - } - const intersectionTypes = typeNode.getTypeNodes().map(this.getTypeBoxType) - return makeTypeCall('Intersect', [ts.factory.createArrayLiteralExpression(intersectionTypes)]) - } -} diff --git a/src/handlers/typebox/keyof-type-handler.ts b/src/handlers/typebox/keyof-type-handler.ts new file mode 100644 index 0000000..331bfd0 --- /dev/null +++ b/src/handlers/typebox/keyof-type-handler.ts @@ -0,0 +1,17 @@ +import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' +import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' +import { Node, SyntaxKind, ts, TypeOperatorTypeNode } from 'ts-morph' + +export class KeyOfTypeHandler extends BaseTypeHandler { + canHandle(node: Node): boolean { + return Node.isTypeOperatorTypeNode(node) && node.getOperator() === SyntaxKind.KeyOfKeyword + } + + handle(node: TypeOperatorTypeNode): ts.Expression { + const operandType = node.getTypeNode() + const typeboxOperand = getTypeBoxType(operandType) + + return makeTypeCall('KeyOf', [typeboxOperand]) + } +} diff --git a/src/handlers/typebox/literal-type-handler.ts b/src/handlers/typebox/literal-type-handler.ts index 2c122e3..b10180d 100644 --- a/src/handlers/typebox/literal-type-handler.ts +++ b/src/handlers/typebox/literal-type-handler.ts @@ -3,23 +3,16 @@ import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox import { Node, SyntaxKind, ts } from 'ts-morph' export class LiteralTypeHandler extends BaseTypeHandler { - constructor() { - super(() => ts.factory.createIdentifier('')) // getTypeBoxType is not used in LiteralTypeHandler - } - canHandle(typeNode?: Node): boolean { - return ( - Node.isLiteralTypeNode(typeNode) || - Node.isTrueLiteral(typeNode) || - Node.isFalseLiteral(typeNode) - ) + canHandle(node: Node): boolean { + return Node.isLiteralTypeNode(node) || Node.isTrueLiteral(node) || Node.isFalseLiteral(node) } - handle(typeNode: Node): ts.Expression { - if (!Node.isLiteralTypeNode(typeNode)) { + handle(node: Node): ts.Expression { + if (!Node.isLiteralTypeNode(node)) { return makeTypeCall('Any') } - const literal = typeNode.getLiteral() + const literal = node.getLiteral() const literalKind = literal.getKind() switch (literalKind) { diff --git a/src/handlers/typebox/object/interface-type-handler.ts b/src/handlers/typebox/object/interface-type-handler.ts new file mode 100644 index 0000000..7546874 --- /dev/null +++ b/src/handlers/typebox/object/interface-type-handler.ts @@ -0,0 +1,12 @@ +import { ObjectLikeBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/object/object-like-base-handler' +import { InterfaceDeclaration, Node, ts } from 'ts-morph' + +export class InterfaceTypeHandler extends ObjectLikeBaseHandler { + canHandle(node: Node): boolean { + return Node.isInterfaceDeclaration(node) + } + + handle(node: InterfaceDeclaration): ts.Expression { + return this.createObjectType(this.processProperties(node.getProperties())) + } +} diff --git a/src/handlers/typebox/object-type-handler.ts b/src/handlers/typebox/object/object-like-base-handler.ts similarity index 57% rename from src/handlers/typebox/object-type-handler.ts rename to src/handlers/typebox/object/object-like-base-handler.ts index 2f5cb53..cfeac4d 100644 --- a/src/handlers/typebox/object-type-handler.ts +++ b/src/handlers/typebox/object/object-like-base-handler.ts @@ -1,27 +1,21 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' -import { Node, ts, TypeLiteralNode } from 'ts-morph' +import { PropertySignature, ts } from 'ts-morph' -export class ObjectTypeHandler extends BaseTypeHandler { - constructor(getTypeBoxType: (typeNode?: Node) => ts.Expression) { - super(getTypeBoxType) - } - - canHandle(typeNode?: Node): boolean { - return Node.isTypeLiteral(typeNode) - } - - handle(typeNode: Node): ts.Expression { - if (!Node.isTypeLiteral(typeNode)) { - return makeTypeCall('Any') - } +export abstract class ObjectLikeBaseHandler extends BaseTypeHandler { + protected processProperties(properties: PropertySignature[]): ts.PropertyAssignment[] { + const propertyAssignments: ts.PropertyAssignment[] = [] - const literal = typeNode as TypeLiteralNode - const properties: ts.PropertyAssignment[] = [] - for (const prop of literal.getProperties()) { + for (const prop of properties) { const propName = prop.getName() const propTypeNode = prop.getTypeNode() - const valueExpr = this.getTypeBoxType(propTypeNode) + + if (!propTypeNode) { + continue + } + + const valueExpr = getTypeBoxType(propTypeNode) const isAlreadyOptional = ts.isCallExpression(valueExpr) && ts.isPropertyAccessExpression(valueExpr.expression) && @@ -36,10 +30,15 @@ export class ObjectTypeHandler extends BaseTypeHandler { ? ts.factory.createIdentifier(propName) : ts.factory.createStringLiteral(propName) - properties.push(ts.factory.createPropertyAssignment(nameNode, maybeOptional)) + propertyAssignments.push(ts.factory.createPropertyAssignment(nameNode, maybeOptional)) } + return propertyAssignments + } + + protected createObjectType(properties: ts.PropertyAssignment[]): ts.Expression { const objectLiteral = ts.factory.createObjectLiteralExpression(properties, true) + return makeTypeCall('Object', [objectLiteral]) } } diff --git a/src/handlers/typebox/object/object-type-handler.ts b/src/handlers/typebox/object/object-type-handler.ts new file mode 100644 index 0000000..0c3ea21 --- /dev/null +++ b/src/handlers/typebox/object/object-type-handler.ts @@ -0,0 +1,12 @@ +import { ObjectLikeBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/object/object-like-base-handler' +import { Node, ts, TypeLiteralNode } from 'ts-morph' + +export class ObjectTypeHandler extends ObjectLikeBaseHandler { + canHandle(node: Node): boolean { + return Node.isTypeLiteral(node) + } + + handle(node: TypeLiteralNode): ts.Expression { + return this.createObjectType(this.processProperties(node.getProperties())) + } +} diff --git a/src/handlers/typebox/partial-type-handler.ts b/src/handlers/typebox/partial-type-handler.ts deleted file mode 100644 index 41a4528..0000000 --- a/src/handlers/typebox/partial-type-handler.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' -import { Node, ts } from 'ts-morph' - -export class PartialTypeHandler extends BaseTypeHandler { - constructor(getTypeBoxType: (typeNode?: Node) => ts.Expression) { - super(getTypeBoxType) - } - - canHandle(typeNode?: Node): boolean { - if (!Node.isTypeReference(typeNode)) { - return false - } - const referencedType = typeNode.getTypeName() - return Node.isIdentifier(referencedType) && referencedType.getText() === 'Partial' - } - - handle(typeNode: Node): ts.Expression { - if (!Node.isTypeReference(typeNode) || typeNode.getTypeArguments().length !== 1) { - return makeTypeCall('Any') - } - const [partialType] = typeNode.getTypeArguments() - const typeboxPartialType = this.getTypeBoxType(partialType) - return makeTypeCall('Partial', [typeboxPartialType]) - } -} diff --git a/src/handlers/typebox/readonly-type-handler.ts b/src/handlers/typebox/readonly-type-handler.ts new file mode 100644 index 0000000..7dafcd4 --- /dev/null +++ b/src/handlers/typebox/readonly-type-handler.ts @@ -0,0 +1,18 @@ +import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' +import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' +import { Node, SyntaxKind, ts, TypeOperatorTypeNode } from 'ts-morph' + +export class ReadonlyTypeHandler extends BaseTypeHandler { + canHandle(node: Node): boolean { + return Node.isTypeOperatorTypeNode(node) && node.getOperator() === SyntaxKind.ReadonlyKeyword + } + + handle(node: TypeOperatorTypeNode): ts.Expression { + const operandType = node.getTypeNode() + const typeboxOperand = getTypeBoxType(operandType) + + // TypeBox uses Readonly utility type for readonly modifiers + return makeTypeCall('Readonly', [typeboxOperand]) + } +} diff --git a/src/handlers/typebox/record-type-handler.ts b/src/handlers/typebox/record-type-handler.ts deleted file mode 100644 index 86d0561..0000000 --- a/src/handlers/typebox/record-type-handler.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' -import { Node, ts } from 'ts-morph' - -export class RecordTypeHandler extends BaseTypeHandler { - constructor(getTypeBoxType: (typeNode?: Node) => ts.Expression) { - super(getTypeBoxType) - } - - canHandle(typeNode?: Node): boolean { - if (!Node.isTypeReference(typeNode)) { - return false - } - const referencedType = typeNode.getTypeName() - return Node.isIdentifier(referencedType) && referencedType.getText() === 'Record' - } - - handle(typeNode: Node): ts.Expression { - if (!Node.isTypeReference(typeNode) || typeNode.getTypeArguments().length !== 2) { - return makeTypeCall('Any') - } - const [keyType, valueType] = typeNode.getTypeArguments() - const typeboxKeyType = this.getTypeBoxType(keyType) - const typeboxValueType = this.getTypeBoxType(valueType) - return makeTypeCall('Record', [typeboxKeyType, typeboxValueType]) - } -} diff --git a/src/handlers/typebox/omit-type-handler.ts b/src/handlers/typebox/reference/omit-type-handler.ts similarity index 62% rename from src/handlers/typebox/omit-type-handler.ts rename to src/handlers/typebox/reference/omit-type-handler.ts index 09da9e5..497d164 100644 --- a/src/handlers/typebox/omit-type-handler.ts +++ b/src/handlers/typebox/reference/omit-type-handler.ts @@ -1,29 +1,21 @@ -import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { TypeReferenceBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/type-reference-base-handler' +import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' import { Node, ts } from 'ts-morph' -export class OmitTypeHandler extends BaseTypeHandler { - constructor(getTypeBoxType: (typeNode?: Node) => ts.Expression) { - super(getTypeBoxType) - } +export class OmitTypeHandler extends TypeReferenceBaseHandler { + protected supportedTypeNames = ['Omit'] + protected expectedArgumentCount = 2 - canHandle(typeNode?: Node): boolean { - if (!Node.isTypeReference(typeNode)) { - return false - } - const referencedType = typeNode.getTypeName() - return Node.isIdentifier(referencedType) && referencedType.getText() === 'Omit' - } + handle(node: Node): ts.Expression { + const typeRef = this.validateTypeReference(node) + const [objectType, keysType] = this.extractTypeArguments(typeRef) - handle(typeNode: Node): ts.Expression { - if (!Node.isTypeReference(typeNode) || typeNode.getTypeArguments().length !== 2) { - return makeTypeCall('Any') - } - const [objectType, keysType] = typeNode.getTypeArguments() if (!keysType) { return makeTypeCall('Any') } - const typeboxObjectType = this.getTypeBoxType(objectType) + + const typeboxObjectType = getTypeBoxType(objectType) let omitKeys: string[] = [] if (Node.isUnionTypeNode(keysType)) { @@ -54,6 +46,7 @@ export class OmitTypeHandler extends BaseTypeHandler { ), ]) } + return makeTypeCall('Omit', [typeboxObjectType, typeboxKeys]) } } diff --git a/src/handlers/typebox/reference/partial-type-handler.ts b/src/handlers/typebox/reference/partial-type-handler.ts new file mode 100644 index 0000000..ad38392 --- /dev/null +++ b/src/handlers/typebox/reference/partial-type-handler.ts @@ -0,0 +1,18 @@ +import { TypeReferenceBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/type-reference-base-handler' +import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' +import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' +import { Node, ts } from 'ts-morph' + +export class PartialTypeHandler extends TypeReferenceBaseHandler { + protected readonly supportedTypeNames = ['Partial'] + protected readonly expectedArgumentCount = 1 + + handle(node: Node): ts.Expression { + const typeRef = this.validateTypeReference(node) + const [innerType] = this.extractTypeArguments(typeRef) + + const typeBoxType = getTypeBoxType(innerType) + + return makeTypeCall('Partial', [typeBoxType]) + } +} diff --git a/src/handlers/typebox/pick-type-handler.ts b/src/handlers/typebox/reference/pick-type-handler.ts similarity index 62% rename from src/handlers/typebox/pick-type-handler.ts rename to src/handlers/typebox/reference/pick-type-handler.ts index 71ef665..902997a 100644 --- a/src/handlers/typebox/pick-type-handler.ts +++ b/src/handlers/typebox/reference/pick-type-handler.ts @@ -1,29 +1,21 @@ -import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { TypeReferenceBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/type-reference-base-handler' +import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' import { Node, ts } from 'ts-morph' -export class PickTypeHandler extends BaseTypeHandler { - constructor(getTypeBoxType: (typeNode?: Node) => ts.Expression) { - super(getTypeBoxType) - } +export class PickTypeHandler extends TypeReferenceBaseHandler { + protected supportedTypeNames = ['Pick'] + protected expectedArgumentCount = 2 - canHandle(typeNode?: Node): boolean { - if (!Node.isTypeReference(typeNode)) { - return false - } - const referencedType = typeNode.getTypeName() - return Node.isIdentifier(referencedType) && referencedType.getText() === 'Pick' - } + handle(node: Node): ts.Expression { + const typeRef = this.validateTypeReference(node) + const [objectType, keysType] = this.extractTypeArguments(typeRef) - handle(typeNode: Node): ts.Expression { - if (!Node.isTypeReference(typeNode) || typeNode.getTypeArguments().length !== 2) { - return makeTypeCall('Any') - } - const [objectType, keysType] = typeNode.getTypeArguments() if (!keysType) { return makeTypeCall('Any') } - const typeboxObjectType = this.getTypeBoxType(objectType) + + const typeboxObjectType = getTypeBoxType(objectType) let pickKeys: string[] = [] if (Node.isUnionTypeNode(keysType)) { @@ -54,6 +46,7 @@ export class PickTypeHandler extends BaseTypeHandler { ), ]) } + return makeTypeCall('Pick', [typeboxObjectType, typeboxKeys]) } } diff --git a/src/handlers/typebox/reference/record-type-handler.ts b/src/handlers/typebox/reference/record-type-handler.ts new file mode 100644 index 0000000..b814b40 --- /dev/null +++ b/src/handlers/typebox/reference/record-type-handler.ts @@ -0,0 +1,19 @@ +import { TypeReferenceBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/type-reference-base-handler' +import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' +import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' +import { Node, ts } from 'ts-morph' + +export class RecordTypeHandler extends TypeReferenceBaseHandler { + protected readonly supportedTypeNames = ['Record'] + protected readonly expectedArgumentCount = 2 + + handle(node: Node): ts.Expression { + const typeRef = this.validateTypeReference(node) + const [keyType, valueType] = this.extractTypeArguments(typeRef) + + const typeBoxKeyType = getTypeBoxType(keyType) + const typeBoxValueType = getTypeBoxType(valueType) + + return makeTypeCall('Record', [typeBoxKeyType, typeBoxValueType]) + } +} diff --git a/src/handlers/typebox/reference/required-type-handler.ts b/src/handlers/typebox/reference/required-type-handler.ts new file mode 100644 index 0000000..17e2529 --- /dev/null +++ b/src/handlers/typebox/reference/required-type-handler.ts @@ -0,0 +1,16 @@ +import { TypeReferenceBaseHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/type-reference-base-handler' +import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' +import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' +import { Node, ts } from 'ts-morph' + +export class RequiredTypeHandler extends TypeReferenceBaseHandler { + protected readonly supportedTypeNames = ['Required'] + protected readonly expectedArgumentCount = 1 + + handle(node: Node): ts.Expression { + const typeRef = this.validateTypeReference(node) + const [innerType] = this.extractTypeArguments(typeRef) + + return makeTypeCall('Required', [getTypeBoxType(innerType)]) + } +} diff --git a/src/handlers/typebox/reference/type-reference-base-handler.ts b/src/handlers/typebox/reference/type-reference-base-handler.ts new file mode 100644 index 0000000..a673521 --- /dev/null +++ b/src/handlers/typebox/reference/type-reference-base-handler.ts @@ -0,0 +1,42 @@ +import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { Node, SyntaxKind, TypeReferenceNode } from 'ts-morph' + +export abstract class TypeReferenceBaseHandler extends BaseTypeHandler { + protected abstract readonly supportedTypeNames: string[] + protected abstract readonly expectedArgumentCount: number + + canHandle(node: Node): boolean { + if (node.getKind() !== SyntaxKind.TypeReference) { + return false + } + + const typeRef = node as TypeReferenceNode + const typeName = typeRef.getTypeName().getText() + + return this.supportedTypeNames.includes(typeName) + } + + protected validateTypeReference(node: Node): TypeReferenceNode { + if (node.getKind() !== SyntaxKind.TypeReference) { + throw new Error(`Expected TypeReference node, got ${node.getKind()}`) + } + + return node as TypeReferenceNode + } + + protected extractTypeArguments(typeRef: TypeReferenceNode): Node[] { + const typeArgs = typeRef.getTypeArguments() + + if (typeArgs.length !== this.expectedArgumentCount) { + throw new Error( + `Expected ${this.expectedArgumentCount} type argument(s), got ${typeArgs.length}`, + ) + } + + return typeArgs + } + + protected getTypeName(typeRef: TypeReferenceNode): string { + return typeRef.getTypeName().getText() + } +} diff --git a/src/handlers/typebox/required-type-handler.ts b/src/handlers/typebox/required-type-handler.ts deleted file mode 100644 index 20513b7..0000000 --- a/src/handlers/typebox/required-type-handler.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' -import { Node, ts } from 'ts-morph' - -export class RequiredTypeHandler extends BaseTypeHandler { - canHandle(typeNode: Node | undefined): boolean { - if (!typeNode) { - return false - } - const typeReferenceNode = typeNode.asKind(ts.SyntaxKind.TypeReference) - if (!typeReferenceNode) { - return false - } - const typeName = typeReferenceNode.getTypeName() - const typeArguments = typeReferenceNode.getTypeArguments() - return typeName.getText() === 'Required' && typeArguments.length === 1 - } - - handle(typeNode: Node | undefined): ts.Expression { - if (!typeNode) { - throw new Error('Type node is undefined.') - } - const typeReferenceNode = typeNode.asKindOrThrow(ts.SyntaxKind.TypeReference) - const typeArguments = typeReferenceNode.getTypeArguments() - - if (typeArguments.length !== 1) { - throw new Error('Required utility type expects exactly one type argument.') - } - - const targetType = typeArguments[0] - const typeboxType = this.getTypeBoxType(targetType) - - if (!typeboxType) { - throw new Error( - `Could not determine TypeBox type for Required argument: ${targetType ? targetType.getText() : 'undefined'}`, - ) - } - - return ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier('Type'), - ts.factory.createIdentifier('Required'), - ), - undefined, - [typeboxType], - ) - } -} diff --git a/src/handlers/typebox/simple-type-handler.ts b/src/handlers/typebox/simple-type-handler.ts index bde0692..e9fbd02 100644 --- a/src/handlers/typebox/simple-type-handler.ts +++ b/src/handlers/typebox/simple-type-handler.ts @@ -26,17 +26,11 @@ const kindToTypeBox: Record = { } export class SimpleTypeHandler extends BaseTypeHandler { - constructor() { - super(() => ts.factory.createIdentifier('')) // getTypeBoxType is not used in SimpleTypeHandler + canHandle(node: Node): boolean { + return node.getKind() in kindToTypeBox } - canHandle(typeNode?: Node): boolean { - const kind = typeNode?.getKind() - return kind !== undefined && kind in kindToTypeBox - } - - handle(typeNode: Node): ts.Expression { - const kind = typeNode?.getKind() ?? SyntaxKind.AnyKeyword - return makeTypeCall(kindToTypeBox[kind as SimpleKinds]) + handle(node: Node): ts.Expression { + return makeTypeCall(kindToTypeBox[node.getKind() as SimpleKinds]) } } diff --git a/src/handlers/typebox/template-literal-type-handler.ts b/src/handlers/typebox/template-literal-type-handler.ts index bd327c9..066c37f 100644 --- a/src/handlers/typebox/template-literal-type-handler.ts +++ b/src/handlers/typebox/template-literal-type-handler.ts @@ -1,25 +1,18 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { TemplateLiteralTypeProcessor } from '@daxserver/validation-schema-codegen/handlers/typebox/template-literal-type-processor' import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' -import { Node, ts } from 'ts-morph' +import { Node, TemplateLiteralTypeNode, ts } from 'ts-morph' export class TemplateLiteralTypeHandler extends BaseTypeHandler { - constructor(getTypeBoxType: (typeNode?: Node) => ts.Expression) { - super(getTypeBoxType) + canHandle(node: Node): boolean { + return Node.isTemplateLiteralTypeNode(node) } - canHandle(typeNode?: Node): boolean { - return Node.isTemplateLiteralTypeNode(typeNode) - } - - handle(typeNode: Node): ts.Expression { - if (!Node.isTemplateLiteralTypeNode(typeNode)) { - return makeTypeCall('Any') - } - + handle(node: TemplateLiteralTypeNode): ts.Expression { const parts: ts.Expression[] = [] // Add the head part (literal string before first substitution) - const head = typeNode.getHead() + const head = node.getHead() const headCompilerNode = head.compilerNode as ts.TemplateHead const headText = headCompilerNode.text if (headText) { @@ -27,52 +20,15 @@ export class TemplateLiteralTypeHandler extends BaseTypeHandler { } // Process template spans (substitutions + following literal parts) - const templateSpans = typeNode.getTemplateSpans() + const templateSpans = node.getTemplateSpans() for (const span of templateSpans) { // Access the compiler node to get type and literal const compilerNode = span.compilerNode as ts.TemplateLiteralTypeSpan // Add the type from the substitution if (compilerNode.type) { - // Handle common type cases directly - const typeKind = compilerNode.type.kind - if (typeKind === ts.SyntaxKind.StringKeyword) { - parts.push(makeTypeCall('String')) - } else if (typeKind === ts.SyntaxKind.NumberKeyword) { - parts.push(makeTypeCall('Number')) - } else if (typeKind === ts.SyntaxKind.LiteralType) { - // Handle literal types (e.g., 'A', 42, true) - const literalType = compilerNode.type as ts.LiteralTypeNode - if (ts.isStringLiteral(literalType.literal)) { - parts.push( - makeTypeCall('Literal', [ts.factory.createStringLiteral(literalType.literal.text)]), - ) - } else if (ts.isNumericLiteral(literalType.literal)) { - parts.push( - makeTypeCall('Literal', [ts.factory.createNumericLiteral(literalType.literal.text)]), - ) - } else { - parts.push(makeTypeCall('String')) // fallback for other literals - } - } else if (typeKind === ts.SyntaxKind.UnionType) { - // For union types, we need to handle each type in the union - const unionType = compilerNode.type as ts.UnionTypeNode - const unionParts = unionType.types.map((t) => { - if (t.kind === ts.SyntaxKind.LiteralType) { - const literalType = t as ts.LiteralTypeNode - if (ts.isStringLiteral(literalType.literal)) { - return makeTypeCall('Literal', [ - ts.factory.createStringLiteral(literalType.literal.text), - ]) - } - } - return makeTypeCall('String') // fallback - }) - parts.push(makeTypeCall('Union', [ts.factory.createArrayLiteralExpression(unionParts)])) - } else { - // Fallback for other types - parts.push(makeTypeCall('String')) - } + const processedType = TemplateLiteralTypeProcessor.processType(compilerNode.type) + parts.push(processedType) } // Add the literal part after the substitution diff --git a/src/handlers/typebox/template-literal-type-processor.ts b/src/handlers/typebox/template-literal-type-processor.ts new file mode 100644 index 0000000..fbf4cca --- /dev/null +++ b/src/handlers/typebox/template-literal-type-processor.ts @@ -0,0 +1,58 @@ +import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' +import { ts } from 'ts-morph' + +/** + * Helper class to process different types within template literals + */ +export class TemplateLiteralTypeProcessor { + /** + * Process a TypeScript type node and convert it to a TypeBox expression + */ + static processType(node: ts.TypeNode): ts.Expression { + switch (node.kind) { + case ts.SyntaxKind.StringKeyword: + return makeTypeCall('String') + case ts.SyntaxKind.NumberKeyword: + return makeTypeCall('Number') + case ts.SyntaxKind.LiteralType: + return this.processLiteralType(node as ts.LiteralTypeNode) + case ts.SyntaxKind.UnionType: + return this.processUnionType(node as ts.UnionTypeNode) + default: + return makeTypeCall('String') + } + } + + /** + * Process literal type nodes (string, number, boolean literals) + */ + private static processLiteralType(literalType: ts.LiteralTypeNode): ts.Expression { + if (ts.isStringLiteral(literalType.literal)) { + return makeTypeCall('Literal', [ts.factory.createStringLiteral(literalType.literal.text)]) + } + + if (ts.isNumericLiteral(literalType.literal)) { + return makeTypeCall('Literal', [ts.factory.createNumericLiteral(literalType.literal.text)]) + } + + // Fallback for other literals (boolean, etc.) + return makeTypeCall('String') + } + + /** + * Process union type nodes + */ + private static processUnionType(unionType: ts.UnionTypeNode): ts.Expression { + const unionParts = unionType.types.map((t) => { + if (t.kind === ts.SyntaxKind.LiteralType) { + const literalType = t as ts.LiteralTypeNode + if (ts.isStringLiteral(literalType.literal)) { + return makeTypeCall('Literal', [ts.factory.createStringLiteral(literalType.literal.text)]) + } + } + return makeTypeCall('String') // fallback + }) + + return makeTypeCall('Union', [ts.factory.createArrayLiteralExpression(unionParts)]) + } +} diff --git a/src/handlers/typebox/tuple-type-handler.ts b/src/handlers/typebox/tuple-type-handler.ts deleted file mode 100644 index f17cc74..0000000 --- a/src/handlers/typebox/tuple-type-handler.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' -import { Node, ts } from 'ts-morph' - -export class TupleTypeHandler extends BaseTypeHandler { - constructor(getTypeBoxType: (typeNode?: Node) => ts.Expression) { - super(getTypeBoxType) - } - - canHandle(typeNode?: Node): boolean { - return Node.isTupleTypeNode(typeNode) - } - - handle(typeNode: Node): ts.Expression { - if (!Node.isTupleTypeNode(typeNode)) { - return makeTypeCall('Any') - } - const tupleTypes = typeNode.getElements().map(this.getTypeBoxType) - return makeTypeCall('Tuple', [ts.factory.createArrayLiteralExpression(tupleTypes)]) - } -} diff --git a/src/handlers/typebox/type-operator-handler.ts b/src/handlers/typebox/type-operator-handler.ts index 19beedd..3b1436c 100644 --- a/src/handlers/typebox/type-operator-handler.ts +++ b/src/handlers/typebox/type-operator-handler.ts @@ -1,26 +1,16 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' -import { Node, SyntaxKind, ts } from 'ts-morph' +import { Node, ts, TypeOperatorTypeNode } from 'ts-morph' export class TypeOperatorHandler extends BaseTypeHandler { - constructor(getTypeBoxType: (typeNode?: Node) => ts.Expression) { - super(getTypeBoxType) + canHandle(node: Node): boolean { + // This handler now serves as a fallback for unhandled type operators + // Specific operators like KeyOf and Readonly have their own handlers + return Node.isTypeOperatorTypeNode(node) } - canHandle(typeNode?: Node): boolean { - return Node.isTypeOperatorTypeNode(typeNode) - } - - handle(typeNode: Node): ts.Expression { - if (!Node.isTypeOperatorTypeNode(typeNode)) { - return makeTypeCall('Any') - } - if (typeNode.getOperator() === SyntaxKind.KeyOfKeyword) { - const operandType = typeNode.getTypeNode() - const typeboxOperand = this.getTypeBoxType(operandType) - return makeTypeCall('KeyOf', [typeboxOperand]) - } - - return makeTypeCall('Any') + handle(node: TypeOperatorTypeNode): ts.Expression { + // Fallback for any unhandled type operators + throw new Error(`Unhandled type operator: ${node.getOperator()}`) + // return makeTypeCall('Any') } } diff --git a/src/handlers/typebox/type-query-handler.ts b/src/handlers/typebox/type-query-handler.ts index 4f4e754..ed7e6f7 100644 --- a/src/handlers/typebox/type-query-handler.ts +++ b/src/handlers/typebox/type-query-handler.ts @@ -1,25 +1,17 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' -import { Node, ts } from 'ts-morph' +import { Node, ts, TypeQueryNode } from 'ts-morph' export class TypeQueryHandler extends BaseTypeHandler { - constructor(getTypeBoxType: (typeNode?: Node) => ts.Expression) { - super(getTypeBoxType) + canHandle(node: Node): boolean { + return Node.isTypeQuery(node) } - canHandle(typeNode?: Node): boolean { - return Node.isTypeQuery(typeNode) - } - - handle(typeNode: Node): ts.Expression { - if (!Node.isTypeQuery(typeNode)) { - return makeTypeCall('Any') - } - + handle(node: TypeQueryNode): ts.Expression { // For typeof expressions, we'll return the referenced type name // This is a simplified approach - in a more complete implementation, // we might want to resolve the actual type of the referenced entity - const exprName = typeNode.getExprName() + const exprName = node.getExprName() if (Node.isIdentifier(exprName)) { const typeName = exprName.getText() diff --git a/src/handlers/typebox/type-reference-handler.ts b/src/handlers/typebox/type-reference-handler.ts index b6d2e79..e264b24 100644 --- a/src/handlers/typebox/type-reference-handler.ts +++ b/src/handlers/typebox/type-reference-handler.ts @@ -1,25 +1,20 @@ import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' -import { Node, ts } from 'ts-morph' +import { Node, ts, TypeReferenceNode } from 'ts-morph' export class TypeReferenceHandler extends BaseTypeHandler { - constructor(getTypeBoxType: (typeNode?: Node) => ts.Expression) { - super(getTypeBoxType) + canHandle(node: Node): boolean { + return Node.isTypeReference(node) } - canHandle(typeNode?: Node): boolean { - return Node.isTypeReference(typeNode) - } + handle(node: TypeReferenceNode): ts.Expression { + const referencedType = node.getTypeName() - handle(typeNode: Node): ts.Expression { - if (!Node.isTypeReference(typeNode)) { - return makeTypeCall('Any') - } - const referencedType = typeNode.getTypeName() if (Node.isIdentifier(referencedType)) { const typeName = referencedType.getText() return ts.factory.createIdentifier(typeName) } + return makeTypeCall('Any') } } diff --git a/src/handlers/typebox/typebox-type-handlers.ts b/src/handlers/typebox/typebox-type-handlers.ts index 3a72888..b5486a8 100644 --- a/src/handlers/typebox/typebox-type-handlers.ts +++ b/src/handlers/typebox/typebox-type-handlers.ts @@ -1,24 +1,26 @@ -import { ArrayTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/array-type-handler' import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { ArrayTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/collection/array-type-handler' +import { IntersectionTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/collection/intersection-type-handler' +import { TupleTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/collection/tuple-type-handler' +import { UnionTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/collection/union-type-handler' import { FunctionTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/function-type-handler' import { IndexedAccessTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/indexed-access-type-handler' -import { InterfaceTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/interface-type-handler' -import { IntersectionTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/intersection-type-handler' +import { KeyOfTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/keyof-type-handler' import { LiteralTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/literal-type-handler' -import { ObjectTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/object-type-handler' -import { OmitTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/omit-type-handler' -import { PartialTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/partial-type-handler' -import { PickTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/pick-type-handler' -import { RecordTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/record-type-handler' -import { RequiredTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/required-type-handler' +import { InterfaceTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/object/interface-type-handler' +import { ObjectTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/object/object-type-handler' +import { ReadonlyTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/readonly-type-handler' +import { OmitTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/omit-type-handler' +import { PartialTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/partial-type-handler' +import { PickTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/pick-type-handler' +import { RecordTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/record-type-handler' +import { RequiredTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/reference/required-type-handler' import { SimpleTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/simple-type-handler' import { TemplateLiteralTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/template-literal-type-handler' -import { TupleTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/tuple-type-handler' -import { TypeOperatorHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/type-operator-handler' import { TypeQueryHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/type-query-handler' import { TypeReferenceHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/type-reference-handler' -import { UnionTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/union-type-handler' -import { Node, SyntaxKind, ts } from 'ts-morph' +import { TypeofTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/typeof-type-handler' +import { Node, SyntaxKind } from 'ts-morph' export class TypeBoxTypeHandlers { private syntaxKindHandlers = new Map() @@ -26,26 +28,28 @@ export class TypeBoxTypeHandlers { private fallbackHandlers: BaseTypeHandler[] private handlerCache = new Map() - constructor(getTypeBoxType: (typeNode?: Node) => ts.Expression) { + constructor() { const simpleTypeHandler = new SimpleTypeHandler() const literalTypeHandler = new LiteralTypeHandler() - const objectTypeHandler = new ObjectTypeHandler(getTypeBoxType) - const arrayTypeHandler = new ArrayTypeHandler(getTypeBoxType) - const tupleTypeHandler = new TupleTypeHandler(getTypeBoxType) - const unionTypeHandler = new UnionTypeHandler(getTypeBoxType) - const intersectionTypeHandler = new IntersectionTypeHandler(getTypeBoxType) - const recordTypeHandler = new RecordTypeHandler(getTypeBoxType) - const partialTypeHandler = new PartialTypeHandler(getTypeBoxType) - const pickTypeHandler = new PickTypeHandler(getTypeBoxType) - const omitTypeHandler = new OmitTypeHandler(getTypeBoxType) - const requiredTypeHandler = new RequiredTypeHandler(getTypeBoxType) - const typeReferenceHandler = new TypeReferenceHandler(getTypeBoxType) - const typeOperatorHandler = new TypeOperatorHandler(getTypeBoxType) - const indexedAccessTypeHandler = new IndexedAccessTypeHandler(getTypeBoxType) - const interfaceTypeHandler = new InterfaceTypeHandler(getTypeBoxType) - const functionTypeHandler = new FunctionTypeHandler(getTypeBoxType) - const typeQueryHandler = new TypeQueryHandler(getTypeBoxType) - const templateLiteralTypeHandler = new TemplateLiteralTypeHandler(getTypeBoxType) + const objectTypeHandler = new ObjectTypeHandler() + const arrayTypeHandler = new ArrayTypeHandler() + const tupleTypeHandler = new TupleTypeHandler() + const unionTypeHandler = new UnionTypeHandler() + const intersectionTypeHandler = new IntersectionTypeHandler() + const recordTypeHandler = new RecordTypeHandler() + const partialTypeHandler = new PartialTypeHandler() + const pickTypeHandler = new PickTypeHandler() + const omitTypeHandler = new OmitTypeHandler() + const requiredTypeHandler = new RequiredTypeHandler() + const typeReferenceHandler = new TypeReferenceHandler() + const keyOfTypeHandler = new KeyOfTypeHandler() + const indexedAccessTypeHandler = new IndexedAccessTypeHandler() + const interfaceTypeHandler = new InterfaceTypeHandler() + const functionTypeHandler = new FunctionTypeHandler() + const typeQueryHandler = new TypeQueryHandler() + const templateLiteralTypeHandler = new TemplateLiteralTypeHandler() + const typeofTypeHandler = new TypeofTypeHandler() + const readonlyTypeHandler = new ReadonlyTypeHandler() // O(1) lookup by SyntaxKind this.syntaxKindHandlers.set(SyntaxKind.AnyKeyword, simpleTypeHandler) @@ -64,7 +68,7 @@ export class TypeBoxTypeHandlers { this.syntaxKindHandlers.set(SyntaxKind.TupleType, tupleTypeHandler) this.syntaxKindHandlers.set(SyntaxKind.UnionType, unionTypeHandler) this.syntaxKindHandlers.set(SyntaxKind.IntersectionType, intersectionTypeHandler) - this.syntaxKindHandlers.set(SyntaxKind.TypeOperator, typeOperatorHandler) + // TypeOperator handling moved to fallback handlers for specific operator types this.syntaxKindHandlers.set(SyntaxKind.IndexedAccessType, indexedAccessTypeHandler) this.syntaxKindHandlers.set(SyntaxKind.InterfaceDeclaration, interfaceTypeHandler) this.syntaxKindHandlers.set(SyntaxKind.FunctionType, functionTypeHandler) @@ -79,16 +83,19 @@ export class TypeBoxTypeHandlers { this.typeReferenceHandlers.set('Required', requiredTypeHandler) // Fallback handlers for complex cases - this.fallbackHandlers = [typeReferenceHandler] + this.fallbackHandlers = [ + typeReferenceHandler, + keyOfTypeHandler, + typeofTypeHandler, + readonlyTypeHandler, + ] } - public getHandler(typeNode?: Node): BaseTypeHandler | undefined { - if (!typeNode) return undefined - - const nodeKind = typeNode.getKind() + public getHandler(node: Node): BaseTypeHandler { + const nodeKind = node.getKind() // Use stable cache key based on node text and kind - const nodeText = typeNode.getText() + const nodeText = node.getText() const cacheKey = `${nodeKind}-${nodeText}` const cachedHandler = this.handlerCache.get(cacheKey) @@ -102,24 +109,29 @@ export class TypeBoxTypeHandlers { handler = this.syntaxKindHandlers.get(nodeKind) // O(1) lookup for type references by name - if (!handler && nodeKind === SyntaxKind.TypeReference && Node.isTypeReference(typeNode)) { - const typeNameNode = typeNode.getTypeName() + if (!handler && nodeKind === SyntaxKind.TypeReference && Node.isTypeReference(node)) { + const typeNameNode = node.getTypeName() if (Node.isIdentifier(typeNameNode)) { const typeNameText = typeNameNode.getText() handler = this.typeReferenceHandlers.get(typeNameText) } + } - // If no specific utility type handler found, use TypeReferenceHandler as default - if (!handler) { - handler = this.fallbackHandlers[0] // TypeReferenceHandler + // If no handler found yet, check fallback handlers + if (!handler) { + for (const fallbackHandler of this.fallbackHandlers) { + if (fallbackHandler.canHandle(node)) { + handler = fallbackHandler + break + } } } if (handler) { this.handlerCache.set(cacheKey, handler) } else { - throw new Error(`No handler found for type: ${typeNode.getText()}`) + throw new Error(`No handler found for type: ${node.getText()}`) } return handler diff --git a/src/handlers/typebox/typeof-type-handler.ts b/src/handlers/typebox/typeof-type-handler.ts new file mode 100644 index 0000000..16a32eb --- /dev/null +++ b/src/handlers/typebox/typeof-type-handler.ts @@ -0,0 +1,15 @@ +import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' +import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' +import { Node, ts } from 'ts-morph' + +export class TypeofTypeHandler extends BaseTypeHandler { + canHandle(node: Node): boolean { + return Node.isTypeQuery(node) + } + + handle(): ts.Expression { + // TypeQuery represents 'typeof' expressions in TypeScript + // For TypeBox, we'll return a String type as typeof returns string literals + return makeTypeCall('String') + } +} diff --git a/src/handlers/typebox/union-type-handler.ts b/src/handlers/typebox/union-type-handler.ts deleted file mode 100644 index eaac0b4..0000000 --- a/src/handlers/typebox/union-type-handler.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { BaseTypeHandler } from '@daxserver/validation-schema-codegen/handlers/typebox/base-type-handler' -import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' -import { Node, ts } from 'ts-morph' - -export class UnionTypeHandler extends BaseTypeHandler { - handledType = ts.SyntaxKind.UnionType - constructor(getTypeBoxType: (typeNode?: Node) => ts.Expression) { - super(getTypeBoxType) - } - - canHandle(typeNode?: Node): boolean { - return Node.isUnionTypeNode(typeNode) - } - - handle(typeNode: Node): ts.Expression { - if (!Node.isUnionTypeNode(typeNode)) { - return makeTypeCall('Any') - } - const unionTypes = typeNode.getTypeNodes().map(this.getTypeBoxType) - return makeTypeCall('Union', [ts.factory.createArrayLiteralExpression(unionTypes)]) - } -} diff --git a/src/parsers/parse-function-declarations.ts b/src/parsers/parse-function-declarations.ts index dbce573..0407a3a 100644 --- a/src/parsers/parse-function-declarations.ts +++ b/src/parsers/parse-function-declarations.ts @@ -1,6 +1,7 @@ import { BaseParser } from '@daxserver/validation-schema-codegen/parsers/base-parser' import { addStaticTypeAlias } from '@daxserver/validation-schema-codegen/utils/add-static-type-alias' import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' +import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' import { FunctionDeclaration, ts, VariableDeclarationKind } from 'ts-morph' export class FunctionDeclarationParser extends BaseParser { @@ -33,7 +34,7 @@ export class FunctionDeclarationParser extends BaseParser { // Convert parameters to TypeBox types const parameterTypes = parameters.map((param) => { const paramTypeNode = param.getTypeNode() - const paramType = getTypeBoxType(paramTypeNode) + const paramType = paramTypeNode ? getTypeBoxType(paramTypeNode) : makeTypeCall('Any') // Check if parameter is optional if (param.hasQuestionToken()) { @@ -51,7 +52,7 @@ export class FunctionDeclarationParser extends BaseParser { }) // Convert return type to TypeBox type - const returnTypeBox = getTypeBoxType(returnType) + const returnTypeBox = returnType ? getTypeBoxType(returnType) : makeTypeCall('Any') // Create TypeBox Function call with parameters array and return type const typeboxExpression = ts.factory.createCallExpression( diff --git a/src/parsers/parse-type-aliases.ts b/src/parsers/parse-type-aliases.ts index b0c4ef9..0a42573 100644 --- a/src/parsers/parse-type-aliases.ts +++ b/src/parsers/parse-type-aliases.ts @@ -1,6 +1,7 @@ import { BaseParser } from '@daxserver/validation-schema-codegen/parsers/base-parser' import { addStaticTypeAlias } from '@daxserver/validation-schema-codegen/utils/add-static-type-alias' import { getTypeBoxType } from '@daxserver/validation-schema-codegen/utils/typebox-call' +import { makeTypeCall } from '@daxserver/validation-schema-codegen/utils/typebox-codegen-utils' import { ts, TypeAliasDeclaration, VariableDeclarationKind } from 'ts-morph' export class TypeAliasParser extends BaseParser { @@ -18,7 +19,7 @@ export class TypeAliasParser extends BaseParser { this.processedTypes.add(typeName) const typeNode = typeAlias.getTypeNode() - const typeboxTypeNode = getTypeBoxType(typeNode) + const typeboxTypeNode = typeNode ? getTypeBoxType(typeNode) : makeTypeCall('Any') const typeboxType = this.printer.printNode( ts.EmitHint.Expression, typeboxTypeNode, diff --git a/src/utils/typebox-call.ts b/src/utils/typebox-call.ts index 68601b6..5de712c 100644 --- a/src/utils/typebox-call.ts +++ b/src/utils/typebox-call.ts @@ -6,13 +6,16 @@ export const TypeBoxStatic = 'Static' let handlers: TypeBoxTypeHandlers | null = null -export const getTypeBoxType = (typeNode?: Node): ts.Expression => { - if (!handlers) { - handlers = new TypeBoxTypeHandlers(getTypeBoxType) +export const getTypeBoxType = (node?: Node): ts.Expression => { + if (!node) { + return makeTypeCall('Any') } - const handler = handlers.getHandler(typeNode) - if (handler) { - return handler.handle(typeNode) + + if (!handlers) { + handlers = new TypeBoxTypeHandlers() } - return makeTypeCall('Any') + + const handler = handlers.getHandler(node) + + return handler ? handler.handle(node) : makeTypeCall('Any') }