Skip to content

Commit

Permalink
refactor(resolve): split tsoa type logics into transformers
Browse files Browse the repository at this point in the history
  • Loading branch information
jackey8616 committed Apr 3, 2024
1 parent efdb468 commit 5066a81
Show file tree
Hide file tree
Showing 7 changed files with 464 additions and 399 deletions.
28 changes: 28 additions & 0 deletions packages/cli/src/metadataGeneration/transformer/dateTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as ts from 'typescript';
import { Tsoa } from '@tsoa/runtime';

import { Transformer } from './transformer';
import { getJSDocTagNames } from '../../utils/jsDocUtils';

export class DateTransformer extends Transformer {
public transform(parentNode?: ts.Node): Tsoa.DateType | Tsoa.DateTimeType {
if (!parentNode) {
return { dataType: 'datetime' };
}
const tags = getJSDocTagNames(parentNode).filter(name => {
return ['isDate', 'isDateTime'].some(m => m === name);
});

if (tags.length === 0) {
return { dataType: 'datetime' };
}
switch (tags[0]) {
case 'isDate':
return { dataType: 'date' };
case 'isDateTime':
return { dataType: 'datetime' };
default:
return { dataType: 'datetime' };
}
}
}
73 changes: 73 additions & 0 deletions packages/cli/src/metadataGeneration/transformer/enumTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as ts from 'typescript';
import { Tsoa } from '@tsoa/runtime';

import { Transformer } from './transformer';
import { isExistJSDocTag } from '../../utils/jsDocUtils';

export class EnumTransformer extends Transformer {
public static mergeMany(many: Tsoa.RefEnumType[]): Tsoa.RefEnumType {
let merged = this.merge(many[0], many[1]);
for (let i = 2; i < many.length; ++i) {
merged = this.merge(merged, many[i]);
}
return merged;
}

public static merge(first: Tsoa.RefEnumType, second: Tsoa.RefEnumType): Tsoa.RefEnumType {
const description = first.description ? (second.description ? `${first.description}\n${second.description}` : first.description) : second.description;

const deprecated = first.deprecated || second.deprecated;

const enums = first.enums ? (second.enums ? [...first.enums, ...second.enums] : first.enums) : second.enums;

const enumVarnames = first.enumVarnames ? (second.enumVarnames ? [...first.enumVarnames, ...second.enumVarnames] : first.enumVarnames) : second.enumVarnames;

return {
dataType: 'refEnum',
description,
enums,
enumVarnames,
refName: first.refName,
deprecated,
};
}

public static transformable(declaration: ts.Node): declaration is ts.EnumDeclaration | ts.EnumMember {
return ts.isEnumDeclaration(declaration) || ts.isEnumMember(declaration);
}

public transform(declaration: ts.EnumDeclaration | ts.EnumMember, enumName: string): Tsoa.RefEnumType {
if (ts.isEnumDeclaration(declaration)) {
return this.transformDeclaration(declaration, enumName);
}
return this.transformMember(declaration, enumName);
}

private transformDeclaration(declaration: ts.EnumDeclaration, enumName: string): Tsoa.RefEnumType {
const enums = declaration.members.map(e => this.resolver.current.typeChecker.getConstantValue(e)).filter(this.isNotUndefined);
const enumVarnames = declaration.members.map(e => e.name.getText()).filter(this.isNotUndefined);

return {
dataType: 'refEnum',
description: this.resolver.getNodeDescription(declaration),
enums,
enumVarnames,
refName: enumName,
deprecated: isExistJSDocTag(declaration, tag => tag.tagName.text === 'deprecated'),
};
}

private transformMember(declaration: ts.EnumMember, enumName: string): Tsoa.RefEnumType {
return {
dataType: 'refEnum',
refName: enumName,
enums: [this.resolver.current.typeChecker.getConstantValue(declaration)!],
enumVarnames: [declaration.name.getText()],
deprecated: isExistJSDocTag(declaration, tag => tag.tagName.text === 'deprecated'),
}
}

private isNotUndefined<T>(item: T): item is Exclude<T, undefined> {
return item === undefined ? false : true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as ts from 'typescript';
import { Tsoa, assertNever } from '@tsoa/runtime';

import { Transformer } from './transformer';
import { getJSDocTagNames } from '../../utils/jsDocUtils';

export class PrimitiveTransformer extends Transformer {
public static attemptToResolveKindToPrimitive(syntaxKind: ts.SyntaxKind): ResolvesToPrimitive | DoesNotResolveToPrimitive {
switch (syntaxKind) {
case ts.SyntaxKind.NumberKeyword:
return { foundMatch: true, resolvedType: 'number' };
case ts.SyntaxKind.StringKeyword:
return { foundMatch: true, resolvedType: 'string' };
case ts.SyntaxKind.BooleanKeyword:
return { foundMatch: true, resolvedType: 'boolean' };
case ts.SyntaxKind.VoidKeyword:
return { foundMatch: true, resolvedType: 'void' };
case ts.SyntaxKind.UndefinedKeyword:
return { foundMatch: true, resolvedType: 'undefined' };
default:
return { foundMatch: false };
}
};

public transform(typeNode: ts.TypeNode, parentNode?: ts.Node): Tsoa.PrimitiveType | undefined {
const resolution = PrimitiveTransformer.attemptToResolveKindToPrimitive(typeNode.kind);
if (!resolution.foundMatch) {
return;
}

const defaultNumberType = this.resolver.current.defaultNumberType;

switch (resolution.resolvedType) {
case 'number':
return this.transformNumber(defaultNumberType, parentNode);
case 'string':
case 'boolean':
case 'void':
case 'undefined':
return { dataType: resolution.resolvedType };
default:
return assertNever(resolution.resolvedType);
}
}

private transformNumber(
defaultNumberType: NonNullable<"double" | "float" | "integer" | "long" | undefined>,
parentNode?: ts.Node,
): Tsoa.PrimitiveType {
if (!parentNode) {
return { dataType: defaultNumberType };
}

const tags = getJSDocTagNames(parentNode).filter(name => {
return ['isInt', 'isLong', 'isFloat', 'isDouble'].some(m => m === name);
});
if (tags.length === 0) {
return { dataType: defaultNumberType };
}

switch (tags[0]) {
case 'isInt':
return { dataType: 'integer' };
case 'isLong':
return { dataType: 'long' };
case 'isFloat':
return { dataType: 'float' };
case 'isDouble':
return { dataType: 'double' };
default:
return { dataType: defaultNumberType };
}
}
}

interface ResolvesToPrimitive {
foundMatch: true;
resolvedType: 'number' | 'string' | 'boolean' | 'void' | 'undefined';
}

interface DoesNotResolveToPrimitive {
foundMatch: false;
}
117 changes: 117 additions & 0 deletions packages/cli/src/metadataGeneration/transformer/propertyTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import * as ts from 'typescript';
import { Tsoa } from '@tsoa/runtime';

import { Transformer } from './transformer';
import { GenerateMetadataError } from '../exceptions';
import { TypeResolver } from '../typeResolver';
import { getInitializerValue } from '../initializer-value';
import { getPropertyValidators } from '../../utils/validatorUtils';
import { isExistJSDocTag } from '../../utils/jsDocUtils';
import { isDecorator } from '../../utils/decoratorUtils';

type OverrideToken = ts.Token<ts.SyntaxKind.QuestionToken> | ts.Token<ts.SyntaxKind.PlusToken> | ts.Token<ts.SyntaxKind.MinusToken> | undefined;

export class PropertyTransformer extends Transformer {
public transform(node: ts.InterfaceDeclaration | ts.ClassDeclaration, overrideToken?: OverrideToken): Tsoa.Property[] {
const isIgnored = (e: ts.TypeElement | ts.ClassElement) => {
let ignore = isExistJSDocTag(e, tag => tag.tagName.text === 'ignore');
ignore = ignore || (e.flags & ts.NodeFlags.ThisNodeHasError) > 0;
return ignore;
};

// Interface model
if (ts.isInterfaceDeclaration(node)) {
return node.members
.filter((member): member is ts.PropertySignature => !isIgnored(member) && ts.isPropertySignature(member))
.map((member: ts.PropertySignature) => this.propertyFromSignature(member, overrideToken));
}

const properties: Array<ts.PropertyDeclaration | ts.ParameterDeclaration> = [];
for (const member of node.members) {
if (!isIgnored(member) && ts.isPropertyDeclaration(member) && !this.hasStaticModifier(member) && this.hasPublicModifier(member)) {
properties.push(member);
}
}

const classConstructor = node.members.find(member => ts.isConstructorDeclaration(member)) as ts.ConstructorDeclaration;

if (classConstructor && classConstructor.parameters) {
const constructorProperties = classConstructor.parameters.filter(parameter => this.isAccessibleParameter(parameter));

properties.push(...constructorProperties);
}

return properties.map(property => this.propertyFromDeclaration(property, overrideToken));
}

private propertyFromSignature(propertySignature: ts.PropertySignature, overrideToken?: OverrideToken): Tsoa.Property {
const identifier = propertySignature.name as ts.Identifier;

if (!propertySignature.type) {
throw new GenerateMetadataError(`No valid type found for property declaration.`);
}

let required = !propertySignature.questionToken;
if (overrideToken && overrideToken.kind === ts.SyntaxKind.MinusToken) {
required = true;
} else if (overrideToken && overrideToken.kind === ts.SyntaxKind.QuestionToken) {
required = false;
}

const def = TypeResolver.getDefault(propertySignature);

const property: Tsoa.Property = {
default: def,
description: this.resolver.getNodeDescription(propertySignature),
example: this.resolver.getNodeExample(propertySignature),
format: this.resolver.getNodeFormat(propertySignature),
name: identifier.text,
required,
type: new TypeResolver(propertySignature.type, this.resolver.current, propertySignature.type.parent, this.resolver.context).resolve(),
validators: getPropertyValidators(propertySignature) || {},
deprecated: isExistJSDocTag(propertySignature, tag => tag.tagName.text === 'deprecated'),
extensions: this.resolver.getNodeExtension(propertySignature),
};
return property;
}

private propertyFromDeclaration(propertyDeclaration: ts.PropertyDeclaration | ts.ParameterDeclaration, overrideToken?: OverrideToken): Tsoa.Property {
const identifier = propertyDeclaration.name as ts.Identifier;
let typeNode = propertyDeclaration.type;

const tsType = this.resolver.current.typeChecker.getTypeAtLocation(propertyDeclaration);

if (!typeNode) {
// Type is from initializer
typeNode = this.resolver.current.typeChecker.typeToTypeNode(tsType, undefined, ts.NodeBuilderFlags.NoTruncation)!;
}

const type = new TypeResolver(typeNode, this.resolver.current, propertyDeclaration, this.resolver.context, tsType).resolve();

let required = !propertyDeclaration.questionToken && !propertyDeclaration.initializer;
if (overrideToken && overrideToken.kind === ts.SyntaxKind.MinusToken) {
required = true;
} else if (overrideToken && overrideToken.kind === ts.SyntaxKind.QuestionToken) {
required = false;
}
let def = getInitializerValue(propertyDeclaration.initializer, this.resolver.current.typeChecker);
if (def === undefined) {
def = TypeResolver.getDefault(propertyDeclaration);
}

const property: Tsoa.Property = {
default: def,
description: this.resolver.getNodeDescription(propertyDeclaration),
example: this.resolver.getNodeExample(propertyDeclaration),
format: this.resolver.getNodeFormat(propertyDeclaration),
name: identifier.text,
required,
type,
validators: getPropertyValidators(propertyDeclaration) || {},
// class properties and constructor parameters may be deprecated either via jsdoc annotation or decorator
deprecated: isExistJSDocTag(propertyDeclaration, tag => tag.tagName.text === 'deprecated') || isDecorator(propertyDeclaration, identifier => identifier.text === 'Deprecated'),
extensions: this.resolver.getNodeExtension(propertyDeclaration),
};
return property;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import * as ts from 'typescript';
import { Tsoa } from '@tsoa/runtime';

import { Transformer } from './transformer';
import { EnumTransformer } from './enumTransformer';
import { TypeResolver } from '../typeResolver';
import { GenerateMetadataError } from '../exceptions';
import { getPropertyValidators } from '../../utils/validatorUtils';

export class ReferenceTransformer extends Transformer {
public static merge(referenceTypes: Tsoa.ReferenceType[]): Tsoa.ReferenceType {
if (referenceTypes.length === 1) {
return referenceTypes[0];
}

if (referenceTypes.every(refType => refType.dataType === 'refEnum')) {
return EnumTransformer.mergeMany(referenceTypes as Tsoa.RefEnumType[]);
}

if (referenceTypes.every(refType => refType.dataType === 'refObject')) {
return this.mergeManyRefObj(referenceTypes as Tsoa.RefObjectType[]);
}

throw new GenerateMetadataError(`These resolved type merge rules are not defined: ${JSON.stringify(referenceTypes)}`);
}

public static mergeManyRefObj(many: Tsoa.RefObjectType[]): Tsoa.RefObjectType {
let merged = this.mergeRefObj(many[0], many[1]);
for (let i = 2; i < many.length; ++i) {
merged = this.mergeRefObj(merged, many[i]);
}
return merged;
}

public static mergeRefObj(first: Tsoa.RefObjectType, second: Tsoa.RefObjectType): Tsoa.RefObjectType {
const description = first.description ? (second.description ? `${first.description}\n${second.description}` : first.description) : second.description;

const deprecated = first.deprecated || second.deprecated;
const example = first.example || second.example;

const properties = [...first.properties, ...second.properties.filter(prop => first.properties.every(firstProp => firstProp.name !== prop.name))];

const mergeAdditionalTypes = (first: Tsoa.Type, second: Tsoa.Type): Tsoa.Type => {
return {
dataType: 'union',
types: [first, second],
};
};

const additionalProperties = first.additionalProperties
? second.additionalProperties
? mergeAdditionalTypes(first.additionalProperties, second.additionalProperties)
: first.additionalProperties
: second.additionalProperties;

const result: Tsoa.RefObjectType = {
dataType: 'refObject',
description,
properties,
additionalProperties,
refName: first.refName,
deprecated,
example,
};

return result;
}

public transform(declaration: ts.TypeAliasDeclaration, refTypeName: string, referencer?: ts.Type): Tsoa.ReferenceType {
const example = this.resolver.getNodeExample(declaration);

const referenceType: Tsoa.ReferenceType = {
dataType: 'refAlias',
default: TypeResolver.getDefault(declaration),
description: this.resolver.getNodeDescription(declaration),
refName: refTypeName,
format: this.resolver.getNodeFormat(declaration),
type: new TypeResolver(declaration.type, this.resolver.current, declaration, this.resolver.context, this.resolver.referencer || referencer).resolve(),
validators: getPropertyValidators(declaration) || {},
...(example && { example }),
};
return referenceType;
}
}
Loading

0 comments on commit 5066a81

Please sign in to comment.