-
Notifications
You must be signed in to change notification settings - Fork 510
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(resolve): split tsoa type logics into transformers
- Loading branch information
1 parent
efdb468
commit 5066a81
Showing
7 changed files
with
464 additions
and
399 deletions.
There are no files selected for viewing
28 changes: 28 additions & 0 deletions
28
packages/cli/src/metadataGeneration/transformer/dateTransformer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
73
packages/cli/src/metadataGeneration/transformer/enumTransformer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
83 changes: 83 additions & 0 deletions
83
packages/cli/src/metadataGeneration/transformer/primitiveTransformer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
117
packages/cli/src/metadataGeneration/transformer/propertyTransformer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
84 changes: 84 additions & 0 deletions
84
packages/cli/src/metadataGeneration/transformer/referenceTransformer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.