Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { generateRoutes } from './module/generate-routes';
import { generateSpec } from './module/generate-spec';
import { fsExists, fsReadFile } from './utils/fs';
import { AbstractRouteGenerator } from './routeGeneration/routeGenerator';
import { extname,isAbsolute } from 'node:path';
import { extname, isAbsolute } from 'node:path';
import type { CompilerOptions } from 'typescript';

const workingDir: string = process.cwd();
Expand Down Expand Up @@ -54,7 +54,7 @@ const isJsExtension = (extension: string): boolean => extension === '.js' || ext
const getConfig = async (configPath = 'tsoa.json'): Promise<Config> => {
let config: Config;
const ext = extname(configPath);
const configFullPath = isAbsolute(configPath) ? configPath : `${workingDir}/${configPath}`
const configFullPath = isAbsolute(configPath) ? configPath : `${workingDir}/${configPath}`;
try {
if (isYamlExtension(ext)) {
const configRaw = await fsReadFile(configFullPath);
Expand Down Expand Up @@ -114,8 +114,9 @@ export const validateSpecConfig = async (config: Config): Promise<ExtendedSpecCo
config.spec.version = config.spec.version || (await versionDefault());

config.spec.specVersion = config.spec.specVersion || 2;
if (config.spec.specVersion !== 2 && config.spec.specVersion !== 3) {
throw new Error('Unsupported Spec version.');
const supportedVersions = [2, 3, 3.1];
if (!supportedVersions.includes(config.spec.specVersion)) {
throw new Error(`Unsupported Spec version: ${config.spec.specVersion}.`);
}

if (config.spec.spec && !['immediate', 'recursive', 'deepmerge', undefined].includes(config.spec.specMerging)) {
Expand Down
31 changes: 31 additions & 0 deletions packages/cli/src/metadataGeneration/typeResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ export class TypeResolver {
return arrayMetaType;
}

if (ts.isRestTypeNode(this.typeNode)) {
return new TypeResolver(this.typeNode.type, this.current, this.parentNode, this.context).resolve();
}

if (ts.isUnionTypeNode(this.typeNode)) {
const types = this.typeNode.types.map(type => {
return new TypeResolver(type, this.current, this.parentNode, this.context).resolve();
Expand All @@ -87,6 +91,33 @@ export class TypeResolver {
return intersectionMetaType;
}

if (ts.isTupleTypeNode(this.typeNode)) {
const elementTypes: Tsoa.Type[] = [];
let restType: Tsoa.Type | undefined;

for (const element of this.typeNode.elements) {
if (ts.isRestTypeNode(element)) {
const resolvedRest = new TypeResolver(element.type, this.current, element, this.context).resolve();

if (resolvedRest.dataType === 'array') {
restType = resolvedRest.elementType;
} else {
restType = resolvedRest;
}
} else {
const typeNode = ts.isNamedTupleMember(element) ? element.type : element;
const type = new TypeResolver(typeNode, this.current, element, this.context).resolve();
elementTypes.push(type);
}
}

return {
dataType: 'tuple',
types: elementTypes,
...(restType ? { restType } : {}),
};
}

if (this.typeNode.kind === ts.SyntaxKind.AnyKeyword || this.typeNode.kind === ts.SyntaxKind.UnknownKeyword) {
const literallyAny: Tsoa.AnyType = {
dataType: 'any',
Expand Down
11 changes: 9 additions & 2 deletions packages/cli/src/module/generate-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MetadataGenerator } from '../metadataGeneration/metadataGenerator';
import { Tsoa, Swagger, Config } from '@tsoa/runtime';
import { SpecGenerator2 } from '../swagger/specGenerator2';
import { SpecGenerator3 } from '../swagger/specGenerator3';
import { SpecGenerator31 } from '../swagger/specGenerator31';
import { fsMkDir, fsWriteFile } from '../utils/fs';

export const getSwaggerOutputPath = (swaggerConfig: ExtendedSpecConfig) => {
Expand All @@ -29,8 +30,14 @@ export const generateSpec = async (
}

let spec: Swagger.Spec;
if (swaggerConfig.specVersion && swaggerConfig.specVersion === 3) {
spec = new SpecGenerator3(metadata, swaggerConfig).GetSpec();
if (swaggerConfig.specVersion) {
if (swaggerConfig.specVersion === 3) {
spec = new SpecGenerator3(metadata, swaggerConfig).GetSpec();
} else if (swaggerConfig.specVersion === 3.1) {
spec = new SpecGenerator31(metadata, swaggerConfig).GetSpec();
} else {
spec = new SpecGenerator2(metadata, swaggerConfig).GetSpec();
}
} else {
spec = new SpecGenerator2(metadata, swaggerConfig).GetSpec();
}
Expand Down
25 changes: 16 additions & 9 deletions packages/cli/src/swagger/specGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { Tsoa, assertNever, Swagger } from '@tsoa/runtime';
import * as handlebars from 'handlebars';

export abstract class SpecGenerator {
constructor(protected readonly metadata: Tsoa.Metadata, protected readonly config: ExtendedSpecConfig) {}
constructor(
protected readonly metadata: Tsoa.Metadata,
protected readonly config: ExtendedSpecConfig,
) {}

protected buildAdditionalProperties(type: Tsoa.Type) {
return this.getSwaggerType(type);
Expand Down Expand Up @@ -59,7 +62,7 @@ export abstract class SpecGenerator {
}
}

protected getSwaggerType(type: Tsoa.Type, title?: string): Swagger.Schema | Swagger.BaseSchema {
protected getSwaggerType(type: Tsoa.Type, title?: string): Swagger.BaseSchema {
if (type.dataType === 'void' || type.dataType === 'undefined') {
return this.getSwaggerTypeForVoid(type.dataType);
} else if (type.dataType === 'refEnum' || type.dataType === 'refObject' || type.dataType === 'refAlias') {
Expand Down Expand Up @@ -91,18 +94,22 @@ export abstract class SpecGenerator {
return this.getSwaggerTypeForIntersectionType(type, title);
} else if (type.dataType === 'nestedObjectLiteral') {
return this.getSwaggerTypeForObjectLiteral(type, title);
} else if (type.dataType === 'tuple') {
throw new Error('Tuple types are only supported in OpenAPI 3.1+');
} else {
return assertNever(type);
}
}

protected abstract getSwaggerTypeForUnionType(type: Tsoa.UnionType, title?: string): Swagger.Schema | Swagger.BaseSchema;
protected abstract getSwaggerTypeForUnionType(type: Tsoa.UnionType, title?: string): Swagger.BaseSchema;

protected abstract getSwaggerTypeForIntersectionType(type: Tsoa.IntersectionType, title?: string): Swagger.Schema | Swagger.BaseSchema;
protected abstract getSwaggerTypeForIntersectionType(type: Tsoa.IntersectionType, title?: string): Swagger.BaseSchema;

protected abstract buildProperties(properties: Tsoa.Property[]): { [propertyName: string]: Swagger.Schema | Swagger.Schema3 };
protected abstract buildProperties(
properties: Tsoa.Property[],
): { [propertyName: string]: Swagger.Schema2 } | { [propertyName: string]: Swagger.Schema3 } | { [propertyName: string]: Swagger.Schema31 };

public getSwaggerTypeForObjectLiteral(objectLiteral: Tsoa.NestedObjectLiteralType, title?: string): Swagger.Schema {
public getSwaggerTypeForObjectLiteral(objectLiteral: Tsoa.NestedObjectLiteralType, title?: string): Swagger.BaseSchema {
const properties = this.buildProperties(objectLiteral.properties);

const additionalProperties = objectLiteral.additionalProperties && this.getSwaggerType(objectLiteral.additionalProperties);
Expand Down Expand Up @@ -146,7 +153,7 @@ export abstract class SpecGenerator {
}
};

protected getSwaggerTypeForPrimitiveType(dataType: Tsoa.PrimitiveTypeLiteral): Swagger.Schema {
protected getSwaggerTypeForPrimitiveType(dataType: Tsoa.PrimitiveTypeLiteral): Swagger.BaseSchema {
if (dataType === 'object') {
if (process.env.NODE_ENV !== 'tsoa_test') {
// eslint-disable-next-line no-console
Expand All @@ -162,7 +169,7 @@ export abstract class SpecGenerator {
}
}

const map: Record<Tsoa.PrimitiveTypeLiteral, Swagger.Schema> = {
const map: Record<Tsoa.PrimitiveTypeLiteral, Swagger.BaseSchema> = {
any: {
// While the any type is discouraged, it does explicitly allows anything, so it should always allow additionalProperties
additionalProperties: true,
Expand All @@ -188,7 +195,7 @@ export abstract class SpecGenerator {
return map[dataType];
}

protected getSwaggerTypeForArrayType(arrayType: Tsoa.ArrayType, title?: string): Swagger.Schema {
protected getSwaggerTypeForArrayType(arrayType: Tsoa.ArrayType, title?: string): Swagger.BaseSchema {
return {
items: this.getSwaggerType(arrayType.elementType, title),
type: 'array',
Expand Down
32 changes: 14 additions & 18 deletions packages/cli/src/swagger/specGenerator2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export class SpecGenerator2 extends SpecGenerator {
if (res.produces) {
produces.push(...res.produces);
}
swaggerResponses[res.name].schema = this.getSwaggerType(res.schema) as Swagger.Schema;
swaggerResponses[res.name].schema = this.getSwaggerType(res.schema) as Swagger.Schema2;
}
if (res.examples && res.examples[0]) {
if ((res.exampleLabels?.filter(e => e).length || 0) > 0) {
Expand Down Expand Up @@ -307,7 +307,7 @@ export class SpecGenerator2 extends SpecGenerator {
title: `${this.getOperationId(controllerName, method)}Body`,
type: 'object',
},
} as Swagger.Parameter;
} as Swagger.Parameter2;
if (required.length) {
parameter.schema.required = required;
}
Expand All @@ -324,17 +324,6 @@ export class SpecGenerator2 extends SpecGenerator {
}

private buildParameter(source: Tsoa.Parameter): Swagger.Parameter2 {
let parameter = {
default: source.default,
description: source.description,
in: source.in,
name: source.name,
required: this.isRequiredWithoutDefault(source),
} as Swagger.Parameter2;
if (source.deprecated) {
parameter['x-deprecated'] = true;
}

let type = source.type;

if (source.in !== 'body' && source.type.dataType === 'refEnum') {
Expand All @@ -348,16 +337,23 @@ export class SpecGenerator2 extends SpecGenerator {
}

const parameterType = this.getSwaggerType(type);
if (parameterType.format) {
parameter.format = this.throwIfNotDataFormat(parameterType.format);
}

let parameter = {
default: source.default,
description: source.description,
in: source.in,
name: source.name,
required: this.isRequiredWithoutDefault(source),
...(source.deprecated ? { 'x-deprecated': true } : {}),
...(parameterType.$ref ? { schema: parameterType } : {}),
...(parameterType.format ? { format: this.throwIfNotDataFormat(parameterType.format) } : {}),
} as Swagger.Parameter2;

if (Swagger.isQueryParameter(parameter) && parameterType.type === 'array') {
parameter.collectionFormat = 'multi';
}

if (parameterType.$ref) {
parameter.schema = parameterType as Swagger.Schema2;
if (parameter.schema) {
return parameter;
}

Expand Down
10 changes: 5 additions & 5 deletions packages/cli/src/swagger/specGenerator3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ export class SpecGenerator3 extends SpecGenerator {
}

if (parameterType.$ref) {
parameter.schema = parameterType as Swagger.Schema;
parameter.schema = parameterType as Swagger.Schema3;
return parameter;
}

Expand Down Expand Up @@ -581,7 +581,7 @@ export class SpecGenerator3 extends SpecGenerator {
return { $ref: `#/components/schemas/${encodeURIComponent(referenceType.refName)}` };
}

protected getSwaggerTypeForPrimitiveType(dataType: Tsoa.PrimitiveTypeLiteral): Swagger.Schema {
protected getSwaggerTypeForPrimitiveType(dataType: Tsoa.PrimitiveTypeLiteral): Swagger.BaseSchema {
if (dataType === 'any') {
// Setting additionalProperties causes issues with code generators for OpenAPI 3
// Therefore, we avoid setting it explicitly (since it's the implicit default already)
Expand All @@ -602,8 +602,8 @@ export class SpecGenerator3 extends SpecGenerator {
// grouping enums is helpful because it makes the spec more readable and it
// bypasses a failure in openapi-generator caused by using anyOf with
// duplicate types.
private groupEnums(types: Array<Swagger.Schema | Swagger.BaseSchema>) {
const returnTypes: Array<Swagger.Schema | Swagger.BaseSchema> = [];
private groupEnums(types: Swagger.BaseSchema[]) {
const returnTypes: Swagger.BaseSchema[] = [];
const enumValuesByType: Record<string, Record<string, boolean | string | number | null>> = {};
for (const type of types) {
if (type.enum && type.type) {
Expand All @@ -630,7 +630,7 @@ export class SpecGenerator3 extends SpecGenerator {
return returnTypes;
}

protected removeDuplicateSwaggerTypes(types: Array<Swagger.Schema | Swagger.BaseSchema>): Array<Swagger.Schema | Swagger.BaseSchema> {
protected removeDuplicateSwaggerTypes(types: Swagger.BaseSchema[]): Swagger.BaseSchema[] {
if (types.length === 1) {
return types;
} else {
Expand Down
Loading
Loading