diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 557665335..180d579c2 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -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(); @@ -54,7 +54,7 @@ const isJsExtension = (extension: string): boolean => extension === '.js' || ext const getConfig = async (configPath = 'tsoa.json'): Promise => { 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); @@ -114,8 +114,9 @@ export const validateSpecConfig = async (config: Config): Promise { return new TypeResolver(type, this.current, this.parentNode, this.context).resolve(); @@ -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', diff --git a/packages/cli/src/module/generate-spec.ts b/packages/cli/src/module/generate-spec.ts index 7b808cc8e..4d6952eb1 100644 --- a/packages/cli/src/module/generate-spec.ts +++ b/packages/cli/src/module/generate-spec.ts @@ -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) => { @@ -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(); } diff --git a/packages/cli/src/swagger/specGenerator.ts b/packages/cli/src/swagger/specGenerator.ts index 571515c66..06c3af2c8 100644 --- a/packages/cli/src/swagger/specGenerator.ts +++ b/packages/cli/src/swagger/specGenerator.ts @@ -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); @@ -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') { @@ -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); @@ -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 @@ -162,7 +169,7 @@ export abstract class SpecGenerator { } } - const map: Record = { + const map: Record = { any: { // While the any type is discouraged, it does explicitly allows anything, so it should always allow additionalProperties additionalProperties: true, @@ -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', diff --git a/packages/cli/src/swagger/specGenerator2.ts b/packages/cli/src/swagger/specGenerator2.ts index bd87f18f5..c7d178d36 100644 --- a/packages/cli/src/swagger/specGenerator2.ts +++ b/packages/cli/src/swagger/specGenerator2.ts @@ -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) { @@ -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; } @@ -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') { @@ -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; } diff --git a/packages/cli/src/swagger/specGenerator3.ts b/packages/cli/src/swagger/specGenerator3.ts index 1510cc766..fe9be5e5f 100644 --- a/packages/cli/src/swagger/specGenerator3.ts +++ b/packages/cli/src/swagger/specGenerator3.ts @@ -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; } @@ -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) @@ -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) { - const returnTypes: Array = []; + private groupEnums(types: Swagger.BaseSchema[]) { + const returnTypes: Swagger.BaseSchema[] = []; const enumValuesByType: Record> = {}; for (const type of types) { if (type.enum && type.type) { @@ -630,7 +630,7 @@ export class SpecGenerator3 extends SpecGenerator { return returnTypes; } - protected removeDuplicateSwaggerTypes(types: Array): Array { + protected removeDuplicateSwaggerTypes(types: Swagger.BaseSchema[]): Swagger.BaseSchema[] { if (types.length === 1) { return types; } else { diff --git a/packages/cli/src/swagger/specGenerator31.ts b/packages/cli/src/swagger/specGenerator31.ts new file mode 100644 index 000000000..37f730252 --- /dev/null +++ b/packages/cli/src/swagger/specGenerator31.ts @@ -0,0 +1,767 @@ +import { Swagger, Tsoa, assertNever } from '@tsoa/runtime'; +import { merge as mergeAnything } from 'merge-anything'; +import { merge as deepMerge } from 'ts-deepmerge'; + +import { ExtendedSpecConfig } from '../cli'; +import { isVoidType } from '../utils/isVoidType'; +import { UnspecifiedObject } from '../utils/unspecifiedObject'; +import { shouldIncludeValidatorInSchema } from '../utils/validatorUtils'; +import { convertColonPathParams, normalisePath } from './../utils/pathUtils'; +import { DEFAULT_REQUEST_MEDIA_TYPE, DEFAULT_RESPONSE_MEDIA_TYPE, getValue } from './../utils/swaggerUtils'; +import { SpecGenerator } from './specGenerator'; + +export class SpecGenerator31 extends SpecGenerator { + constructor( + protected readonly metadata: Tsoa.Metadata, + protected readonly config: ExtendedSpecConfig, + ) { + super(metadata, config); + } + + public GetSpec() { + let spec: Swagger.Spec31 = { + openapi: '3.1.0', + components: this.buildComponents(), + info: this.buildInfo(), + paths: this.buildPaths(), + servers: this.buildServers(), + tags: this.config.tags, + }; + + if (this.config.spec) { + this.config.specMerging = this.config.specMerging || 'immediate'; + const mergeFuncs: { [key: string]: (spec: UnspecifiedObject, merge: UnspecifiedObject) => UnspecifiedObject } = { + immediate: Object.assign, + recursive: mergeAnything, + deepmerge: (spec: UnspecifiedObject, merge: UnspecifiedObject): UnspecifiedObject => deepMerge(spec, merge), + }; + + spec = mergeFuncs[this.config.specMerging](spec as unknown as UnspecifiedObject, this.config.spec as UnspecifiedObject) as unknown as Swagger.Spec31; + } + + return spec; + } + + private buildInfo() { + const info: Swagger.Info = { + title: this.config.name || '', + }; + if (this.config.version) { + info.version = this.config.version; + } + if (this.config.description) { + info.description = this.config.description; + } + if (this.config.termsOfService) { + info.termsOfService = this.config.termsOfService; + } + if (this.config.license) { + info.license = { name: this.config.license }; + } + if (this.config.contact) { + info.contact = this.config.contact; + } + + return info; + } + + private buildComponents() { + const components = { + examples: {}, + headers: {}, + parameters: {}, + requestBodies: {}, + responses: {}, + schemas: this.buildSchema(), + securitySchemes: {}, + }; + + if (this.config.securityDefinitions) { + components.securitySchemes = this.translateSecurityDefinitions(this.config.securityDefinitions); + } + + return components; + } + + private translateSecurityDefinitions(definitions: { [name: string]: Swagger.SecuritySchemes }) { + const defs: { [name: string]: Swagger.SecuritySchemes } = {}; + Object.keys(definitions).forEach(key => { + if (definitions[key].type === 'basic') { + defs[key] = { + scheme: 'basic', + type: 'http', + } as Swagger.BasicSecurity3; + } else if (definitions[key].type === 'oauth2') { + /* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ + const definition = definitions[key] as + | Swagger.OAuth2PasswordSecurity + | Swagger.OAuth2ApplicationSecurity + | Swagger.OAuth2ImplicitSecurity + | Swagger.OAuth2AccessCodeSecurity + | Swagger.OAuth2Security3; + const oauth = (defs[key] || { + type: 'oauth2', + description: definitions[key].description, + flows: (this.hasOAuthFlows(definition) && definition.flows) || {}, + }) as Swagger.OAuth2Security3; + + if (this.hasOAuthFlow(definition) && definition.flow === 'password') { + oauth.flows.password = { tokenUrl: definition.tokenUrl, scopes: definition.scopes || {} } as Swagger.OAuth2SecurityFlow3; + } else if (this.hasOAuthFlow(definition) && definition.flow === 'accessCode') { + oauth.flows.authorizationCode = { tokenUrl: definition.tokenUrl, authorizationUrl: definition.authorizationUrl, scopes: definition.scopes || {} } as Swagger.OAuth2SecurityFlow3; + } else if (this.hasOAuthFlow(definition) && definition.flow === 'application') { + oauth.flows.clientCredentials = { tokenUrl: definition.tokenUrl, scopes: definition.scopes || {} } as Swagger.OAuth2SecurityFlow3; + } else if (this.hasOAuthFlow(definition) && definition.flow === 'implicit') { + oauth.flows.implicit = { authorizationUrl: definition.authorizationUrl, scopes: definition.scopes || {} } as Swagger.OAuth2SecurityFlow3; + } + + defs[key] = oauth; + } else { + defs[key] = definitions[key]; + } + }); + return defs; + } + + private hasOAuthFlow(definition: any): definition is { flow: string } { + return !!definition.flow; + } + + private hasOAuthFlows(definition: any): definition is { flows: Swagger.OAuthFlow } { + return !!definition.flows; + } + + private buildServers() { + const basePath = normalisePath(this.config.basePath as string, '/', undefined, false); + const scheme = this.config.schemes ? this.config.schemes[0] : 'https'; + const hosts = this.config.servers ? this.config.servers : this.config.host ? [this.config.host!] : undefined; + const convertHost = (host: string) => ({ url: `${scheme}://${host}${basePath}` }); + return (hosts?.map(convertHost) || [{ url: basePath }]) as Swagger.Server[]; + } + + private buildSchema(): { [name: string]: Swagger.Schema31 } { + const schemas: { [name: string]: Swagger.Schema31 } = {}; + + Object.keys(this.metadata.referenceTypeMap).forEach(typeName => { + const referenceType = this.metadata.referenceTypeMap[typeName]; + + if (referenceType.dataType === 'refObject') { + const required = referenceType.properties.filter(p => this.isRequiredWithoutDefault(p) && !this.hasUndefined(p)).map(p => p.name); + + const schema: Swagger.Schema31 = { + type: 'object', + description: referenceType.description, + properties: this.buildProperties(referenceType.properties), + required: required.length > 0 ? Array.from(new Set(required)) : undefined, + }; + + if (referenceType.additionalProperties) { + schema.additionalProperties = this.buildAdditionalProperties(referenceType.additionalProperties) as Swagger.Schema31; + } else { + schema.additionalProperties = this.determineImplicitAdditionalPropertiesValue(); + } + + if (referenceType.example) { + schema.example = referenceType.example; + } + + if (referenceType.deprecated) { + schema.deprecated = true; + } + + schemas[referenceType.refName] = schema; + } else if (referenceType.dataType === 'refEnum') { + const enumTypes = this.determineTypesUsedInEnum(referenceType.enums); + + if (enumTypes.size === 1) { + schemas[referenceType.refName] = { + type: enumTypes.has('string') ? 'string' : 'number', + enum: referenceType.enums, + description: referenceType.description, + }; + } else { + schemas[referenceType.refName] = { + anyOf: [ + { + type: 'number', + enum: referenceType.enums.filter(e => typeof e === 'number'), + }, + { + type: 'string', + enum: referenceType.enums.filter(e => typeof e === 'string'), + }, + ], + description: referenceType.description, + }; + } + + if (this.config.xEnumVarnames && referenceType.enumVarnames && referenceType.enums.length === referenceType.enumVarnames.length) { + (schemas[referenceType.refName] as any)['x-enum-varnames'] = referenceType.enumVarnames; + } + + if (referenceType.example) { + schemas[referenceType.refName].example = referenceType.example; + } + + if (referenceType.deprecated) { + schemas[referenceType.refName].deprecated = true; + } + } else if (referenceType.dataType === 'refAlias') { + const swaggerType = this.getSwaggerType(referenceType.type) as Swagger.Schema31; + const format = referenceType.format as Swagger.DataFormat; + const validators = Object.keys(referenceType.validators) + .filter(shouldIncludeValidatorInSchema) + .reduce((acc, key) => { + return { + ...acc, + [key]: referenceType.validators[key]!.value, + }; + }, {}); + + schemas[referenceType.refName] = { + ...swaggerType, + default: referenceType.default ?? swaggerType.default, + format: format ?? swaggerType.format, + description: referenceType.description, + example: referenceType.example, + ...validators, + }; + + if (referenceType.deprecated) { + schemas[referenceType.refName].deprecated = true; + } + } else { + assertNever(referenceType); + } + }); + + return schemas; + } + + private buildPaths() { + const paths: { [pathName: string]: Swagger.Path3 } = {}; + + this.metadata.controllers.forEach(controller => { + const normalisedControllerPath = normalisePath(controller.path, '/'); + // construct documentation using all methods except @Hidden + controller.methods + .filter(method => !method.isHidden) + .forEach(method => { + const normalisedMethodPath = normalisePath(method.path, '/'); + let path = normalisePath(`${normalisedControllerPath}${normalisedMethodPath}`, '/', '', false); + path = convertColonPathParams(path); + paths[path] = paths[path] || {}; + this.buildMethod(controller.name, method, paths[path], controller.produces); + }); + }); + + return paths; + } + + private buildMethod(controllerName: string, method: Tsoa.Method, pathObject: any, defaultProduces?: string[]) { + const pathMethod: Swagger.Operation31 = (pathObject[method.method] = this.buildOperation(controllerName, method, defaultProduces)); + pathMethod.description = method.description; + pathMethod.summary = method.summary; + pathMethod.tags = method.tags; + + // Use operationId tag otherwise fallback to generated. Warning: This doesn't check uniqueness. + pathMethod.operationId = method.operationId || pathMethod.operationId; + + if (method.deprecated) { + pathMethod.deprecated = method.deprecated; + } + + if (method.security) { + pathMethod.security = method.security; + } + + const bodyParams: Tsoa.Parameter[] = method.parameters.filter(p => p.in === 'body'); + const bodyPropParams: Tsoa.Parameter[] = method.parameters.filter(p => p.in === 'body-prop'); + const formParams: Tsoa.Parameter[] = method.parameters.filter(p => p.in === 'formData'); + const queriesParams: Tsoa.Parameter[] = method.parameters.filter(p => p.in === 'queries'); + + pathMethod.parameters = method.parameters + .filter(p => { + return ['body', 'formData', 'request', 'body-prop', 'res', 'queries', 'request-prop'].indexOf(p.in) === -1; + }) + .map(p => this.buildParameter(p)); + + if (queriesParams.length > 1) { + throw new Error('Only one queries parameter allowed per controller method.'); + } + + if (queriesParams.length === 1) { + pathMethod.parameters.push(...this.buildQueriesParameter(queriesParams[0])); + } + + if (bodyParams.length > 1) { + throw new Error('Only one body parameter allowed per controller method.'); + } + + if (bodyParams.length > 0 && formParams.length > 0) { + throw new Error('Either body parameter or form parameters allowed per controller method - not both.'); + } + + if (bodyPropParams.length > 0) { + if (!bodyParams.length) { + bodyParams.push({ + in: 'body', + name: 'body', + parameterName: 'body', + required: true, + type: { + dataType: 'nestedObjectLiteral', + properties: [], + } as Tsoa.NestedObjectLiteralType, + validators: {}, + deprecated: false, + }); + } + + const type: Tsoa.NestedObjectLiteralType = bodyParams[0].type as Tsoa.NestedObjectLiteralType; + bodyPropParams.forEach((bodyParam: Tsoa.Parameter) => { + type.properties.push(bodyParam as Tsoa.Property); + }); + } + + if (bodyParams.length > 0) { + pathMethod.requestBody = this.buildRequestBody(controllerName, method, bodyParams[0]); + } else if (formParams.length > 0) { + pathMethod.requestBody = this.buildRequestBodyWithFormData(controllerName, method, formParams); + } + + method.extensions.forEach(ext => (pathMethod[ext.key] = ext.value)); + } + + protected buildOperation(controllerName: string, method: Tsoa.Method, defaultProduces?: string[]): Swagger.Operation31 { + const swaggerResponses: { [name: string]: Swagger.Response31 } = {}; + + method.responses.forEach((res: Tsoa.Response) => { + swaggerResponses[res.name] = { + description: res.description, + }; + + if (res.schema && !isVoidType(res.schema)) { + swaggerResponses[res.name].content = {}; + const produces: string[] = res.produces || defaultProduces || [DEFAULT_RESPONSE_MEDIA_TYPE]; + for (const p of produces) { + const { content } = swaggerResponses[res.name]; + swaggerResponses[res.name].content = { + ...content, + [p]: { + schema: this.getSwaggerType(res.schema, this.config.useTitleTagsForInlineObjects ? this.getOperationId(controllerName, method) + 'Response' : undefined) as Swagger.Schema31, + }, + }; + } + + if (res.examples) { + let exampleCounter = 1; + const examples = res.examples.reduce((acc, ex, currentIndex) => { + const exampleLabel = res.exampleLabels?.[currentIndex]; + return { ...acc, [exampleLabel === undefined ? `Example ${exampleCounter++}` : exampleLabel]: { value: ex } }; + }, {}); + for (const p of produces) { + /* eslint-disable @typescript-eslint/dot-notation */ + (swaggerResponses[res.name].content || {})[p]['examples'] = examples; + } + } + } + + if (res.headers) { + const headers: { [name: string]: Swagger.Header3 } = {}; + if (res.headers.dataType === 'refObject') { + headers[res.headers.refName] = { + schema: this.getSwaggerTypeForReferenceType(res.headers) as Swagger.Schema31, + description: res.headers.description, + }; + } else if (res.headers.dataType === 'nestedObjectLiteral') { + res.headers.properties.forEach((each: Tsoa.Property) => { + headers[each.name] = { + schema: this.getSwaggerType(each.type) as Swagger.Schema31, + description: each.description, + required: this.isRequiredWithoutDefault(each), + }; + }); + } else { + assertNever(res.headers); + } + swaggerResponses[res.name].headers = headers; + } + }); + + const operation: Swagger.Operation31 = { + operationId: this.getOperationId(controllerName, method), + responses: swaggerResponses, + }; + + return operation; + } + + private buildRequestBodyWithFormData(controllerName: string, method: Tsoa.Method, parameters: Tsoa.Parameter[]): Swagger.RequestBody31 { + const required: string[] = []; + const properties: Record = {}; + + for (const parameter of parameters) { + const mediaType = this.buildMediaType(controllerName, method, parameter); + if (!mediaType.schema) continue; + + const schema = { ...mediaType.schema } as Swagger.Schema31; + + if (parameter.deprecated) { + schema.deprecated = true; + } + + properties[parameter.name] = schema; + + if (this.isRequiredWithoutDefault(parameter)) { + required.push(parameter.name); + } + } + + return { + required: required.length > 0, + content: { + 'multipart/form-data': { + schema: { + type: 'object', + properties, + ...(required.length > 0 ? { required } : {}), + }, + }, + }, + }; + } + + private buildRequestBody(controllerName: string, method: Tsoa.Method, parameter: Tsoa.Parameter): Swagger.RequestBody31 { + const mediaType = this.buildMediaType(controllerName, method, parameter); + const consumes = method.consumes || DEFAULT_REQUEST_MEDIA_TYPE; + + return { + description: parameter.description, + required: this.isRequiredWithoutDefault(parameter), + content: { + [consumes]: mediaType, + }, + }; + } + + private buildMediaType(controllerName: string, method: Tsoa.Method, parameter: Tsoa.Parameter): Swagger.MediaType31 { + const validators = Object.keys(parameter.validators) + .filter(shouldIncludeValidatorInSchema) + .reduce((acc, key) => { + return { + ...acc, + [key]: parameter.validators[key]!.value, + }; + }, {}); + + const mediaType: Swagger.MediaType31 = { + schema: { + ...(this.getSwaggerType(parameter.type, this.config.useTitleTagsForInlineObjects ? this.getOperationId(controllerName, method) + 'RequestBody' : undefined) as Swagger.Schema31), + ...validators, + ...(parameter.description && { description: parameter.description }), + }, + }; + + const parameterExamples = parameter.example; + const parameterExampleLabels = parameter.exampleLabels; + if (parameterExamples === undefined) { + mediaType.example = parameterExamples; + } else if (parameterExamples.length === 1) { + mediaType.example = parameterExamples[0]; + } else { + let exampleCounter = 1; + mediaType.examples = parameterExamples.reduce((acc, ex, currentIndex) => { + const exampleLabel = parameterExampleLabels?.[currentIndex]; + return { ...acc, [exampleLabel === undefined ? `Example ${exampleCounter++}` : exampleLabel]: { value: ex } }; + }, {}); + } + + return mediaType; + } + + private buildQueriesParameter(source: Tsoa.Parameter): Swagger.Parameter31[] { + if (source.type.dataType === 'refObject' || source.type.dataType === 'nestedObjectLiteral') { + const properties = source.type.properties; + + return properties.map(property => this.buildParameter(this.queriesPropertyToQueryParameter(property))); + } + throw new Error(`Queries '${source.name}' parameter must be an object.`); + } + + private buildParameter(source: Tsoa.Parameter): Swagger.Parameter31 { + const schema: Swagger.Schema31 = { + default: source.default, + }; + + const parameterType = this.getSwaggerType(source.type) as Swagger.Schema31; + + // Use format and type if present + if (parameterType.format) { + schema.format = this.throwIfNotDataFormat(parameterType.format); + } + + if (parameterType.type) { + schema.type = this.throwIfNotDataType(parameterType.type); + } + + // Handle ref case + if (parameterType.$ref) { + return { + name: source.name, + in: source.in as 'query' | 'header' | 'path' | 'cookie', + required: this.isRequiredWithoutDefault(source), + deprecated: source.deprecated || undefined, + description: source.description, + schema: parameterType, + ...this.buildExamples(source), + }; + } + + // Copy array-related and enum data + if (parameterType.items !== undefined) { + schema.items = parameterType.items; + } + if (parameterType.enum !== undefined) { + schema.enum = parameterType.enum; + } + + // Apply validators + Object.entries(source.validators) + .filter(([key]) => shouldIncludeValidatorInSchema(key)) + .forEach(([key, val]) => { + (schema as any)[key] = val!.value; + }); + + // Assemble final parameter object + const parameter: Swagger.Parameter31 = { + name: source.name, + in: source.in as 'query' | 'header' | 'path' | 'cookie', + required: this.isRequiredWithoutDefault(source), + deprecated: source.deprecated || undefined, + description: source.description, + schema, + ...this.buildExamples(source), + }; + + return parameter; + } + + protected buildProperties(source: Tsoa.Property[]): { [propertyName: string]: Swagger.Schema31 } { + const properties: { [propertyName: string]: Swagger.Schema31 } = {}; + + source.forEach(property => { + let swaggerType = this.getSwaggerType(property.type) as Swagger.Schema31; + const format = property.format as Swagger.DataFormat; + + swaggerType.description = property.description; + swaggerType.example = property.example; + swaggerType.format = format || swaggerType.format; + + if (!swaggerType.$ref) { + swaggerType.default = property.default; + + Object.keys(property.validators) + .filter(shouldIncludeValidatorInSchema) + .forEach(key => { + swaggerType = { + ...swaggerType, + [key]: property.validators[key]!.value, + }; + }); + } + + if (property.deprecated) { + swaggerType.deprecated = true; + } + + if (property.extensions) { + property.extensions.forEach(ext => { + swaggerType[ext.key] = ext.value; + }); + } + + properties[property.name] = swaggerType; + }); + + return properties; + } + + protected getSwaggerTypeForReferenceType(referenceType: Tsoa.ReferenceType): Swagger.BaseSchema { + return { $ref: `#/components/schemas/${encodeURIComponent(referenceType.refName)}` }; + } + + 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) + return {}; + } else if (dataType === 'file') { + return { type: 'string', format: 'binary' }; + } + + return super.getSwaggerTypeForPrimitiveType(dataType); + } + + private isNull(type: Tsoa.Type) { + return type.dataType === 'enum' && type.enums.length === 1 && type.enums[0] === null; + } + + // Join disparate enums with the same type into one. + // + // 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: Swagger.BaseSchema[]) { + const returnTypes: Swagger.BaseSchema[] = []; + const enumValuesByType: Record> = {}; + for (const type of types) { + if (type.enum && type.type) { + for (const enumValue of type.enum) { + if (!enumValuesByType[type.type]) { + enumValuesByType[type.type] = {}; + } + enumValuesByType[type.type][String(enumValue)] = enumValue; + } + } + // preserve non-enum types + else { + returnTypes.push(type); + } + } + + Object.keys(enumValuesByType).forEach(dataType => + returnTypes.push({ + type: dataType, + enum: Object.values(enumValuesByType[dataType]), + }), + ); + + return returnTypes; + } + + protected removeDuplicateSwaggerTypes(types: Swagger.BaseSchema[]): Swagger.BaseSchema[] { + if (types.length === 1) { + return types; + } else { + const typesSet = new Set(); + for (const type of types) { + typesSet.add(JSON.stringify(type)); + } + return Array.from(typesSet).map(typeString => JSON.parse(typeString)); + } + } + + protected getSwaggerTypeForUnionType(type: Tsoa.UnionType, title?: string) { + // Filter out nulls and undefineds + const actualSwaggerTypes = this.removeDuplicateSwaggerTypes( + this.groupEnums( + type.types + .filter(x => !this.isNull(x)) + .filter(x => x.dataType !== 'undefined') + .map(x => this.getSwaggerType(x)), + ), + ); + const nullable = type.types.some(x => this.isNull(x)); + + if (nullable) { + if (actualSwaggerTypes.length === 1) { + const [swaggerType] = actualSwaggerTypes; + // for ref union with null, use an allOf with a single + // element since you can't attach nullable directly to a ref. + // https://swagger.io/docs/specification/using-ref/#syntax + if (swaggerType.$ref) { + return { allOf: [swaggerType], nullable }; + } + + // Note that null must be explicitly included in the list of enum values. Using nullable: true alone is not enough here. + // https://swagger.io/docs/specification/data-models/enums/ + if (swaggerType.enum) { + swaggerType.enum.push(null); + } + + return { ...(title && { title }), ...swaggerType, nullable }; + } else { + return { ...(title && { title }), anyOf: actualSwaggerTypes, nullable }; + } + } else { + if (actualSwaggerTypes.length === 1) { + return { ...(title && { title }), ...actualSwaggerTypes[0] }; + } else { + return { ...(title && { title }), anyOf: actualSwaggerTypes }; + } + } + } + + protected getSwaggerTypeForIntersectionType(type: Tsoa.IntersectionType, title?: string) { + return { allOf: type.types.map(x => this.getSwaggerType(x)), ...(title && { title }) }; + } + + protected getSwaggerTypeForEnumType(enumType: Tsoa.EnumType, title?: string): Swagger.Schema3 { + const types = this.determineTypesUsedInEnum(enumType.enums); + + if (types.size === 1) { + const type = types.values().next().value; + const nullable = enumType.enums.includes(null) ? true : false; + return { ...(title && { title }), type, enum: enumType.enums.map(member => getValue(type, member)), nullable }; + } else { + const valuesDelimited = Array.from(types).join(','); + throw new Error(`Enums can only have string or number values, but enum had ${valuesDelimited}`); + } + } + + protected buildExamples(source: Pick): { + example?: unknown; + examples?: { [name: string]: Swagger.Example3 }; + } { + const { example: parameterExamples, exampleLabels } = source; + + if (parameterExamples === undefined) { + return {}; + } + + if (parameterExamples.length === 1) { + return { + example: parameterExamples[0], + }; + } + + let exampleCounter = 1; + const examples = parameterExamples.reduce( + (acc, ex, idx) => { + const label = exampleLabels?.[idx]; + const name = label ?? `Example ${exampleCounter++}`; + acc[name] = { value: ex }; + return acc; + }, + {} as Record, + ); + + return { examples }; + } + + protected getSwaggerType(type: Tsoa.Type, title?: string): Swagger.BaseSchema { + if (type.dataType === 'tuple') { + const tupleType = type as Tsoa.TupleType; + const prefixItems = tupleType.types.map(t => this.getSwaggerType(t)) as Swagger.Schema31[]; + + const schema: Swagger.Schema31 = { + type: 'array', + prefixItems, + minItems: prefixItems.length, + ...(tupleType.restType + ? { + items: this.getSwaggerType(tupleType.restType) as Swagger.Schema31, + } + : { + maxItems: prefixItems.length, + items: false, + }), + }; + + return schema as unknown as Swagger.BaseSchema; + } + + return super.getSwaggerType(type, title); + } +} diff --git a/packages/cli/src/utils/internalTypeGuards.ts b/packages/cli/src/utils/internalTypeGuards.ts index e28ab99b7..d5bd6d2d2 100644 --- a/packages/cli/src/utils/internalTypeGuards.ts +++ b/packages/cli/src/utils/internalTypeGuards.ts @@ -48,6 +48,8 @@ export function isRefType(metaType: Tsoa.Type): metaType is Tsoa.ReferenceType { return true; case 'string': return false; + case 'tuple': + return false; case 'union': return false; case 'void': diff --git a/packages/runtime/src/config.ts b/packages/runtime/src/config.ts index 2b26c65ae..4fcafe279 100644 --- a/packages/runtime/src/config.ts +++ b/packages/runtime/src/config.ts @@ -103,6 +103,7 @@ export interface SpecConfig { * Possible values: * - 2: generates OpenAPI version 2. * - 3: generates OpenAPI version 3. + * - 3.1: generates OpenAPI version 3.1. */ specVersion?: Swagger.SupportedSpecMajorVersion; diff --git a/packages/runtime/src/metadataGeneration/tsoa.ts b/packages/runtime/src/metadataGeneration/tsoa.ts index 9b7e05627..761bce072 100644 --- a/packages/runtime/src/metadataGeneration/tsoa.ts +++ b/packages/runtime/src/metadataGeneration/tsoa.ts @@ -49,6 +49,7 @@ export namespace Tsoa { validators: Validators; deprecated: boolean; exampleLabels?: Array; + $ref?: Swagger.BaseSchema; } export interface ResParameter extends Response, Parameter { @@ -124,11 +125,12 @@ export namespace Tsoa { | 'nestedObjectLiteral' | 'union' | 'intersection' - | 'undefined'; + | 'undefined' + | 'tuple'; export type RefTypeLiteral = 'refObject' | 'refEnum' | 'refAlias'; - export type PrimitiveTypeLiteral = Exclude; + export type PrimitiveTypeLiteral = Exclude; export interface TypeBase { dataType: TypeStringLiteral; @@ -156,7 +158,8 @@ export namespace Tsoa { | RefAliasType | NestedObjectLiteralType | UnionType - | IntersectionType; + | IntersectionType + | TupleType; export interface StringType extends TypeBase { dataType: 'string'; @@ -281,6 +284,12 @@ export namespace Tsoa { types: Type[]; } + export interface TupleType extends TypeBase { + dataType: 'tuple'; + types: Type[]; + restType?: Type; + } + export interface ReferenceTypeMap { [refName: string]: Tsoa.ReferenceType; } diff --git a/packages/runtime/src/swagger/swagger.ts b/packages/runtime/src/swagger/swagger.ts index e9b2594d5..b5328a550 100644 --- a/packages/runtime/src/swagger/swagger.ts +++ b/packages/runtime/src/swagger/swagger.ts @@ -6,7 +6,7 @@ export namespace Swagger { export type Protocol = 'http' | 'https' | 'ws' | 'wss'; - export type SupportedSpecMajorVersion = 2 | 3; + export type SupportedSpecMajorVersion = 2 | 3 | 3.1; export interface Spec { info: Info; @@ -23,7 +23,7 @@ export namespace Swagger { produces?: string[]; paths: { [name: string]: Path }; definitions?: { [name: string]: Schema2 }; - parameters?: { [name: string]: Parameter }; + parameters?: { [name: string]: Parameter2 }; responses?: { [name: string]: Response }; security?: Security[]; securityDefinitions?: { [name: string]: SecuritySchemes }; @@ -36,18 +36,29 @@ export namespace Swagger { paths: { [name: string]: Path3 }; } + export interface Spec31 extends Spec { + openapi: '3.1.0'; + servers: Server[]; + components: Components31; + paths: { [name: string]: Path3 }; + } + export interface Components { callbacks?: { [name: string]: unknown }; examples?: { [name: string]: Example3 | string }; headers?: { [name: string]: unknown }; links?: { [name: string]: unknown }; - parameters?: { [name: string]: Parameter }; + parameters?: { [name: string]: Parameter3 }; requestBodies?: { [name: string]: unknown }; responses?: { [name: string]: Response }; schemas?: { [name: string]: Schema3 }; securitySchemes?: { [name: string]: SecuritySchemes }; } + export interface Components31 extends Omit { + schemas?: { [name: string]: Schema31 }; + } + export interface Server { url: string; } @@ -89,49 +100,78 @@ export namespace Swagger { description?: string; } - export interface BaseParameter extends BaseSchema { + export type BaseParameter = { name: string; - in: 'query' | 'header' | 'path' | 'formData' | 'body'; + in: 'query' | 'header' | 'path' | 'formData' | 'body' | 'cookie'; required?: boolean; description?: string; - example?: unknown; - examples?: { [name: string]: Example3 | string }; - schema: Schema; - type: DataType; - format?: DataFormat; deprecated?: boolean; - } + [ext: `x-${string}`]: unknown; + } & Pick; - export interface BodyParameter extends BaseParameter { + export type BodyParameter = BaseParameter & { in: 'body'; - } + }; - export interface QueryParameter extends BaseParameter { - in: 'query'; - allowEmptyValue?: boolean; + export type FormDataParameter = BaseParameter & { + in: 'formData'; + type: DataType; + format?: DataFormat; collectionFormat?: 'csv' | 'ssv' | 'tsv' | 'pipes' | 'multi'; - } + default?: unknown; + }; - export function isQueryParameter(parameter: BaseParameter): parameter is QueryParameter { - return parameter.in === 'query'; - } + type QueryParameter = BaseParameter & { + in: 'query'; + type: DataType; + format?: DataFormat; + collectionFormat?: 'csv' | 'ssv' | 'tsv' | 'pipes' | 'multi'; + default?: unknown; + }; - export interface PathParameter extends BaseParameter { + type PathParameter = BaseParameter & { in: 'path'; - } + type: DataType; + format?: DataFormat; + default?: unknown; + }; - export interface HeaderParameter extends BaseParameter { + type HeaderParameter = BaseParameter & { in: 'header'; + type: DataType; + format?: DataFormat; + default?: unknown; + }; + + type Swagger2BaseParameter = BaseParameter & { + schema: Schema2; + }; + + export type Swagger2BodyParameter = Swagger2BaseParameter & BodyParameter; + export type Swagger2FormDataParameter = Swagger2BaseParameter & FormDataParameter; + export type Swagger2QueryParameter = Swagger2BaseParameter & QueryParameter; + export type Swagger2PathParameter = Swagger2BaseParameter & PathParameter; + export type Swagger2HeaderParameter = Swagger2BaseParameter & HeaderParameter; + + export type Parameter2 = Swagger2BodyParameter | Swagger2FormDataParameter | Swagger2QueryParameter | Swagger2PathParameter | Swagger2HeaderParameter; + + export function isQueryParameter(parameter: unknown): parameter is Swagger2QueryParameter { + return typeof parameter === 'object' && parameter !== null && 'in' in parameter && parameter.in === 'query'; } - export interface FormDataParameter extends BaseParameter { - in: 'formData'; - collectionFormat?: 'csv' | 'ssv' | 'tsv' | 'pipes' | 'multi'; + export interface Parameter3 extends BaseParameter { + in: 'query' | 'header' | 'path' | 'cookie'; + schema: Schema3; + style?: string; + explode?: boolean; + allowReserved?: boolean; + example?: unknown; + examples?: { [name: string]: Example3 | string }; } - export type Parameter = BodyParameter | FormDataParameter | QueryParameter | PathParameter | HeaderParameter; - export type Parameter2 = Parameter & { 'x-deprecated'?: boolean }; - export type Parameter3 = Parameter; + export interface Parameter31 extends Omit { + schema: Schema31; + } export interface Path { $ref?: string; @@ -191,12 +231,28 @@ export namespace Swagger { [ext: `x-${string}`]: unknown; } + export interface Operation31 extends Omit { + parameters?: Parameter31[]; + requestBody?: RequestBody31; + responses: { [name: string]: Response31 }; + } + export interface RequestBody { content: { [requestMediaType: string]: MediaType }; description?: string; required?: boolean; } + export interface RequestBody31 { + content: { [requestMediaType: string]: MediaType31 }; + description?: string; + required?: boolean; + $ref?: string; + summary?: string; + examples?: { [media: string]: Example3 | string }; + [ext: `x-${string}`]: unknown; + } + export interface MediaType { schema?: Schema3; example?: unknown; @@ -204,9 +260,16 @@ export namespace Swagger { encoding?: { [name: string]: unknown }; } + export interface MediaType31 { + schema?: Schema31; + example?: unknown; + examples?: { [name: string]: Example3 | string }; + encoding?: { [name: string]: unknown }; + } + export interface Response { description: string; - schema?: Schema; + schema?: BaseSchema; headers?: { [name: string]: Header }; examples?: { [responseMediaType: string]: { [exampleName: string]: Example3 | string } }; } @@ -222,7 +285,21 @@ export namespace Swagger { headers?: { [name: string]: Header3 }; } - export interface BaseSchema { + export interface Response31 { + description: string; + content?: { + [responseMediaType: string]: { + schema?: Schema31; + examples?: { [name: string]: Example3 | string }; + example?: unknown; + encoding?: { [name: string]: unknown }; + }; + }; + headers?: { [name: string]: Header3 }; + links?: { [name: string]: unknown }; // If needed per spec + } + + export interface BaseSchema

{ type?: string; format?: DataFormat; $ref?: string; @@ -245,38 +322,64 @@ export namespace Swagger { minProperties?: number; enum?: Array; 'x-enum-varnames'?: string[]; - items?: BaseSchema; [ext: `x-${string}`]: unknown; + + // moved from Schema + additionalProperties?: boolean | BaseSchema; + properties?: { [propertyName: string]: P }; + discriminator?: string; + readOnly?: boolean; + xml?: XML; + externalDocs?: ExternalDocs; + example?: unknown; + required?: string[]; + + items?: BaseSchema; + } + + export interface Schema31 extends Omit { + type?: DataType; // could support an array, but we already do anyOf for that + nullable?: boolean; + deprecated?: boolean; + example?: unknown; + examples?: unknown[]; + + properties?: { [key: string]: Schema31 }; + additionalProperties?: boolean | Schema31; + + items?: Schema31 | false; + prefixItems?: Schema31[]; + contains?: Schema31; + + allOf?: Schema31[]; + anyOf?: Schema31[]; + oneOf?: Schema31[]; + not?: Schema31; + propertyNames?: Schema31; + + discriminator?: { + propertyName: string; + mapping?: Record; + }; } - export interface Schema3 extends Omit { + export interface Schema3 extends Omit { type?: DataType; nullable?: boolean; anyOf?: BaseSchema[]; allOf?: BaseSchema[]; deprecated?: boolean; + properties?: { [propertyName: string]: Schema3 }; } - export interface Schema2 extends Schema { + export interface Schema2 extends BaseSchema { + type?: DataType; properties?: { [propertyName: string]: Schema2 }; ['x-nullable']?: boolean; ['x-deprecated']?: boolean; } - export interface Schema extends BaseSchema { - type?: DataType; - format?: DataFormat; - additionalProperties?: boolean | BaseSchema; - properties?: { [propertyName: string]: Schema3 }; - discriminator?: string; - readOnly?: boolean; - xml?: XML; - externalDocs?: ExternalDocs; - example?: unknown; - required?: string[]; - } - export interface Header { description?: string; type: 'string' | 'number' | 'integer' | 'boolean' | 'array'; @@ -299,16 +402,29 @@ export namespace Swagger { multipleOf?: number; } - export interface Header3 extends BaseSchema { - required?: boolean; + export interface Header3 { description?: string; + required?: boolean; + deprecated?: boolean; + allowEmptyValue?: boolean; + + style?: string; + explode?: boolean; + allowReserved?: boolean; + + schema?: Schema3 | Schema31; example?: unknown; - examples?: { - [name: string]: Example3 | string; + examples?: { [media: string]: Example3 | string }; + + content?: { + [media: string]: { + schema?: Schema3 | Schema31; + example?: unknown; + examples?: { [name: string]: Example3 | string }; + }; }; - schema: Schema; - type?: DataType; - format?: DataFormat; + + [ext: `x-${string}`]: unknown; } export interface XML { diff --git a/tests/fixtures/controllers/postController31.ts b/tests/fixtures/controllers/postController31.ts new file mode 100644 index 000000000..79c4fed69 --- /dev/null +++ b/tests/fixtures/controllers/postController31.ts @@ -0,0 +1,141 @@ +import { Body, Deprecated, File, FormField, Patch, Post, Query, Route, UploadedFile, UploadedFiles } from '@tsoa/runtime'; +import { ModelService } from '../services/modelService'; +import { GenericRequest, TestClassModel, TestModel, TupleTestModel } from '../testModel31'; + +@Route('PostTest') +export class PostTestController { + private statusCode?: number = undefined; + + public setStatus(statusCode: number) { + this.statusCode = statusCode; + } + + public getStatus() { + return this.statusCode; + } + + public getHeaders() { + return []; + } + + @Post() + public async postModel(@Body() model: TestModel): Promise { + return model; + } + + @Post('Object') + public async postObject(@Body() body: { obj: { [key: string]: string } }): Promise<{ [key: string]: string }> { + return body.obj; + } + + @Post('MultiType') + public async postMultiType(@Body() input: number | string): Promise { + return input; + } + + @Patch() + public async updateModel(@Body() model: TestModel): Promise { + return new ModelService().getModel(); + } + + @Post('WithDifferentReturnCode') + public async postWithDifferentReturnCode(@Body() model: TestModel): Promise { + this.setStatus(201); + return model; + } + + @Post('WithClassModel') + public async postClassModel(@Body() model: TestClassModel): Promise { + const augmentedModel = new TestClassModel('test', 'test2', 'test3', 'test4', 'test5'); + augmentedModel.id = 700; + + return augmentedModel; + } + + @Post('File') + public async postWithFile(@UploadedFile('someFile') aFile: File): Promise { + return aFile; + } + + @Post('FileOptional') + public async postWithOptionalFile(@UploadedFile('optionalFile') optionalFile?: File): Promise { + return optionalFile?.originalname ?? 'no file'; + } + + @Post('FileWithoutName') + public async postWithFileWithoutName(@UploadedFile() aFile: File): Promise { + return aFile; + } + + @Post('ManyFilesAndFormFields') + public async postWithFiles(@UploadedFiles('someFiles') files: File[], @FormField('a') a: string, @FormField('c') c: string): Promise { + return files; + } + + @Post('ManyFilesInDifferentFields') + public async postWithDifferentFields(@UploadedFile('file_a') fileA: File, @UploadedFile('file_b') fileB: File): Promise { + return [fileA, fileB]; + } + + @Post('ManyFilesInDifferentArrayFields') + public async postWithDifferentArrayFields(@UploadedFiles('files_a') filesA: File[], @UploadedFile('file_b') fileB: File, @UploadedFiles('files_c') filesC: File[]): Promise { + return [filesA, [fileB], filesC]; + } + + @Post('MixedFormDataWithFilesContainsOptionalFile') + public async mixedFormDataWithFile( + @FormField('username') username: string, + @UploadedFile('avatar') avatar: File, + @UploadedFile('optionalAvatar') optionalAvatar?: File, + ): Promise<{ username: string; avatar: File; optionalAvatar?: File }> { + return { username, avatar, optionalAvatar }; + } + + /** + * + * @param aFile File description of multipart + * @param a FormField description of multipart + * @param c + */ + @Post('DescriptionOfFileAndFormFields') + public async postWithFileAndParams(@UploadedFile('file') aFile: File, @FormField('a') a: string, @FormField('c') c: string): Promise { + return aFile; + } + + @Post('DeprecatedFormField') + public async postWithDeprecatedParam(@FormField('a') a: string, @FormField('dontUse') @Deprecated() dontUse?: string): Promise { + return new ModelService().getModel(); + } + + @Post('Location') + public async postModelAtLocation(): Promise { + return new ModelService().getModel(); + } + + @Post('Multi') + public async postWithMultiReturn(): Promise { + const model = new ModelService().getModel(); + + return [model, model]; + } + + @Post('WithId/{id}') + public async postWithId(id: number): Promise { + return new ModelService().getModel(); + } + + @Post('WithBodyAndQueryParams') + public async postWithBodyAndQueryParams(@Body() model: TestModel, @Query() query: string): Promise { + return new ModelService().getModel(); + } + + @Post('GenericBody') + public async getGenericRequest(@Body() genericReq: GenericRequest): Promise { + return genericReq.value; + } + + @Post('TupleTest') + public async postTuple(@Body() model: TupleTestModel): Promise { + return model; + } +} diff --git a/tests/fixtures/testModel31.ts b/tests/fixtures/testModel31.ts new file mode 100644 index 000000000..9ef00b4e0 --- /dev/null +++ b/tests/fixtures/testModel31.ts @@ -0,0 +1,1299 @@ +/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ +/* This is what we want to test here*/ + +import { Deprecated, Example, Extension } from '@tsoa/runtime'; + +/** + * This is a description of a model + * @tsoaModel + * @example + * { + * "boolArray": [true, false], + * "boolValue": true, + * "dateValue": "2018-06-25T15:45:00Z", + * "id": 2, + * "modelValue": { + * "id": 3, + * "email": "test(at)example.com" + * }, + * "modelsArray": [], + * "numberArray": [1, 2, 3], + * "numberArrayReadonly": [1, 2, 3], + * "numberValue": 1, + * "optionalString": "optional string", + * "strLiteralArr": ["Foo", "Bar"], + * "strLiteralVal": "Foo", + * "stringArray": ["string one", "string two"], + * "stringValue": "a string" + * } + * @example + * { + * "stringValue": "(example2)a string" + * } + */ +export interface TestModel extends Model { + and: TypeAliasModel1 & TypeAliasModel2; + /** + * This is a description of this model property, numberValue + */ + numberValue: number; + numberArray: number[]; + readonly numberArrayReadonly: readonly number[]; + /** + * @example "letmein" + * @example "letmein(example)2" + * @format password + */ + stringValue: string; + stringArray: string[]; + /** + * @default true + */ + boolValue: boolean; + boolArray: boolean[]; + object: object; + objectArray: object[]; + undefinedValue: undefined; + enumValue?: EnumIndexValue; + enumArray?: EnumIndexValue[]; + enumNumberValue?: EnumNumberValue; + enumStringNumberValue?: EnumStringNumberValue; + enumStringNumberArray?: EnumStringNumberValue[]; + enumNumberArray?: EnumNumberValue[]; + enumStringValue?: EnumStringValue; + enumStringProperty?: EnumStringValue.VALUE_1; + enumStringArray?: EnumStringValue[]; + modelValue: TestSubModel; + modelsArray: TestSubModel[]; + strLiteralVal: StrLiteral; + strLiteralArr: StrLiteral[]; + nullableStringLiteral?: 'NULLABLE_LIT_1' | 'NULLABLE_LIT_2' | null; + unionPrimitiveType?: 'String' | 1 | 20.0 | true | false; + nullableUnionPrimitiveType?: 'String' | 1 | 20.0 | true | false | null; + undefineableUnionPrimitiveType: 'String' | 1 | 20.0 | true | false | undefined; + singleFloatLiteralType?: 3.1415; + negativeNumberLiteralType?: -1; + dateValue?: Date; + optionalString?: string; + anyType?: any; + unknownType?: unknown; + genericTypeObject?: Generic<{ foo: string; bar: boolean }>; + indexed?: Partial; + indexedValue?: IndexedValue; + parenthesizedIndexedValue?: ParenthesizedIndexedValue; + indexedValueReference?: IndexedValueReference; + indexedValueGeneric?: IndexedValueGeneric; + stringUnionRecord?: Record<'record-foo' | 'record-bar', { data: string }>; + numberUnionRecord?: Record<1 | 2, { data: string }>; + stringRecord?: Record; + numberRecord?: Record; + emptyRecord?: Record; + // modelsObjectDirect?: {[key: string]: TestSubModel2;}; + modelsObjectIndirect?: TestSubModelContainer; + modelsObjectIndirectNS?: TestSubModelContainerNamespace.TestSubModelContainer; + modelsObjectIndirectNS2?: TestSubModelContainerNamespace.InnerNamespace.TestSubModelContainer2; + modelsObjectIndirectNS_Alias?: TestSubModelContainerNamespace_TestSubModelContainer; + modelsObjectIndirectNS2_Alias?: TestSubModelContainerNamespace_InnerNamespace_TestSubModelContainer2; + + modelsArrayIndirect?: TestSubArrayModelContainer; + modelsEnumIndirect?: TestSubEnumModelContainer; + or: TypeAliasModel1 | TypeAliasModel2; + referenceAnd: TypeAliasModelCase1; + typeAliasCase1?: TypeAliasModelCase1; + TypeAliasCase2?: TypeAliasModelCase2; + + typeAliases?: { + word: Word; + fourtyTwo: FourtyTwo; + dateAlias?: DateAlias; + unionAlias: UnionAlias; + intersectionAlias: IntersectionAlias; + nOLAlias: NolAlias; + genericAlias: GenericAlias; + genericAlias2: GenericAlias; + forwardGenericAlias: ForwardGenericAlias; + }; + + advancedTypeAliases?: { + omit?: Omit; + omitHidden?: Omit; + partial?: Partial; + excludeToEnum?: Exclude; + excludeToAlias?: Exclude; + // prettier-ignore + excludeLiteral?: Exclude; + excludeToInterface?: Exclude; + excludeTypeToPrimitive?: NonNullable; + + pick?: Pick, 'list'>; + + readonlyClass?: Readonly; + + defaultArgs?: DefaultTestModel; + heritageCheck?: HeritageTestModel; + heritageCheck2?: HeritageTestModel2; + }; + + genericMultiNested?: GenericRequest>; + // eslint-disable-next-line @typescript-eslint/array-type + genericNestedArrayKeyword1?: GenericRequest>; + genericNestedArrayCharacter1?: GenericRequest; + // eslint-disable-next-line @typescript-eslint/array-type + genericNestedArrayKeyword2?: GenericRequest>; + genericNestedArrayCharacter2?: GenericRequest; + mixedUnion?: string | TypeAliasModel1; + + objLiteral: { + name: string; + nested?: { + bool: boolean; + optional?: number; + allNestedOptional: { + one?: string; + two?: string; + }; + additionals?: { + [name: string]: TypeAliasModel1; + }; + }; + /** @deprecated */ + deprecatedSubProperty?: number; + }; + + /** not deprecated */ + notDeprecatedProperty?: number; + /** although the properties won't be explicity deprecated in the spec, they'll be implicitly deprecated due to the ref pulling it in */ + propertyOfDeprecatedType?: DeprecatedType; + propertyOfDeprecatedClass?: DeprecatedClass; + /** @deprecated */ + deprecatedProperty?: number; + deprecatedFieldsOnInlineMappedTypeFromSignature?: { + [K in keyof TypeWithDeprecatedProperty as `${K}Prop`]: boolean; + }; + deprecatedFieldsOnInlineMappedTypeFromDeclaration?: { + [K in keyof ClassWithDeprecatedProperty as `${K}Prop`]: boolean; + }; + notDeprecatedFieldsOnInlineMappedTypeWithIndirection?: { + [K in Exclude]: boolean; + }; + + defaultGenericModel?: GenericModel; + + // prettier-ignore + stringAndBoolArray?: Array<(string | boolean)>; + + /** + * @example { + * "numberOrNull": null, + * "wordOrNull": null, + * "maybeString": null, + * "justNull": null + * } + */ + nullableTypes?: { + /** + * @isInt + * @minimum 5 + */ + numberOrNull: number | null; + wordOrNull: Maybe; + maybeString: Maybe; + justNull: null; + }; + + templateLiteralString?: TemplateLiteralString; + inlineTLS?: `${Uppercase}`; + inlineMappedType?: { [K in Exclude]: boolean }; + inlineMappedTypeRemapped?: { + [K in keyof ParameterTestModel as `${Capitalize}Prop`]?: string; + }; + + /** + * @extension {"x-key-1": "value-1"} + * @extension {"x-key-2": "value-2"} + */ + extensionComment?: boolean; + + keyofLiteral?: keyof Items; + + namespaces?: { + simple: NamespaceType; + inNamespace1: Namespace1.NamespaceType; + typeHolder1: Namespace1.TypeHolder; + inModule: Namespace2.Namespace2.NamespaceType; + typeHolder2: Namespace2.TypeHolder; + }; + + defaults?: { + basic: DefaultsClass; + replacedTypes: ReplaceTypes; + /** + * @default undefined + */ + defaultUndefined?: string; + /** + * @default null + */ + defaultNull: string | null; + /** + * @default + * { + * "a": "a", + * "b": 2 + * } + */ + defaultObject: { a: string; b: number }; + /** + * @default `\`"'\"\'\n\t\r\b\f\v\0\g\x\\`//\0, \v is not supported... + * + */ + stringEscapeCharacters: undefined; //type is not really interesting + /** + * @default //Comment1 + * 4 + * //Comment2 + * + */ + comments: undefined; //type is not really interesting + /** + * @default { + * //Alma + * `\\`: '\n' + * + * } + * + */ + jsonCharacters: undefined; //type is not really interesting + }; + + jsDocTypeNames?: { + simple: Partial<{ a: string }>; + commented: Partial<{ + /** comment */ + a: string; + }>; + multilineCommented: Partial<{ + /** + * multiline + * comment + */ + a: string; + }>; + defaultValue: Partial<{ + /** @default "true" */ + a: string; + }>; + deprecated: Partial<{ + /** @deprecated */ + a: string; + }>; + validators: Partial<{ + /** @minLength 3 */ + a: string; + }>; + examples: Partial<{ + /** @example "example" */ + a: string; + }>; + extensions: Partial<{ + /** @extension {"x-key-1": "value-1"} */ + a: string; + }>; + ignored: Partial<{ + /** @ignore */ + a: string; + }>; + + indexedSimple: Partial<{ [a: string]: string }>; + indexedCommented: Partial<{ + /** comment */ + [a: string]: string; + }>; + indexedMultilineCommented: Partial<{ + /** + * multiline + * comment + */ + [a: string]: string; + }>; + indexedDefaultValue: Partial<{ + /** @default "true" */ + [a: string]: string; + }>; + indexedDeprecated: Partial<{ + /** @deprecated */ + [a: string]: string; + }>; + indexedValidators: Partial<{ + /** @minLength 3 */ + [a: string]: string; + }>; + indexedExamples: Partial<{ + /** @example "example" */ + [a: string]: string; + }>; + indexedExtensions: Partial<{ + /** @extension {"x-key-1": "value-1"} */ + [a: string]: string; + }>; + indexedIgnored: Partial<{ + /** @ignore */ + [a: string]: string; + }>; + }; + + jsdocMap?: { + omitted: Omit; + partial: Partial; + replacedTypes: ReplaceStringAndNumberTypes; + doubleReplacedTypes: ReplaceStringAndNumberTypes>; + postfixed: Postfixed; + values: Values; + typesValues: InternalTypes>; + onlyOneValue: JsDocced['numberValue']; + synonym: JsDoccedSynonym; + synonym2: JsDoccedSynonym2; + }; + + duplicatedDefinitions?: { + interfaces: DuplicatedInterface; + enums: DuplicatedEnum; + enumMember: DuplicatedEnum.C; + namespaceMember: DuplicatedEnum.D; + }; + + mappeds?: { + unionMap: Partial<{ a: string } | { b: number }>; + indexedUnionMap: Partial<{ a: string } | { [b: string]: number }>; + doubleIndexedUnionMap: Partial<{ [a: string]: string } | { [b: string]: number }>; + + intersectionMap: Partial<{ a: string } & { b: number }>; + indexedIntersectionMap: Partial<{ a: string } & { [b: string]: number }>; + doubleIndexedIntersectionMap: Partial<{ [a: string]: string } & { [b: number]: number }>; + parenthesizedMap: Partial<{ a: string } | ({ b: string } & { c: string })>; + parenthesizedMap2: Partial<({ a: string } | { b: string }) & { c: string }>; + + undefinedMap: Partial; + nullMap: Partial; + }; + + conditionals?: { + simpeConditional: string extends string ? number : boolean; + simpeFalseConditional: string extends number ? number : boolean; + typedConditional: Conditional; + typedFalseConditional: Conditional; + dummyConditional: Dummy>; + dummyFalseConditional: Dummy>; + mappedConditional: Partial; + mappedTypedConditional: Partial>; + }; + + typeOperators?: { + keysOfAny: KeysMember; + keysOfInterface: KeysMember; + simple: keyof NestedTypeLiteral; + keyofItem: keyof NestedTypeLiteral['b']; + keyofAnyItem: keyof NestedTypeLiteral['e']; + keyofAny: keyof any; + stringLiterals: keyof Record<'A' | 'B' | 'C', string>; + stringAndNumberLiterals: keyof Record<'A' | 'B' | 3, string>; + keyofEnum: keyof typeof DuplicatedEnum; + numberAndStringKeys: keyof { [3]: string; [4]: string; a: string }; + oneStringKeyInterface: keyof { a: string }; + oneNumberKeyInterface: keyof { [3]: string }; + indexStrings: keyof { [a: string]: string }; + indexNumbers: keyof { [a: number]: string }; + }; + + nestedTypes?: { + multiplePartial: Partial>; + separateField: Partial, 'a'>>; + separateField2: Partial, 'a' | 'b'>>; + separateField3: Partial, 'a' | 'b'>>; + }; + + computedKeys?: { + [EnumDynamicPropertyKey.STRING_KEY]: string; + [EnumDynamicPropertyKey.NUMBER_KEY]: string; + }; +} + +type SeparateField = { + omitted: Omit; + field: T[Field]; +}; + +type KeysMember = { + keys: keyof T; +}; + +interface NestedTypeLiteral { + a: string; + b: { + c: string; + d: string; + }; + e: any; +} + +type Dummy = T; + +type Conditional = T extends CheckType ? TrueType : FalseType; + +interface DuplicatedInterface { + a: string; +} + +interface DuplicatedInterface { + a: string; + b: string; +} + +class DuplicatedInterface { + a = 'defaultA'; +} + +enum DuplicatedEnum { + A = 'AA', + B = 'BB', +} + +enum DuplicatedEnum { + C = 'CC', +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace DuplicatedEnum { + export type D = 'DD'; +} + +interface JsDocced { + /** + * @maxLength 3 + * @default "def" + */ + stringValue: string; + /** + * @isInt + * @default 6 + */ + numberValue: number; +} + +type JsDoccedKeys = keyof JsDocced; +type JsDoccedSynonym = { [key in JsDoccedKeys]: JsDocced[key] }; +type JsDoccedSynonym2 = { [key in keyof JsDocced]: JsDocced[key] }; +type ReplaceTypes = { [K in keyof T]: T[K] extends Type1 ? Type2 : Type1 }; +type ReplaceStringAndNumberTypes = ReplaceTypes; +type Postfixed = { [K in keyof T as `${K & string}${Postfix}`]: T[K] }; +type Values = { [K in keyof T]: { value: T[K] } }; +type InternalTypes> = { [K in keyof T]: T[K]['value'] }; + +class DefaultsClass { + /** + * @default true + */ + boolValue1?: boolean; + /** + * @default false + */ + boolValue2? = true; + boolValue3? = false; + boolValue4?: boolean; +} + +type NamespaceType = string; + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace Namespace1 { + export interface NamespaceType { + inFirstNamespace: string; + } + + export interface TypeHolder { + inNamespace1_1: Namespace1.NamespaceType; + inNamespace1_2: NamespaceType; + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace Namespace1 { + export interface NamespaceType { + inFirstNamespace2: string; + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace Namespace2 { + interface NamespaceType { + inSecondNamespace: string; + } + + // eslint-disable-next-line @typescript-eslint/prefer-namespace-keyword, @typescript-eslint/no-namespace + export module Namespace2 { + export interface NamespaceType { + inModule: string; + other?: NamespaceType; + } + } + + export interface TypeHolder { + inModule: Namespace2.NamespaceType; + inNamespace2: NamespaceType; + } +} + +type Items = { + type1: unknown; + type2: unknown; +}; + +/** @deprecated */ +interface DeprecatedType { + value: string; +} + +@Deprecated() +class DeprecatedClass {} + +interface TypeWithDeprecatedProperty { + ok: boolean; + /** @deprecated */ + notOk?: boolean; +} + +class ClassWithDeprecatedProperty { + ok!: boolean; + @Deprecated() + notOk?: boolean; + /** @deprecated */ + stillNotOk?: boolean; +} + +interface Generic { + foo: T; +} + +interface Indexed { + foo: { + bar: string; + }; +} + +const indexedValue = { + foo: 'FOO', + bar: 'BAR', +} as const; +export type IndexedValueTypeReference = typeof indexedValue; + +export type IndexedValue = (typeof indexedValue)[keyof typeof indexedValue]; + +// prettier-ignore +export type ParenthesizedIndexedValue = (typeof indexedValue)[keyof typeof indexedValue]; + +export type IndexedValueReference = IndexedValueTypeReference[keyof IndexedValueTypeReference]; + +export type IndexedValueGeneric = Value[keyof Value]; + +const otherIndexedValue = { + foo: 'fOO', +} as const; + +export type ForeignIndexedValue = (typeof indexedValue)[keyof typeof otherIndexedValue]; +type Maybe = T | null; + +export interface TypeAliasModel1 { + value1: string; +} + +export interface TypeAliasModel2 { + value2: string; +} + +export class TypeAliasModel3 { + public value3!: string; +} + +export type TypeAlias4 = { value4: string }; + +export type TypeAliasDateTime = { + /** + * @isDateTime + */ + dateTimeValue: Date; +}; + +export type TypeAliasDate = { + /** + * @isDate + */ + dateValue: Date; +}; + +export type TypeAliasModelCase1 = TypeAliasModel1 & TypeAliasModel2; + +export type TypeAliasModelCase2 = TypeAliasModelCase1 & TypeAliasModel3; + +type UnionAndIntersectionAlias = OneOrTwo & ThreeOrFour; +type OneOrTwo = TypeAliasModel1 | TypeAliasModel2; +type ThreeOrFour = TypeAliasModel3 | TypeAlias4; + +/** + * A Word shall be a non-empty sting + * @minLength 1 + * @format password + */ +type Word = string; + +/** + * The number 42 expressed through OpenAPI + * @isInt + * @default 42 + * @minimum 42 + * @maximum 42 + * @example 42 + */ +type FourtyTwo = number; + +/** + * @isDate invalid ISO 8601 date format, i.e. YYYY-MM-DD + */ +type DateAlias = Date; + +type UnionAlias = TypeAliasModelCase2 | TypeAliasModel2; +type IntersectionAlias = { value1: string; value2: string } & TypeAliasModel1; +/* tslint:disable-next-line */ +type NolAlias = { value1: string; value2: string }; +type GenericAlias = T; +type ForwardGenericAlias = GenericAlias | T; + +type EnumUnion = EnumIndexValue | EnumNumberValue; + +/** + * EnumIndexValue. + */ +export enum EnumIndexValue { + VALUE_1, + VALUE_2, +} + +/** + * EnumNumberValue. + */ +export enum EnumNumberValue { + VALUE_0 = 0, + VALUE_1 = 2, + VALUE_2 = 5, +} + +/** + * EnumStringNumberValue. + * @tsoaModel + */ +export enum EnumStringNumberValue { + VALUE_0 = '0', + VALUE_1 = '2', + VALUE_2 = '5', +} + +/** + * EnumStringValue. + * @example "VALUE_1" + */ +export enum EnumStringValue { + EMPTY = '', + VALUE_1 = 'VALUE_1', + VALUE_2 = 'VALUE_2', +} + +/** + * EnumDynamicPropertyKey. + */ +export enum EnumDynamicPropertyKey { + STRING_KEY = 'enumDynamicKey', + NUMBER_KEY = 1, +} + +/** + * StrLiteral. + * @example "Foo" + */ +// shortened from StringLiteral to make the tslint enforced +// alphabetical sorting cleaner +export type StrLiteral = '' | 'Foo' | 'Bar'; + +export interface TestSubModelContainer { + [key: string]: TestSubModel2; +} + +export interface TestSubArrayModelContainer { + [key: string]: TestSubModel2[]; +} + +export interface TestSubEnumModelContainer { + [key: string]: EnumStringValue; +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace TestSubModelContainerNamespace { + export interface TestSubModelContainer { + [key: string]: TestSubModelNamespace.TestSubModelNS; + } + + // eslint-disable-next-line @typescript-eslint/no-namespace + export namespace InnerNamespace { + export interface TestSubModelContainer2 { + [key: string]: TestSubModelNamespace.TestSubModelNS; + } + } +} +export type TestSubModelContainerNamespace_TestSubModelContainer = TestSubModelContainerNamespace.TestSubModelContainer; +export type TestSubModelContainerNamespace_InnerNamespace_TestSubModelContainer2 = TestSubModelContainerNamespace.InnerNamespace.TestSubModelContainer2; + +export interface TestSubModel extends Model { + email: string; + circular?: TestModel; +} + +export interface TestSubModel2 extends TestSubModel { + testSubModel2: boolean; +} + +export interface HeritageTestModel extends TypeAlias4, Partial> {} + +export interface HeritageBaseModel { + value: string; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface HeritageTestModel2 extends HeritageBaseModel {} + +export interface DefaultTestModel> { + t: GenericRequest; + u: DefaultArgs; +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace TestSubModelNamespace { + export interface TestSubModelNS extends TestSubModel { + testSubModelNS: boolean; + } +} + +export interface BooleanResponseModel { + success: boolean; +} + +export interface TruncationTestModel { + demo01: string; + demo02: string; + demo03: string; + demo04: string; + demo05: string; + demo06: string; + demo07: string; + demo08: string; + demo09: string; + demo10: string; + demo11: string; + demo12: string; + demo13: string; + demo14: string; + demo15: string; + demo16: string; + demo17: string; + d?: string; +} + +export interface UserResponseModel { + id: number; + name: string; +} + +export class ParameterTestModel { + public firstname!: string; + public lastname!: string; + /** + * @isInt + * @minimum 1 + * @maximum 100 + */ + public age!: number; + /** + * @isFloat + */ + public weight!: number; + public human!: boolean; + public gender!: Gender; + public nicknames?: string[]; +} + +export class ValidateCustomErrorModel {} + +export class ValidateModel { + /** + * @isFloat Invalid float error message. + */ + public floatValue!: number; + /** + * @isDouble Invalid double error message. + */ + public doubleValue!: number; + /** + * @isInt invalid integer number + */ + public intValue!: number; + /** + * @isLong Custom Required long number. + */ + public longValue!: number; + /** + * @isBoolean + */ + public booleanValue!: boolean; + /** + * @isArray + */ + public arrayValue!: number[]; + /** + * @isDate invalid ISO 8601 date format, i.e. YYYY-MM-DD + */ + public dateValue!: Date; + /** + * @isDateTime + */ + public datetimeValue!: Date; + + /** + * @maximum 10 + */ + public numberMax10!: number; + /** + * @minimum 5 + */ + public numberMin5!: number; + /** + * @maxLength 10 + */ + public stringMax10Lenght!: string; + /** + * @minLength 5 + */ + public stringMin5Lenght!: string; + /** + * @pattern ^[a-zA-Z]+$ + */ + public stringPatternAZaz!: string; + /** + * @pattern `^([A-Z])(?!@)$` + */ + public quotedStringPatternA!: string; + /** + * @maxItems 5 + */ + public arrayMax5Item!: number[]; + /** + * @minItems 2 + */ + public arrayMin2Item!: number[]; + /** + * @uniqueItems + */ + public arrayUniqueItem!: number[]; + + /** + * @ignore + */ + public ignoredProperty!: string; + + public model!: TypeAliasModel1; + public intersection?: TypeAliasModel1 & TypeAliasModel2; + public intersectionNoAdditional?: TypeAliasModel1 & TypeAliasModel2; + public mixedUnion?: string | TypeAliasModel1; + public singleBooleanEnum?: true; + + public typeAliases?: { + word: Word; + fourtyTwo: FourtyTwo; + unionAlias: UnionAlias; + intersectionAlias: IntersectionAlias; + intersectionAlias2?: TypeAliasModelCase2; + unionIntersectionAlias1?: UnionAndIntersectionAlias; + unionIntersectionAlias2?: UnionAndIntersectionAlias; + unionIntersectionAlias3?: UnionAndIntersectionAlias; + unionIntersectionAlias4?: UnionAndIntersectionAlias; + nOLAlias: NolAlias; + genericAlias: GenericAlias; + genericAlias2: GenericAlias; + forwardGenericAlias: ForwardGenericAlias; + }; + + public nullableTypes!: { + /** + * @isInt + * @minimum 5 + */ + numberOrNull: number | null; + wordOrNull: Maybe; + maybeString: Maybe; + justNull: null; + nestedNullable: Array<{ property: 'string literal' | null }>[number]; + }; + + public nestedObject!: { + /** + * @isFloat Invalid float error message. + */ + floatValue: number; + /** + * @isDouble Invalid double error message. + */ + doubleValue: number; + /** + * @isInt invalid integer number + */ + intValue: number; + /** + * @isLong Custom Required long number. + */ + longValue: number; + /** + * @isBoolean + */ + booleanValue: boolean; + /** + * @isArray + */ + arrayValue: number[]; + /** + * @isDate invalid ISO 8601 date format, i.e. YYYY-MM-DD + */ + dateValue: Date; + /** + * @isDateTime + */ + datetimeValue: Date; + + /** + * @maximum 10 + */ + numberMax10: number; + /** + * @minimum 5 + */ + numberMin5: number; + /** + * @maxLength 10 + */ + stringMax10Lenght: string; + /** + * @minLength 5 + */ + stringMin5Lenght: string; + /** + * @pattern ^[a-zA-Z]+$ + */ + stringPatternAZaz: string; + /** + * @pattern `^([A-Z])(?!@)$` + */ + quotedStringPatternA: string; + /** + * @maxItems 5 + */ + arrayMax5Item: number[]; + /** + * @minItems 2 + */ + arrayMin2Item: number[]; + /** + * @uniqueItems + */ + arrayUniqueItem: number[]; + + model: TypeAliasModel1; + intersection?: TypeAliasModel1 & TypeAliasModel2; + intersectionNoAdditional?: TypeAliasModel1 & TypeAliasModel2; + mixedUnion?: string | TypeAliasModel1; + }; +} + +export interface ValidateMapStringToNumber { + [key: string]: number; +} + +export interface ValidateMapStringToAny { + [key: string]: any; +} + +/** + * Gender msg + */ +export enum Gender { + MALE = 'MALE', + FEMALE = 'FEMALE', +} + +export interface ErrorResponseModel { + status: number; + + /** + * @minLength 2 + */ + message: string; + + /** + * @ignore + */ + hidden?: string; +} + +export interface Model { + id: number; +} + +export class TestClassBaseModel { + public id!: number; + public defaultValue1 = 'Default Value 1'; +} + +// bug #158 +export class Account { + public id!: number; +} + +export class PrivateModel { + public stringPropDec1!: string; + + /** + * @minLength 2 + */ + public stringPropDec2!: string; + + /** + * @ignore + */ + public stringPropDec3!: string; + + private hidden!: string; + + constructor( + public id: number, + arg: boolean, + private privArg: boolean, + ) { + this.hidden && this.privArg ? '' : ''; + } +} + +enum MyEnum { + OK, + KO, +} + +interface IndexedInterface { + foo: 'bar'; +} +type IndexedInterfaceAlias = IndexedInterface; +class IndexedClass { + public foo!: 'bar'; +} + +interface Indexed { + foo: { + bar: string; + }; + interface: IndexedInterface; + alias: IndexedInterfaceAlias; + class: IndexedClass; +} +type IndexType = 'foo'; +const fixedArray = ['foo', 'bar'] as const; + +const ClassIndexTest = { + foo: ['id'], +} as const; +type Names = keyof typeof ClassIndexTest; +type ResponseDistribute = T extends Names + ? { + [key in T]: Record<(typeof ClassIndexTest)[T][number], U>; + } + : never; +type IndexRecordAlias = ResponseDistribute; + +/** + * This is a description of TestClassModel + */ +export class TestClassModel extends TestClassBaseModel { + public account!: Account; + public defaultValue2 = 'Default Value 2'; + public enumKeys!: keyof typeof MyEnum; + public keyInterface?: keyof Model; + public indexedType?: Indexed[IndexType]['bar']; + public indexedTypeToInterface?: Indexed['interface']; + public indexedTypeToClass?: Indexed['class']; + public indexedTypeToAlias?: Indexed['alias']; + public indexedResponse?: IndexRecordAlias['foo']; + public indexedResponseObject?: IndexRecordAlias<{ myProp1: string }>['foo']; + public arrayUnion?: (typeof fixedArray)[number]; + public objectUnion?: Record[string]; + /** + * This is a description of a public string property + * + * @minLength 3 + * @maxLength 20 + * @pattern ^[a-zA-Z]+$ + * @example "classPropExample" + * @title Example title + */ + public publicStringProperty!: string; + /** + * @minLength 0 + * @maxLength 10 + */ + public optionalPublicStringProperty?: string; + /** + * @format email + * @pattern `^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$` + */ + public emailPattern?: string; + stringProperty!: string; + protected protectedStringProperty!: string; + public static staticStringProperty: string; + @Deprecated() + public deprecated1?: boolean; + /** @deprecated */ + public deprecated2?: boolean; + @Extension('x-key-1', 'value-1') + @Extension('x-key-2', 'value-2') + public extensionTest?: boolean; + /** + * @extension {"x-key-1": "value-1"} + * @extension {"x-key-2": "value-2"} + */ + public extensionComment?: boolean; + @Example('stringValue') + public stringExample?: string; + @Example({ + id: 1, + label: 'labelValue', + }) + public objectExample?: { + id: number; + label: string; + }; + + /** + * @param publicConstructorVar This is a description for publicConstructorVar + */ + constructor( + public publicConstructorVar: string, + protected protectedConstructorVar: string, + defaultConstructorArgument: string, + readonly readonlyConstructorArgument: string, + public optionalPublicConstructorVar?: string, + @Deprecated() public deprecatedPublicConstructorVar?: boolean, + /** @deprecated */ public deprecatedPublicConstructorVar2?: boolean, + @Deprecated() deprecatedNonPublicConstructorVar?: boolean, + ) { + super(); + } + + public myIgnoredMethod() { + return 'ignored'; + } +} + +type NonFunctionPropertyNames = { + [K in keyof T]: T[K] extends CallableFunction ? never : K; +}[keyof T]; +type NonFunctionProperties = Pick>; +export class GetterClass { + public a!: 'b'; + + get foo() { + return 'bar'; + } + + public toJSON(): NonFunctionProperties & { foo: string } { + return Object.assign({}, this, { foo: this.foo }); + } +} + +export class SimpleClassWithToJSON { + public a: string; + public b: boolean; + + constructor(a: string, b: boolean) { + this.a = a; + this.b = b; + } + + public toJSON(): { a: string } { + return { a: this.a }; + } +} + +export interface GetterInterface { + toJSON(): { foo: string }; +} + +export interface GetterInterfaceHerited extends GetterInterface { + foo: number; +} + +export interface GenericModel { + result: T; + union?: T | string; + nested?: GenericRequest; + heritageCheck?: ThingContainerWithTitle; +} +export interface DefaultArgs { + name: T; +} + +export interface GenericRequest { + name: string; + value: T; +} + +interface ThingContainerWithTitle extends GenericContainer { + // T is TestModel[] here + t: T; + title: string; +} + +interface GenericContainer { + id: string; + // T is number here + list: T[]; + dangling: DanglingContext; +} + +/** + * This should only be used inside GenericContainer to check its + * type argument T gets propagated while TSameNameDifferentValue does not + * and instead, the interface {@link TSameNameDifferentValue} is used. + */ +interface DanglingContext { + number: T; + shouldBeString: TSameNameDifferentValue; +} + +interface TSameNameDifferentValue { + str: string; +} + +type OrderDirection = 'asc' | 'desc'; + +type OrderOptions = `${keyof E & string}:${OrderDirection}`; + +type TemplateLiteralString = OrderOptions; + +export type StringAndNumberTuple = [string, number]; +export type TupleWithRest = [string, ...number[]]; + +export interface TupleTestModel { + fixedTuple: StringAndNumberTuple; + variadicTuple: TupleWithRest; +} diff --git a/tests/tsoa.json b/tests/tsoa.json index 8cb950aaf..8617babcd 100644 --- a/tests/tsoa.json +++ b/tests/tsoa.json @@ -22,7 +22,7 @@ } }, "yaml": true, - "specVersion": 2 + "specVersion": 3.1 }, "routes": { "basePath": "/v1", diff --git a/tests/unit/swagger/config.spec.ts b/tests/unit/swagger/config.spec.ts index 17386f580..67820f0dc 100644 --- a/tests/unit/swagger/config.spec.ts +++ b/tests/unit/swagger/config.spec.ts @@ -79,7 +79,7 @@ describe('Configuration', () => { throw new Error('Should not get here, expecting error regarding unsupported Spec version'); }, err => { - expect(err.message).to.equal('Unsupported Spec version.'); + expect(err.message).to.equal('Unsupported Spec version: -2.'); done(); }, ); diff --git a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts index 0137136d3..378bf2b60 100644 --- a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts +++ b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts @@ -316,7 +316,7 @@ describe('Definition generation', () => { expect(propertySchema.items.type).to.equal('object'); // The "PetShop" Swagger editor considers it valid to have additionalProperties on an array of objects // So, let's convince TypeScript - const itemsAsSchema = propertySchema.items as Swagger.Schema; + const itemsAsSchema = propertySchema.items ; if (currentSpec.specName === 'specWithNoImplicitExtras' || currentSpec.specName === 'dynamicSpecWithNoImplicitExtras') { expect(itemsAsSchema.additionalProperties).to.eq(false, forSpec(currentSpec)); } else { @@ -3571,7 +3571,7 @@ describe('Definition generation', () => { if (!property.items) { throw new Error(`There were no items on the property model.`); } - expect((property.items as Swagger.Schema).$ref).to.equal('#/definitions/TestModel'); + expect((property.items ).$ref).to.equal('#/definitions/TestModel'); }); it('should generate different definitions for a generic primitive', () => { const definition = getValidatedDefinition('GenericModel_string_', currentSpec).properties; @@ -3607,7 +3607,7 @@ describe('Definition generation', () => { if (!property.items) { throw new Error(`There were no items on the property model.`); } - expect((property.items as Swagger.Schema).type).to.equal('string'); + expect((property.items ).type).to.equal('string'); }); it('should propagate generics', () => { const definition = getValidatedDefinition('GenericModel_TestModel-Array_', currentSpec).properties; diff --git a/tests/unit/swagger/pathGeneration/deleteRoutes.spec.ts b/tests/unit/swagger/pathGeneration/deleteRoutes.spec.ts index 20419bff2..c238fedf2 100644 --- a/tests/unit/swagger/pathGeneration/deleteRoutes.spec.ts +++ b/tests/unit/swagger/pathGeneration/deleteRoutes.spec.ts @@ -42,7 +42,7 @@ describe('DELETE route generation', () => { return VerifyPath(spec, route, path => path.delete, isCollection, isNoContent); } - function getVerifiedParameters(actionRoute: string): Swagger.Parameter[] { + function getVerifiedParameters(actionRoute: string): Swagger.Parameter2[] { const path = verifyPath(actionRoute, false, true); if (!path.delete) { throw new Error('No delete operation.'); diff --git a/tests/unit/swagger/pathGeneration/patchRoutes.spec.ts b/tests/unit/swagger/pathGeneration/patchRoutes.spec.ts index 24c20579a..522adee57 100644 --- a/tests/unit/swagger/pathGeneration/patchRoutes.spec.ts +++ b/tests/unit/swagger/pathGeneration/patchRoutes.spec.ts @@ -25,7 +25,7 @@ describe('PATCH route generation', () => { verifyPath(actionRoute, true); }); - const getValidatedParameters = (actionRoute: string): Swagger.Parameter[] => { + const getValidatedParameters = (actionRoute: string): Swagger.Parameter2[] => { const path = verifyPath(actionRoute); if (!path.patch) { throw new Error('No patch operation.'); diff --git a/tests/unit/swagger/pathGeneration/postRoutes.spec.ts b/tests/unit/swagger/pathGeneration/postRoutes.spec.ts index 5194202c2..b8c544ba6 100644 --- a/tests/unit/swagger/pathGeneration/postRoutes.spec.ts +++ b/tests/unit/swagger/pathGeneration/postRoutes.spec.ts @@ -12,7 +12,7 @@ describe('POST route generation', () => { const spec = new SpecGenerator2(metadata, getDefaultExtendedOptions()).GetSpec(); const baseRoute = '/PostTest'; - const getValidatedParameters = (actionRoute: string): Swagger.Parameter[] => { + const getValidatedParameters = (actionRoute: string): Swagger.Parameter2[] => { const path = verifyPath(actionRoute); if (!path.post) { throw new Error('No patch operation.'); diff --git a/tests/unit/swagger/schemaDetails3.spec.ts b/tests/unit/swagger/schemaDetails3.spec.ts index ceb1a34b6..bf468a389 100644 --- a/tests/unit/swagger/schemaDetails3.spec.ts +++ b/tests/unit/swagger/schemaDetails3.spec.ts @@ -1346,7 +1346,7 @@ describe('Definition generation for OpenAPI 3.0.0', () => { expect(propertySchema.items.type).to.equal('object'); // The "PetShop" Swagger editor considers it valid to have additionalProperties on an array of objects // So, let's convince TypeScript - const itemsAsSchema = propertySchema.items as Swagger.Schema; + const itemsAsSchema = propertySchema.items ; if (currentSpec.specName === 'specWithNoImplicitExtras') { expect(itemsAsSchema.additionalProperties).to.eq(false, forSpec(currentSpec)); } else { diff --git a/tests/unit/swagger/schemaDetails31.spec.ts b/tests/unit/swagger/schemaDetails31.spec.ts new file mode 100644 index 000000000..74ca435d0 --- /dev/null +++ b/tests/unit/swagger/schemaDetails31.spec.ts @@ -0,0 +1,4901 @@ +import { ExtendedSpecConfig } from '@tsoa/cli/cli'; +import { MetadataGenerator } from '@tsoa/cli/metadataGeneration/metadataGenerator'; +import { SpecGenerator31 } from '@tsoa/cli/swagger/specGenerator31'; +import { Swagger, Tsoa } from '@tsoa/runtime'; +import { expect } from 'chai'; +import 'mocha'; +import * as os from 'os'; +import { versionMajorMinor } from 'typescript'; +import { getDefaultExtendedOptions } from '../../fixtures/defaultOptions'; +import { EnumDynamicPropertyKey, TestModel } from '../../fixtures/testModel'; + +describe('Definition generation for OpenAPI 3.1.0', () => { + const metadataGet = new MetadataGenerator('./fixtures/controllers/getController.ts').Generate(); + const metadataPost = new MetadataGenerator('./fixtures/controllers/postController31.ts').Generate(); + + const defaultOptions: ExtendedSpecConfig = getDefaultExtendedOptions(); + const optionsWithServers = Object.assign>({}, defaultOptions, { + servers: ['localhost:3000', 'staging.api.com'], + }); + const optionsWithNoAdditional = Object.assign>({}, defaultOptions, { + noImplicitAdditionalProperties: 'silently-remove-extras', + }); + const optionsWithXEnumVarnames = Object.assign>({}, defaultOptions, { + xEnumVarnames: true, + }); + const optionsWithOperationIdTemplate = Object.assign>({}, defaultOptions, { + operationIdTemplate: "{{replace controllerName 'Controller' ''}}_{{titleCase method.name}}", + }); + + interface SpecAndName { + spec: Swagger.Spec31; + /** + * If you want to add another spec here go for it. The reason why we use a string literal is so that tests below won't have "magic string" errors when expected test results differ based on the name of the spec you're testing. + */ + specName: 'specDefault' | 'specWithServers' | 'specWithNoImplicitExtras' | 'specWithXEnumVarnames' | 'specWithOperationIdTemplate'; + } + + const specDefault: SpecAndName = { + spec: new SpecGenerator31(metadataGet, defaultOptions).GetSpec(), + specName: 'specDefault', + }; + const specWithServers: SpecAndName = { + spec: new SpecGenerator31(metadataGet, optionsWithServers).GetSpec(), + specName: 'specWithServers', + }; + const specWithNoImplicitExtras: SpecAndName = { + spec: new SpecGenerator31(metadataGet, optionsWithNoAdditional).GetSpec(), + specName: 'specWithNoImplicitExtras', + }; + const specWithXEnumVarnames: SpecAndName = { + spec: new SpecGenerator31(metadataGet, optionsWithXEnumVarnames).GetSpec(), + specName: 'specWithXEnumVarnames', + }; + + const getComponentSchema = (name: string, chosenSpec: SpecAndName) => { + if (!chosenSpec.spec.components.schemas) { + throw new Error(`No schemas were generated for ${chosenSpec.specName}.`); + } + + const schema = chosenSpec.spec.components.schemas[name]; + + if (!schema) { + throw new Error(`${name} should have been automatically generated in ${chosenSpec.specName}.`); + } + + return schema; + }; + + /** + * This allows us to iterate over specs that have different options to ensure that certain behavior is consistent + */ + const allSpecs: SpecAndName[] = [specDefault, specWithNoImplicitExtras]; + + function forSpec(chosenSpec: SpecAndName): string { + return `for the ${chosenSpec.specName} spec`; + } + + describe('tags', () => { + it('should generate a valid tags array', () => { + expect(specDefault.spec.tags).to.deep.equal([{ name: 'hello', description: 'Endpoints related to greeting functionality' }]); + }); + }); + + describe('servers', () => { + it('should replace the parent schemes element', () => { + expect(specDefault.spec).to.not.have.property('schemes'); + expect(specDefault.spec.servers[0].url).to.match(/^https/); + }); + + it('should replace the parent host element', () => { + expect(specDefault.spec).to.not.have.property('host'); + expect(specDefault.spec.servers[0].url).to.match(/localhost:3000/); + }); + + it('should replace the parent hosts element', () => { + expect(specWithServers.spec.servers[0].url).to.match(/localhost:3000/); + expect(specWithServers.spec.servers[1].url).to.match(/staging\.api\.com/); + }); + + it('should replace the parent basePath element', () => { + expect(specDefault.spec).to.not.have.property('basePath'); + expect(specDefault.spec.servers[0].url).to.match(/\/v1/); + }); + + it('should have relative URL when no host is defined', () => { + const optionsWithNoHost = Object.assign({}, defaultOptions); + delete optionsWithNoHost.host; + + const spec: Swagger.Spec31 = new SpecGenerator31(metadataGet, optionsWithNoHost).GetSpec(); + expect(spec.servers[0].url).to.equal('/v1'); + }); + }); + + describe('info', () => { + it('should generate a valid info object', () => { + expect(specDefault.spec.info).to.deep.equal({ + title: 'Test API', + description: 'Description of a test API', + termsOfService: 'https://example.com/terms/', + contact: { email: 'jane@doe.com', name: 'Jane Doe', url: 'www.jane-doe.com' }, + license: { name: 'MIT' }, + version: '1.0.0', + }); + }); + }); + + describe('security', () => { + it('should replace the parent securityDefinitions with securitySchemes within components', () => { + expect(specDefault.spec).to.not.have.property('securityDefinitions'); + expect(specDefault.spec.components.securitySchemes).to.be.ok; + }); + + it('should replace type: basic with type: http and scheme: basic', () => { + if (!specDefault.spec.components.securitySchemes) { + throw new Error('No security schemes.'); + } + if (!specDefault.spec.components.securitySchemes.basic) { + throw new Error('No basic security scheme.'); + } + + const basic = specDefault.spec.components.securitySchemes.basic as Swagger.BasicSecurity3; + + expect(basic.type).to.equal('http'); + expect(basic.scheme).to.equal('basic'); + }); + + it('should replace type: oauth2 with type password: oauth2 and flows with password', () => { + if (!specDefault.spec.components.securitySchemes) { + throw new Error('No security schemes.'); + } + if (!specDefault.spec.components.securitySchemes.password) { + throw new Error('No basic security scheme.'); + } + + const password = specDefault.spec.components.securitySchemes.password as Swagger.OAuth2Security3; + + expect(password.type).to.equal('oauth2'); + expect(password.flows.password).exist; + + const flow = password.flows.password; + + if (!flow) { + throw new Error('No password flow.'); + } + + expect(flow.tokenUrl).to.equal('/ats-api/auth/token'); + expect(flow.authorizationUrl).to.be.undefined; + + expect(flow.scopes).to.eql({ + user_read: 'user read', + user_write: 'user_write', + }); + }); + + it('should replace type: oauth2 with type application: oauth2 and flows with clientCredentials', () => { + if (!specDefault.spec.components.securitySchemes) { + throw new Error('No security schemes.'); + } + if (!specDefault.spec.components.securitySchemes.application) { + throw new Error('No basic security scheme.'); + } + + const app = specDefault.spec.components.securitySchemes.application as Swagger.OAuth2Security3; + + expect(app.type).to.equal('oauth2'); + expect(app.flows.clientCredentials).exist; + + const flow = app.flows.clientCredentials; + + if (!flow) { + throw new Error('No clientCredentials flow.'); + } + + expect(flow.tokenUrl).to.equal('/ats-api/auth/token'); + expect(flow.authorizationUrl).to.be.undefined; + + expect(flow.scopes).to.eql({ + user_read: 'user read', + user_write: 'user_write', + }); + }); + + it('should replace type: oauth2 with type accessCode: oauth2 and flows with authorizationCode', () => { + if (!specDefault.spec.components.securitySchemes) { + throw new Error('No security schemes.'); + } + if (!specDefault.spec.components.securitySchemes.accessCode) { + throw new Error('No basic security scheme.'); + } + + const authCode = specDefault.spec.components.securitySchemes.accessCode as Swagger.OAuth2Security3; + + expect(authCode.type).to.equal('oauth2'); + expect(authCode.flows.authorizationCode).exist; + + const flow = authCode.flows.authorizationCode; + + if (!flow) { + throw new Error('No authorizationCode flow.'); + } + + expect(flow.tokenUrl).to.equal('/ats-api/auth/token'); + expect(flow.authorizationUrl).to.equal('/ats-api/auth/authorization'); + + expect(flow.scopes).to.eql({ + user_read: 'user read', + user_write: 'user_write', + }); + }); + + it('should replace type: oauth2 with type implicit: oauth2 and flows with implicit', () => { + if (!specDefault.spec.components.securitySchemes) { + throw new Error('No security schemes.'); + } + if (!specDefault.spec.components.securitySchemes.implicit) { + throw new Error('No basic security scheme.'); + } + + const imp = specDefault.spec.components.securitySchemes.implicit as Swagger.OAuth2Security3; + + expect(imp.type).to.equal('oauth2'); + expect(imp.flows.implicit).exist; + + const flow = imp.flows.implicit; + + if (!flow) { + throw new Error('No implicit flow.'); + } + + expect(flow.tokenUrl).to.be.undefined; + expect(flow.authorizationUrl).to.equal('/ats-api/auth/authorization'); + + expect(flow.scopes).to.eql({ + user_read: 'user read', + user_write: 'user_write', + }); + }); + + it('should allow bearer scheme', () => { + const bearer: Swagger.BearerSecurity3 = { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }; + const optionsWithBearer = Object.assign({}, defaultOptions, { + securityDefinitions: { + bearer, + }, + }); + + const metadata = new MetadataGenerator('./fixtures/controllers/exampleController.ts').Generate(); + const exampleSpec = new SpecGenerator31(metadata, optionsWithBearer).GetSpec(); + + expect(exampleSpec.components.securitySchemes).to.eql({ + bearer, + }); + }); + + it('should allow openId scheme', () => { + const openId: Swagger.OpenIDSecurity = { + type: 'openIdConnect', + openIdConnectUrl: 'https://example.com/.well-known/openid-configuration', + }; + const optionsWithOpenId = Object.assign({}, defaultOptions, { + securityDefinitions: { + openId, + }, + }); + + const metadata = new MetadataGenerator('./fixtures/controllers/exampleController.ts').Generate(); + const exampleSpec = new SpecGenerator31(metadata, optionsWithOpenId).GetSpec(); + + expect(exampleSpec.components.securitySchemes).to.eql({ + openId, + }); + }); + }); + + describe('example comment', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/exampleController.ts').Generate(); + const exampleSpec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + + it('should generate single example for model', () => { + if (exampleSpec.components === undefined) { + throw new Error('No components find!'); + } + if (exampleSpec.components.schemas === undefined) { + throw new Error('No schemas find!'); + } + + const example = exampleSpec.components.schemas?.Location.example; + expect(example).to.be.not.undefined; + expect(example).to.deep.equal({ + contry: '123', + city: '456', + }); + }); + + describe('should generate multiple example for Parameters', () => { + it('@Path parameter in Get method', () => { + const pathParams = exampleSpec.paths['/ExampleTest/path/{path}'].get!.parameters![0]; + expect(pathParams.example).to.be.undefined; + expect(pathParams.examples).to.deep.equal({ 'Example 1': { value: 'an_example_path' }, 'Example 2': { value: 'an_example_path2' } }); + }); + + it('@Query parameter in Get method', () => { + const queryParams = exampleSpec.paths['/ExampleTest/query'].get!.parameters![0]; + expect(queryParams.example).to.be.undefined; + expect(queryParams.examples).to.deep.equal({ 'Example 1': { value: 'an_example_query' }, 'Example 2': { value: 'an_example_query2' } }); + }); + + it('@Header parameter in Get method', () => { + const headerParams = exampleSpec.paths['/ExampleTest/header'].get!.parameters![0]; + expect(headerParams.example).to.be.undefined; + expect(headerParams.examples).to.deep.equal({ 'Example 1': { value: 'aaaaaaLongCookie' }, 'Example 2': { value: 'aaaaaaLongCookie2' } }); + }); + + it('@Body parameter in Post method', () => { + const postBodyParams = exampleSpec.paths['/ExampleTest/post_body'].post?.requestBody?.content?.['application/json']; + expect(postBodyParams?.example).to.be.undefined; + expect(postBodyParams?.examples).to.deep.equal({ + 'Example 1': { + value: { + contry: '1', + city: '1', + }, + }, + 'Example 2': { + value: { + contry: '2', + city: '2', + }, + }, + }); + }); + + it('Single @BodyProp parameters in Post method', () => { + const postBodyParams = exampleSpec.paths['/ExampleTest/post_body_prop_single'].post?.requestBody?.content?.['application/json']; + expect(postBodyParams?.schema?.required).to.have.lengthOf(1); + expect(postBodyParams?.schema?.properties).to.have.property('prop1'); + }); + + it('Two @BodyProp parameters in Post method', () => { + const postBodyParams = exampleSpec.paths['/ExampleTest/post_body_prop'].post?.requestBody?.content?.['application/json']; + expect(postBodyParams?.schema?.required).to.have.lengthOf(2); + expect(postBodyParams?.schema?.properties).to.have.property('prop1'); + expect(postBodyParams?.schema?.properties).to.have.property('prop2'); + }); + + it('Two parameter with @Body and @Path in Post method', () => { + const path = exampleSpec.paths['/ExampleTest/two_parameter/{s}'].post!; + + const bodyParams = path.requestBody?.content?.['application/json']; + expect(bodyParams?.example).to.be.undefined; + expect(bodyParams?.examples).to.deep.equal({ + 'Example 1': { + value: { + contry: '1', + city: '1', + }, + }, + 'Example 2': { + value: { + contry: '2', + city: '2', + }, + }, + }); + + const pathParams = path.parameters![0]; + expect(pathParams?.example).to.be.undefined; + expect(pathParams?.examples).to.deep.equal({ 'Example 1': { value: 'aa0' }, 'Example 2': { value: 'aa1' }, 'Example 3': { value: 'aa2' } }); + }); + + it('Array with two @Body parameters in Post method', () => { + const bodyParams = exampleSpec.paths['/ExampleTest/array_with_object'].post?.requestBody?.content?.['application/json']; + expect(bodyParams?.example).to.be.undefined; + expect(bodyParams?.examples).to.deep.equal({ + 'Example 1': { + value: [ + { + contry: '1', + city: '1', + }, + { + contry: '2', + city: '2', + }, + ], + }, + 'Example 2': { + value: [ + { + contry: '22', + city: '22', + }, + { + contry: '33', + city: '33', + }, + ], + }, + }); + }); + }); + + it('Supports custom example labels', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/exampleController.ts').Generate(); + const exampleSpec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + + const examples = exampleSpec.paths['/ExampleTest/CustomBodyExampleLabels']?.post?.requestBody!.content!['application/json'].examples; + + expect(examples).to.deep.eq({ + '': { + value: 'No Custom Label', + }, + CustomLabel: { value: 'CustomLabel' }, + CustomLabel2: { value: 'CustomLabel2' }, + 'Example 1': { value: 'Unlabeled 1' }, + 'Example 2': { value: 'Another unlabeled one' }, + 'Example 3': { + value: 'Unlabeled 2', + }, + }); + }); + }); + + describe('paths', () => { + describe('uploadedFiles', () => { + /** + * Tests according to openapi v3.1.0 specs + * @link http://spec.openapis.org/oas/v3.1.0 + * Validated and tested GUI with swagger.io + * @link https://editor.swagger.io/ + */ + it('should have requestBody with single multipart/form-data', () => { + // Act + const specPost = new SpecGenerator31(metadataPost, getDefaultExtendedOptions()).GetSpec(); + const pathPost = specPost.paths['/PostTest/File'].post; + if (!pathPost) { + throw new Error('PostTest file method not defined'); + } + if (!pathPost.requestBody) { + throw new Error('PostTest file method has no requestBody'); + } + + // Assert + expect(pathPost.parameters).to.have.length(0); + expect(pathPost.requestBody).to.deep.equal({ + required: true, + content: { + 'multipart/form-data': { + schema: { + type: 'object', + properties: { + someFile: { + type: 'string', + format: 'binary', + }, + }, + required: ['someFile'], + }, + }, + }, + }); + }); + it('should consume multipart/form-data and have formData parameter with no name', () => { + // Act + const specPost = new SpecGenerator31(metadataPost, getDefaultExtendedOptions()).GetSpec(); + const pathPost = specPost.paths['/PostTest/FileWithoutName'].post; + if (!pathPost) { + throw new Error('PostTest file method not defined'); + } + if (!pathPost.requestBody) { + throw new Error('PostTest file method has no requestBody'); + } + + // Assert + expect(pathPost.parameters).to.have.length(0); + expect(pathPost.requestBody).to.deep.equal({ + required: true, + content: { + 'multipart/form-data': { + schema: { + type: 'object', + properties: { + aFile: { + type: 'string', + format: 'binary', + }, + }, + required: ['aFile'], + }, + }, + }, + }); + }); + it('should consume multipart/form-data and have multiple formData parameter', () => { + // Act + const specPost = new SpecGenerator31(metadataPost, getDefaultExtendedOptions()).GetSpec(); + const pathPost = specPost.paths['/PostTest/ManyFilesAndFormFields'].post; + if (!pathPost) { + throw new Error('PostTest file method not defined'); + } + if (!pathPost.requestBody) { + throw new Error('PostTest file method has no requestBody'); + } + + // Assert + expect(pathPost.parameters).to.have.length(0); + expect(pathPost.requestBody).to.deep.equal({ + required: true, + content: { + 'multipart/form-data': { + schema: { + type: 'object', + properties: { + someFiles: { + type: 'array', + items: { + type: 'string', + format: 'binary', + }, + }, + a: { + type: 'string', + }, + c: { + type: 'string', + }, + }, + required: ['someFiles', 'a', 'c'], + }, + }, + }, + }); + }); + it('should not treat optional file as required', () => { + // Act + const specPost = new SpecGenerator31(metadataPost, getDefaultExtendedOptions()).GetSpec(); + const pathPost = specPost.paths['/PostTest/FileOptional'].post; + if (!pathPost) { + throw new Error('PostTest file method not defined'); + } + if (!pathPost.requestBody) { + throw new Error('PostTest file method has no requestBody'); + } + + // Assert + expect(pathPost.parameters).to.have.length(0); + expect(pathPost.requestBody).to.deep.equal({ + required: false, + content: { + 'multipart/form-data': { + schema: { + type: 'object', + properties: { + optionalFile: { + type: 'string', + format: 'binary', + }, + }, + }, + }, + }, + }); + }); + it('should consume multipart/form-data and have multiple formData parameter with optional descriptions', () => { + // Act + const specPost = new SpecGenerator31(metadataPost, getDefaultExtendedOptions()).GetSpec(); + const pathPost = specPost.paths['/PostTest/DescriptionOfFileAndFormFields'].post; + if (!pathPost) { + throw new Error('PostTest file method not defined'); + } + if (!pathPost.requestBody) { + throw new Error('PostTest file method has no requestBody'); + } + + // Assert + expect(pathPost.parameters).to.have.length(0); + expect(pathPost.requestBody).to.deep.equal({ + required: true, + content: { + 'multipart/form-data': { + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + description: 'File description of multipart', + }, + a: { type: 'string', description: 'FormField description of multipart' }, + c: { type: 'string' }, + }, + required: ['file', 'a', 'c'], + }, + }, + }, + }); + }); + }); + describe('requestBody', () => { + it('should replace the body parameter with a requestBody', () => { + const specPost = new SpecGenerator31(metadataPost, JSON.parse(JSON.stringify(defaultOptions))).GetSpec(); + + if (!specPost.paths) { + throw new Error('Paths are not defined.'); + } + if (!specPost.paths['/PostTest']) { + throw new Error('PostTest path not defined.'); + } + if (!specPost.paths['/PostTest'].post) { + throw new Error('PostTest post method not defined.'); + } + + const method = specPost.paths['/PostTest'].post; + + if (!method || !method.parameters) { + throw new Error('Parameters not defined.'); + } + + expect(method.parameters).to.deep.equal([]); + + if (!method.requestBody) { + throw new Error('Request body not defined.'); + } + + expect(method.requestBody.content['application/json'].schema).to.deep.equal({ + $ref: '#/components/schemas/TestModel', + }); + }); + }); + + describe('hidden paths', () => { + it('should not contain hidden paths', () => { + const metadataHiddenMethod = new MetadataGenerator('./fixtures/controllers/hiddenMethodController.ts').Generate(); + const specHiddenMethod = new SpecGenerator31(metadataHiddenMethod, JSON.parse(JSON.stringify(defaultOptions))).GetSpec(); + + expect(specHiddenMethod.paths).to.have.keys(['/Controller/normalGetMethod', '/Controller/hiddenQueryMethod']); + }); + + it('should not contain hidden query params', () => { + const metadataHidden = new MetadataGenerator('./fixtures/controllers/hiddenMethodController.ts').Generate(); + const specHidden = new SpecGenerator31(metadataHidden, JSON.parse(JSON.stringify(defaultOptions))).GetSpec(); + + if (!specHidden.paths) { + throw new Error('Paths are not defined.'); + } + if (!specHidden.paths['/Controller/hiddenQueryMethod']) { + throw new Error('hiddenQueryMethod path not defined.'); + } + if (!specHidden.paths['/Controller/hiddenQueryMethod'].get) { + throw new Error('hiddenQueryMethod get method not defined.'); + } + + const method = specHidden.paths['/Controller/hiddenQueryMethod'].get; + expect(method.parameters).to.have.lengthOf(1); + + const normalParam = method.parameters![0]; + expect(normalParam.in).to.equal('query'); + expect(normalParam.name).to.equal('normalParam'); + expect(normalParam.required).to.be.true; + expect(normalParam.schema.type).to.equal('string'); + }); + + it('should not contain paths for hidden controller', () => { + const metadataHiddenController = new MetadataGenerator('./fixtures/controllers/hiddenController.ts').Generate(); + const specHiddenController = new SpecGenerator31(metadataHiddenController, JSON.parse(JSON.stringify(defaultOptions))).GetSpec(); + + expect(specHiddenController.paths).to.be.empty; + }); + }); + + describe('methods', () => { + describe('operationId', () => { + // for backwards compatibility. + it('should default to title-cased method name.', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/exampleController.ts').Generate(); + const exampleSpec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + const operationId = exampleSpec.paths['/ExampleTest/post_body']?.post?.operationId; + expect(operationId).to.eq('Post'); + }); + it('should utilize operationIdTemplate if set.', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/exampleController.ts').Generate(); + const exampleSpec = new SpecGenerator31(metadata, optionsWithOperationIdTemplate).GetSpec(); + const operationId = exampleSpec.paths['/ExampleTest/post_body']?.post?.operationId; + expect(operationId).to.eq('ExampleTest_Post'); + }); + }); + + describe('responses', () => { + describe('should generate headers from method reponse decorator.', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/responseHeaderController.ts').Generate(); + const responseSpec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + + it('proper schema ref for header class.', () => { + const pathsWithHeaderClass = ['SuccessResponseWithHeaderClass', 'ResponseWithHeaderClass', 'TsoaResponseWithHeaderClass']; + pathsWithHeaderClass.forEach((path: string) => { + const responses = responseSpec.paths[`/ResponseHeader/${path}`].get?.responses; + expect(responses?.[200]?.headers?.ResponseHeader).to.deep.eq({ + schema: { + $ref: '#/components/schemas/ResponseHeader', + }, + description: "response header's description", + }); + }); + }); + it('with header object.', () => { + expect(responseSpec.paths['/ResponseHeader/SuccessResponseWithObject'].get?.responses?.[200]?.headers).to.deep.eq({ + linkA: { + required: true, + schema: { type: 'string' }, + description: undefined, + }, + linkB: { + required: true, + schema: { type: 'array', items: { type: 'string' } }, + description: undefined, + }, + linkOpt: { + required: false, + schema: { type: 'string' }, + description: undefined, + }, + }); + expect(responseSpec.paths['/ResponseHeader/ResponseWithObject'].get?.responses?.[200]?.headers).to.deep.eq({ + linkC: { + required: true, + schema: { type: 'string' }, + description: undefined, + }, + linkD: { + required: true, + schema: { type: 'array', items: { type: 'string' } }, + description: undefined, + }, + linkOpt: { + required: false, + schema: { type: 'string' }, + description: undefined, + }, + }); + expect(responseSpec.paths['/ResponseHeader/TsoaResponseWithObject'].get?.responses?.[200]?.headers).to.deep.eq({ + linkE: { + required: true, + schema: { type: 'string' }, + description: undefined, + }, + linkF: { + required: true, + schema: { type: 'array', items: { type: 'string' } }, + description: undefined, + }, + linkOpt: { + required: false, + schema: { type: 'string' }, + description: undefined, + }, + }); + }); + }); + describe('should generate headers from class response decorator.', () => { + it('with header class.', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/commonResponseHeaderClassController.ts').Generate(); + const responseSpec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + const paths = ['Response1', 'Response2']; + paths.forEach((path: string) => { + const responses = responseSpec.paths[`/CommonResponseHeaderClass/${path}`].get?.responses; + expect(responses?.[200]?.headers?.CommonResponseHeader).to.deep.eq({ + schema: { + $ref: '#/components/schemas/CommonResponseHeader', + }, + description: "Common response header's description", + }); + }); + }); + + it('with header object.', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/commonResponseHeaderObjectController.ts').Generate(); + const responseSpec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + const paths = ['Response1', 'Response2']; + paths.forEach((path: string) => { + const responses = responseSpec.paths[`/CommonResponseHeaderObject/${path}`].get?.responses; + expect(responses?.[200]?.headers).to.deep.eq({ + objectA: { + required: true, + schema: { type: 'string' }, + description: undefined, + }, + objectB: { + required: true, + schema: { type: 'array', items: { type: 'string' } }, + description: undefined, + }, + objectC: { + required: false, + schema: { type: 'string' }, + description: undefined, + }, + }); + }); + }); + }); + + it('Should not generate models with hidden controller referenced.', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/commonResponseHiddenModelController.ts').Generate(); + const responseSpec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + expect(responseSpec.components.schemas).to.be.deep.eq({}); + }); + + describe('media types', () => { + let mediaTypeTest: Swagger.Spec31; + let requestAcceptHeaderTest: Swagger.Spec31; + + before(function () { + this.timeout(10_000); + const mediaTypeMetadata = new MetadataGenerator('./fixtures/controllers/mediaTypeController.ts').Generate(); + mediaTypeTest = new SpecGenerator31(mediaTypeMetadata, getDefaultExtendedOptions()).GetSpec(); + + const requestAcceptHeaderMetadata = new MetadataGenerator('./fixtures/controllers/requestExpressController').Generate(); + requestAcceptHeaderTest = new SpecGenerator31(requestAcceptHeaderMetadata, getDefaultExtendedOptions()).GetSpec(); + }); + + it('Should use controller Produces decorator as a default media type', () => { + const [mediaTypeOk] = Object.keys(mediaTypeTest.paths['/MediaTypeTest/Default/{userId}']?.get?.responses?.[200]?.content ?? {}); + const [mediaTypeNotFound] = Object.keys(mediaTypeTest.paths['/MediaTypeTest/Default/{userId}']?.get?.responses?.[404]?.content ?? {}); + + expect(mediaTypeOk).to.eql('application/vnd.mycompany.myapp+json'); + expect(mediaTypeNotFound).to.eql('application/vnd.mycompany.myapp+json'); + }); + + it('Should be possible to define multiple media types on controller level', () => { + const [v1, v2] = Object.keys(requestAcceptHeaderTest.paths['/RequestAcceptHeaderTest/Default/{userId}']?.get?.responses?.[200]?.content ?? {}); + + expect(v1).to.eql('application/vnd.mycompany.myapp+json'); + expect(v2).to.eql('application/vnd.mycompany.myapp.v2+json'); + }); + + it('Should generate custom media type from method Produces decorator', () => { + const [mediaType] = Object.keys(mediaTypeTest.paths['/MediaTypeTest/Custom/security.txt']?.get?.responses?.[200]?.content ?? {}); + const [v1, v2, v3, v4] = Object.keys(requestAcceptHeaderTest.paths['/RequestAcceptHeaderTest/Multi/{userId}']?.get?.responses?.[200]?.content ?? {}); + + expect(mediaType).to.eql('text/plain'); + expect(v1).to.eql('application/vnd.mycompany.myapp+json'); + expect(v2).to.eql('application/vnd.mycompany.myapp.v2+json'); + expect(v3).to.eql('application/vnd.mycompany.myapp.v3+json'); + expect(v4).to.eql('application/vnd.mycompany.myapp.v4+json'); + }); + + it('Should generate custom media types from method reponse decorators', () => { + const [mediaTypeAccepted] = Object.keys(mediaTypeTest.paths['/MediaTypeTest/Custom']?.post?.responses?.[202]?.content ?? {}); + const [mediaTypeBadRequest] = Object.keys(mediaTypeTest.paths['/MediaTypeTest/Custom']?.post?.responses?.[400]?.content ?? {}); + const [v3, v4] = Object.keys(requestAcceptHeaderTest.paths['/RequestAcceptHeaderTest/Multi']?.post?.responses?.[202]?.content ?? {}); + const [br1, br2] = Object.keys(requestAcceptHeaderTest.paths['/RequestAcceptHeaderTest/Multi']?.post?.responses?.[400]?.content ?? {}); + + expect(mediaTypeAccepted).to.eql('application/vnd.mycompany.myapp.v2+json'); + expect(mediaTypeBadRequest).to.eql('application/problem+json'); + expect(v3).to.eql('application/vnd.mycompany.myapp.v3+json'); + expect(v4).to.eql('application/vnd.mycompany.myapp.v4+json'); + expect(br1).to.eql('application/problem+json'); + expect(br2).to.eql('application/json'); + }); + + it('Should generate custom media types from header in @Res decorator', () => { + const [mediaTypeConflict] = Object.keys(mediaTypeTest.paths['/MediaTypeTest/Custom']?.post?.responses?.[409]?.content ?? {}); + + expect(mediaTypeConflict).to.eql('application/problem+json'); + }); + + it('Should generate custom media type of request body from method Consumes decorator', () => { + const [bodyMediaTypeDefault] = Object.keys(mediaTypeTest.paths['/MediaTypeTest/Default']?.post?.requestBody?.content ?? {}); + const [bodyMediaTypeCustom] = Object.keys(mediaTypeTest.paths['/MediaTypeTest/Custom']?.post?.requestBody?.content ?? {}); + + expect(bodyMediaTypeDefault).to.eql('application/json'); + expect(bodyMediaTypeCustom).to.eql('application/vnd.mycompany.myapp.v2+json'); + }); + }); + + it('Supports multiple examples', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/exampleController.ts').Generate(); + const exampleSpec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + + const examples = exampleSpec.paths['/ExampleTest/MultiResponseExamples']?.get?.responses?.[200]?.content?.['application/json'].examples; + + expect(examples).to.deep.eq({ + 'Example 1': { + value: 'test 1', + }, + 'Example 2': { + value: 'test 2', + }, + }); + }); + + it('Supports custom example labels', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/exampleController.ts').Generate(); + const exampleSpec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + const examples = exampleSpec.paths['/ExampleTest/CustomExampleLabels']?.get?.responses?.[400]?.content?.['application/json'].examples; + + expect(examples).to.deep.eq({ + NoSuchCountry: { value: { errorMessage: 'No such country', errorCode: 40000 } }, + '': { + value: { + errorCode: 40000, + errorMessage: 'No custom label', + }, + }, + 'Example 1': { value: 'Unlabeled 1' }, + 'Example 2': { value: 'Another unlabeled one' }, + NoSuchCity: { value: { errorMessage: 'No such city', errorCode: 40000 } }, + 'Example 3': { + value: { + session: 'asd.f', + }, + }, + 'Example 4': { + value: { + errorCode: 40000, + errorMessage: 'No custom label', + }, + }, + }); + }); + + it('Supports example with produces', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/exampleController.ts').Generate(); + const exampleSpec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + const examples = exampleSpec.paths['/ExampleTest/ResponseExampleWithProduces']?.get?.responses?.[200]?.content?.['text/plain'].examples; + + expect(examples).to.deep.eq({ + OneExample: { + value: 'test example response', + }, + }); + }); + + it('Supports mutli examples with produces', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/exampleController.ts').Generate(); + const exampleSpec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + const produces = exampleSpec.paths['/ExampleTest/ResponseMultiExamplesWithProduces']?.get?.responses?.[200]?.content; + + expect(produces).to.deep.eq({ + 'application/json': { + examples: { + OneExample: { + value: 'test example response', + }, + TwoExample: { + value: 'test example response', + }, + }, + schema: { + type: 'string', + }, + }, + 'text/plain': { + examples: { + OneExample: { + value: 'test example response', + }, + TwoExample: { + value: 'test example response', + }, + }, + schema: { + type: 'string', + }, + }, + }); + }); + + it('uses the correct imported value for the @Example<>', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/exampleController.ts').Generate(); + const exampleSpec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + const examples = exampleSpec.paths['/ExampleTest/ResponseExampleWithImportedValue']?.get?.responses?.[200]?.content?.['application/json'].examples; + + expect(examples).to.deep.eq({ + 'Example 1': { + value: 'test example response', + }, + }); + }); + + it('uses the correct imported value for the @Example<> with label', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/exampleController.ts').Generate(); + const exampleSpec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + const examples = exampleSpec.paths['/ExampleTest/ResponseExampleWithLabel']?.get?.responses?.[200]?.content?.['application/json'].examples; + + expect(examples).to.deep.eq({ + Custom_label: { + value: 'test example response', + }, + }); + }); + + it('uses the correct imported value for multiple @Example<> with label', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/exampleController.ts').Generate(); + const exampleSpec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + const examples = exampleSpec.paths['/ExampleTest/ResponseMultiExampleWithLabel']?.get?.responses?.[200]?.content?.['application/json'].examples; + + expect(examples).to.deep.eq({ + OneExample: { + value: 'test example response', + }, + AnotherExample: { + value: 'another example', + }, + 'Example 1': { + value: 'no label example', + }, + }); + }); + + it('uses minus prefix token number value at @Example model', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/exampleController.ts').Generate(); + const exampleSpec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + const examples = exampleSpec.paths['/ExampleTest/ResponseExampleWithMinusOperatorPrefixValue']?.get?.responses?.[200]?.content?.['application/json'].examples; + + expect(examples).to.deep.eq({ + 'Example 1': { + value: { + id: -1, + description: 'test doc des', + }, + }, + }); + }); + + it('uses plus prefix token number value at @Example model', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/exampleController.ts').Generate(); + const exampleSpec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + const examples = exampleSpec.paths['/ExampleTest/ResponseExampleWithPlusOperatorPrefixValue']?.get?.responses?.[200]?.content?.['application/json'].examples; + + expect(examples).to.deep.eq({ + 'Example 1': { + value: { + id: 1, + description: 'test doc des', + }, + }, + }); + }); + }); + + describe('deprecation', () => { + it('marks deprecated methods as deprecated', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/deprecatedController.ts').Generate(); + const deprecatedSpec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + + expect(deprecatedSpec.paths['/Controller/deprecatedGetMethod']?.get?.deprecated).to.eql(true); + expect(deprecatedSpec.paths['/Controller/deprecatedGetMethod2']?.get?.deprecated).to.eql(true); + }); + + it('marks deprecated parameters as deprecated', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/parameterController.ts').Generate(); + const deprecatedSpec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + + const parameters = deprecatedSpec.paths['/ParameterTest/ParameterDeprecated']?.post?.parameters ?? []; + expect(parameters.map(param => param.deprecated)).to.eql([undefined, true, true]); + }); + }); + }); + + describe('form field deprecation', () => { + it('should consume multipart/form-data and have deprecated formData parameter', () => { + // Act + const specPost = new SpecGenerator31(metadataPost, getDefaultExtendedOptions()).GetSpec(); + const pathPost = specPost.paths['/PostTest/DeprecatedFormField'].post; + if (!pathPost) { + throw new Error('PostTest file method not defined'); + } + if (!pathPost.requestBody) { + throw new Error('PostTest file method has no requestBody'); + } + + // Assert + expect(pathPost.parameters).to.have.length(0); + expect(pathPost.requestBody).to.deep.equal({ + required: true, + content: { + 'multipart/form-data': { + schema: { + type: 'object', + properties: { + a: { + type: 'string', + }, + dontUse: { + type: 'string', + deprecated: true, + }, + }, + required: ['a'], + }, + }, + }, + }); + }); + }); + }); + + describe('components', () => { + describe('schemas', () => { + it('should replace definitions with schemas', () => { + if (!specDefault.spec.components.schemas) { + throw new Error('Schemas not defined.'); + } + + expect(specDefault.spec).to.not.have.property('definitions'); + expect(specDefault.spec.components.schemas.TestModel).to.exist; + }); + + it('should replace x-nullable with nullable', () => { + if (!specDefault.spec.components.schemas) { + throw new Error('Schemas not defined.'); + } + if (!specDefault.spec.components.schemas.TestModel) { + throw new Error('TestModel not defined.'); + } + + const testModel = specDefault.spec.components.schemas.TestModel; + + if (!testModel.properties) { + throw new Error('testModel.properties should have been a truthy object'); + } + expect(testModel.properties.optionalString).to.not.have.property('x-nullable'); + expect(testModel.properties.optionalString.nullable).to.be.undefined; + }); + }); + }); + + describe('xEnumVarnames', () => { + it('EnumNumberValue', () => { + const schema = getComponentSchema('EnumNumberValue', specWithXEnumVarnames); + expect(schema['x-enum-varnames']).to.eql(['VALUE_0', 'VALUE_1', 'VALUE_2']); + }); + it('EnumStringValue', () => { + const schema = getComponentSchema('EnumStringValue', specWithXEnumVarnames); + expect(schema['x-enum-varnames']).to.eql(['EMPTY', 'VALUE_1', 'VALUE_2']); + }); + it('EnumStringNumberValue', () => { + const schema = getComponentSchema('EnumStringNumberValue', specWithXEnumVarnames); + expect(schema['x-enum-varnames']).to.eql(['VALUE_0', 'VALUE_1', 'VALUE_2']); + }); + }); + + allSpecs.forEach(currentSpec => { + describe(`for ${currentSpec.specName}`, () => { + describe('should set additionalProperties to false if noImplicitAdditionalProperties is set to "throw-on-extras" (when there are no dictionary or any types)', () => { + // Arrange + + // Assert + if (!currentSpec.spec.components.schemas) { + throw new Error('spec.components.schemas should have been truthy'); + } + + const interfaceModelName = 'TestModel'; + + /** + * By creating a record of "keyof T" we ensure that contributors will need add a test for any new property that is added to the model + */ + const assertionsPerProperty: Record void> = { + id: (propertyName, propertySchema) => { + // should generate properties from extended interface + expect(propertySchema.type).to.eq('number', `for property ${propertyName}.type`); + expect(propertySchema.format).to.eq('double', `for property ${propertyName}.format`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + }, + numberValue: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq('number', `for property ${propertyName}.type`); + expect(propertySchema.format).to.eq('double', `for property ${propertyName}.format`); + const descriptionFromJsDocs = 'This is a description of this model property, numberValue'; + expect(propertySchema.description).to.eq(descriptionFromJsDocs, `for property ${propertyName}.description`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + }, + // tslint:disable-next-line: object-literal-sort-keys + numberArray: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq('array', `for property ${propertyName}.type`); + if (!propertySchema.items) { + throw new Error(`There was no 'items' property on ${propertyName}.`); + } + expect(propertySchema.items.type).to.eq('number', `for property ${propertyName}.items.type`); + expect(propertySchema.items.format).to.eq('double', `for property ${propertyName}.items.format`); + expect(propertySchema.description).to.eq(undefined, `for property ${propertyName}.description`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + }, + // tslint:disable-next-line: object-literal-sort-keys + numberArrayReadonly: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq('array', `for property ${propertyName}.type`); + if (!propertySchema.items) { + throw new Error(`There was no 'items' property on ${propertyName}.`); + } + expect(propertySchema.items.type).to.eq('number', `for property ${propertyName}.items.type`); + expect(propertySchema.items.format).to.eq('double', `for property ${propertyName}.items.format`); + expect(propertySchema.description).to.eq(undefined, `for property ${propertyName}.description`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + }, + stringValue: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq('string', `for property ${propertyName}.type`); + expect(propertySchema.format).to.eq('password', `for property ${propertyName}.format`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + }, + stringArray: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq('array', `for property ${propertyName}.type`); + if (!propertySchema.items) { + throw new Error(`There was no 'items' property on ${propertyName}.`); + } + expect(propertySchema.items.type).to.eq('string', `for property ${propertyName}.items.type`); + expect(propertySchema.items.format).to.eq(undefined, `for property ${propertyName}.items.format`); + expect(propertySchema.description).to.eq(undefined, `for property ${propertyName}.description`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + }, + boolValue: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq('boolean', `for property ${propertyName}.type`); + expect(propertySchema.default).to.eq(true, `for property ${propertyName}.default`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + }, + boolArray: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq('array', `for property ${propertyName}.type`); + if (!propertySchema.items) { + throw new Error(`There was no 'items' property on ${propertyName}.`); + } + expect(propertySchema.items.type).to.eq('boolean', `for property ${propertyName}.items.type`); + expect(propertySchema.items.default).to.eq(undefined, `for property ${propertyName}.items.default`); + expect(propertySchema.description).to.eq(undefined, `for property ${propertyName}.description`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + }, + undefinedValue: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq(undefined, `for property ${propertyName}.type`); + }, + objLiteral: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.include({ + properties: { + name: { + type: 'string', + default: undefined, + description: undefined, + format: undefined, + example: undefined, + }, + deprecatedSubProperty: { type: 'number', default: undefined, description: undefined, format: 'double', example: undefined, deprecated: true }, + nested: { + properties: { + additionals: { + properties: {}, + type: 'object', + default: undefined, + description: undefined, + format: undefined, + example: undefined, + additionalProperties: { + $ref: '#/components/schemas/TypeAliasModel1', + }, + }, + allNestedOptional: { + properties: { + one: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined }, + two: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined }, + }, + type: 'object', + default: undefined, + description: undefined, + format: undefined, + example: undefined, + }, + bool: { type: 'boolean', default: undefined, description: undefined, format: undefined, example: undefined }, + optional: { format: 'double', type: 'number', default: undefined, description: undefined, example: undefined }, + }, + required: ['allNestedOptional', 'bool'], + type: 'object', + default: undefined, + description: undefined, + format: undefined, + example: undefined, + }, + }, + required: ['name'], + type: 'object', + }); + }, + notDeprecatedProperty: (propertyName, propertySchema) => { + expect(propertySchema.deprecated).to.eq(undefined, `for property ${propertyName}.deprecated`); + }, + propertyOfDeprecatedType: (propertyName, propertySchema) => { + // property is not explicitly deprecated, but the type's schema is + expect(propertySchema.deprecated).to.eq(undefined, `for property ${propertyName}.deprecated`); + const typeSchema = currentSpec.spec.components.schemas!['DeprecatedType']; + expect(typeSchema.deprecated).to.eq(true, `for DeprecatedType`); + }, + propertyOfDeprecatedClass: (propertyName, propertySchema) => { + // property is not explicitly deprecated, but the type's schema is + expect(propertySchema.deprecated).to.eq(undefined, `for property ${propertyName}.deprecated`); + const typeSchema = currentSpec.spec.components.schemas!['DeprecatedClass']; + expect(typeSchema.deprecated).to.eq(true, `for DeprecatedClass`); + }, + deprecatedProperty: (propertyName, propertySchema) => { + expect(propertySchema.deprecated).to.eq(true, `for property ${propertyName}.deprecated`); + }, + deprecatedFieldsOnInlineMappedTypeFromSignature: (propertyName, propertySchema) => { + expect(propertySchema.properties!.okProp.deprecated).to.eql(undefined, `for property okProp.deprecated`); + expect(propertySchema.properties!.notOkProp.deprecated).to.eql(undefined, `for property notOkProp.deprecated`); + }, + deprecatedFieldsOnInlineMappedTypeFromDeclaration: (propertyName, propertySchema) => { + expect(propertySchema.properties!.okProp.deprecated).to.eql(undefined, `for property okProp.deprecated`); + expect(propertySchema.properties!.notOkProp.deprecated).to.eql(undefined, `for property notOkProp.deprecated`); + expect(propertySchema.properties!.stillNotOkProp.deprecated).to.eql(undefined, `for property stillNotOkProp.deprecated`); + }, + notDeprecatedFieldsOnInlineMappedTypeWithIndirection: (propertyName, propertySchema) => { + // See corresponding `deprecated: false` in TypeResolver#resolve + expect(propertySchema.properties!.notOk.deprecated).to.eql(undefined, `for property notOk.deprecated`); + }, + object: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq('object', `for property ${propertyName}`); + if (currentSpec.specName === 'specWithNoImplicitExtras') { + expect(propertySchema.additionalProperties).to.eq(false, forSpec(currentSpec)); + } else { + expect(propertySchema.additionalProperties).to.eq(true, forSpec(currentSpec)); + } + }, + objectArray: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq('array', `for property ${propertyName}`); + // Now check the items on the array of objects + if (!propertySchema.items) { + throw new Error(`There was no 'items' property on ${propertyName}.`); + } + expect(propertySchema.items.type).to.equal('object'); + // The "PetShop" Swagger editor considers it valid to have additionalProperties on an array of objects + // So, let's convince TypeScript + const itemsAsSchema = propertySchema.items; + if (currentSpec.specName === 'specWithNoImplicitExtras') { + expect(itemsAsSchema.additionalProperties).to.eq(false, forSpec(currentSpec)); + } else { + expect(itemsAsSchema.additionalProperties).to.eq(true, forSpec(currentSpec)); + } + }, + enumValue: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq(undefined, `for property ${propertyName}.type`); + expect(propertySchema.$ref).to.eq('#/components/schemas/EnumIndexValue', `for property ${propertyName}.$ref`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + }, + enumArray: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq('array', `for property ${propertyName}.type`); + expect(propertySchema.description).to.eq(undefined, `for property ${propertyName}.description`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + if (!propertySchema.items) { + throw new Error(`There was no 'items' property on ${propertyName}.`); + } + expect(propertySchema.items.$ref).to.eq('#/components/schemas/EnumIndexValue', `for property ${propertyName}.items.$ref`); + }, + enumNumberValue: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq(undefined, `for property ${propertyName}.type`); + expect(propertySchema.$ref).to.eq('#/components/schemas/EnumNumberValue', `for property ${propertyName}.$ref`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + + const schema = getComponentSchema('EnumNumberValue', currentSpec); + expect(schema.type).to.eq('number'); + expect(schema.enum).to.eql([0, 2, 5]); + expect(schema['x-enum-varnames']).to.eq(undefined); + }, + enumStringNumberValue: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq(undefined, `for property ${propertyName}.type`); + expect(propertySchema.$ref).to.eq('#/components/schemas/EnumStringNumberValue', `for property ${propertyName}.$ref`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + + const schema = getComponentSchema('EnumStringNumberValue', currentSpec); + expect(schema.type).to.eq('string'); + expect(schema.enum).to.eql(['0', '2', '5']); + expect(schema['x-enum-varnames']).to.eq(undefined); + }, + enumStringNumberArray: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq('array', `for property ${propertyName}.type`); + expect(propertySchema.description).to.eq(undefined, `for property ${propertyName}.description`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + if (!propertySchema.items) { + throw new Error(`There was no 'items' property on ${propertyName}.`); + } + expect(propertySchema.items.$ref).to.eq('#/components/schemas/EnumStringNumberValue', `for property ${propertyName}.items.$ref`); + }, + enumNumberArray: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq('array', `for property ${propertyName}.type`); + expect(propertySchema.description).to.eq(undefined, `for property ${propertyName}.description`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + if (!propertySchema.items) { + throw new Error(`There was no 'items' property on ${propertyName}.`); + } + expect(propertySchema.items.$ref).to.eq('#/components/schemas/EnumNumberValue', `for property ${propertyName}.items.$ref`); + }, + enumStringValue: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq(undefined, `for property ${propertyName}.type`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + expect(propertySchema.$ref).to.eq('#/components/schemas/EnumStringValue', `for property ${propertyName}.$ref`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + + const schema = getComponentSchema('EnumStringValue', currentSpec); + expect(schema.type).to.eq('string'); + expect(schema.description).to.eql('EnumStringValue.'); + expect(schema.enum).to.eql(['', 'VALUE_1', 'VALUE_2']); + expect(schema.example).to.eql('VALUE_1'); + expect(schema['x-enum-varnames']).to.eq(undefined); + }, + enumStringProperty: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/EnumStringValue.VALUE_1'); + const schema = getComponentSchema('EnumStringValue.VALUE_1', currentSpec); + expect(schema).to.deep.eq({ + description: undefined, + enum: ['VALUE_1'], + type: 'string', + }); + }, + enumStringArray: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq('array', `for property ${propertyName}.type`); + expect(propertySchema.description).to.eq(undefined, `for property ${propertyName}.description`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + if (!propertySchema.items) { + throw new Error(`There was no 'items' property on ${propertyName}.`); + } + expect(propertySchema.items.$ref).to.eq('#/components/schemas/EnumStringValue', `for property ${propertyName}.items.$ref`); + }, + modelValue: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/TestSubModel', `for property ${propertyName}.$ref`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + }, + modelsArray: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq('array', `for property ${propertyName}.type`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + if (!propertySchema.items) { + throw new Error(`There was no 'items' property on ${propertyName}.`); + } + expect(propertySchema.items.$ref).to.eq('#/components/schemas/TestSubModel', `for property ${propertyName}.items.$ref`); + }, + strLiteralVal: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/StrLiteral', `for property ${propertyName}.$ref`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}[x-nullable]`); + + const componentSchema = getComponentSchema('StrLiteral', currentSpec); + expect(componentSchema).to.deep.eq({ + default: undefined, + description: 'StrLiteral.', + enum: ['', 'Foo', 'Bar'], + example: 'Foo', + format: undefined, + type: 'string', + }); + }, + strLiteralArr: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq('array', `for property ${propertyName}.type`); + expect(propertySchema.items && propertySchema.items.$ref).to.eq('#/components/schemas/StrLiteral', `for property ${propertyName}.$ref`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}[x-nullable]`); + }, + unionPrimitiveType: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.eq({ + anyOf: [ + { type: 'string', enum: ['String'] }, + { type: 'number', enum: [1, 20] }, + { type: 'boolean', enum: [true, false] }, + ], + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }); + }, + nullableUnionPrimitiveType: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.eq({ + anyOf: [ + { type: 'string', enum: ['String'] }, + { type: 'number', enum: [1, 20] }, + { type: 'boolean', enum: [true, false] }, + ], + default: undefined, + description: undefined, + example: undefined, + format: undefined, + nullable: true, + }); + }, + undefineableUnionPrimitiveType: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.eq({ + anyOf: [ + { type: 'string', enum: ['String'] }, + { type: 'number', enum: [1, 20] }, + { type: 'boolean', enum: [true, false] }, + ], + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }); + }, + singleFloatLiteralType: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq('number', `for property ${propertyName}.type`); + expect(propertySchema.nullable).to.eq(false, `for property ${propertyName}.nullable`); + if (!propertySchema.enum) { + throw new Error(`There was no 'enum' property on ${propertyName}.`); + } + expect(propertySchema.enum).to.have.length(1, `for property ${propertyName}.enum`); + expect(propertySchema.enum).to.include(3.1415, `for property ${propertyName}.enum`); + }, + negativeNumberLiteralType: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq('number', `for property ${propertyName}.type`); + expect(propertySchema.nullable).to.eq(false, `for property ${propertyName}.nullable`); + if (!propertySchema.enum) { + throw new Error(`There was no 'enum' property on ${propertyName}.`); + } + expect(propertySchema.enum).to.have.length(1, `for property ${propertyName}.enum`); + expect(propertySchema.enum).to.include(-1, `for property ${propertyName}.enum`); + }, + dateValue: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq('string', `for property ${propertyName}.type`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + expect(propertySchema.format).to.eq('date-time', `for property ${propertyName}.format`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + }, + optionalString: (propertyName, propertySchema) => { + // should generate an optional property from an optional property + expect(propertySchema.type).to.eq('string', `for property ${propertyName}.type`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + expect(propertySchema).to.not.haveOwnProperty('format', `for property ${propertyName}`); + }, + anyType: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq(undefined, `for property ${propertyName}`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + expect(propertySchema.additionalProperties).to.eq(undefined, 'because the "any" type always allows more properties be definition'); + }, + unknownType: (propertyName, propertySchema) => { + expect(propertySchema.type).to.eq(undefined, `for property ${propertyName}`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + expect(propertySchema.additionalProperties).to.eq(undefined, 'because the "unknown" type always allows more properties be definition'); + }, + genericTypeObject: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/Generic__foo-string--bar-boolean__'); + }, + indexed: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/Partial_Indexed-at-foo_'); + }, + indexedValue: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/IndexedValue'); + const schema = getComponentSchema('IndexedValue', currentSpec); + expect(schema).to.deep.eq({ + type: 'string', + enum: ['FOO', 'BAR'], + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }); + }, + parenthesizedIndexedValue: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/ParenthesizedIndexedValue'); + const schema = getComponentSchema('ParenthesizedIndexedValue', currentSpec); + expect(schema).to.deep.eq({ + type: 'string', + enum: ['FOO', 'BAR'], + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }); + }, + indexedValueReference: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/IndexedValueReference'); + const schema = getComponentSchema('IndexedValueReference', currentSpec); + expect(schema).to.deep.eq({ + type: 'string', + enum: ['FOO', 'BAR'], + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }); + }, + indexedValueGeneric: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/IndexedValueGeneric_IndexedValueTypeReference_'); + const schema = getComponentSchema('IndexedValueGeneric_IndexedValueTypeReference_', currentSpec); + expect(schema).to.deep.eq({ + type: 'string', + enum: ['FOO', 'BAR'], + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }); + }, + stringUnionRecord: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/Record_record-foo-or-record-bar._data-string__'); + const schema = getComponentSchema('Record_record-foo-or-record-bar._data-string__', currentSpec); + expect(schema).to.be.deep.eq({ + properties: { + 'record-foo': { + properties: { + data: { type: 'string', description: undefined, example: undefined, format: undefined, default: undefined }, + }, + required: ['data'], + type: 'object', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + 'record-bar': { + properties: { + data: { type: 'string', description: undefined, example: undefined, format: undefined, default: undefined }, + }, + required: ['data'], + type: 'object', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + }, + required: ['record-foo', 'record-bar'], + type: 'object', + default: undefined, + example: undefined, + format: undefined, + description: 'Construct a type with a set of properties K of type T', + }); + }, + numberUnionRecord: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/Record_1-or-2._data-string__'); + const schema = getComponentSchema('Record_1-or-2._data-string__', currentSpec); + expect(schema).to.be.deep.eq({ + properties: { + [1]: { + properties: { + data: { type: 'string', description: undefined, example: undefined, format: undefined, default: undefined }, + }, + required: ['data'], + type: 'object', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + [2]: { + properties: { + data: { type: 'string', description: undefined, example: undefined, format: undefined, default: undefined }, + }, + required: ['data'], + type: 'object', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + }, + required: ['1', '2'], + type: 'object', + default: undefined, + example: undefined, + format: undefined, + description: 'Construct a type with a set of properties K of type T', + }); + }, + stringRecord: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/Record_string._data-string__'); + const schema = getComponentSchema('Record_string._data-string__', currentSpec); + expect(schema).to.be.deep.eq({ + additionalProperties: { + properties: { + data: { + default: undefined, + description: undefined, + example: undefined, + format: undefined, + type: 'string', + }, + }, + required: ['data'], + type: 'object', + }, + default: undefined, + description: 'Construct a type with a set of properties K of type T', + example: undefined, + format: undefined, + properties: {}, + type: 'object', + }); + }, + numberRecord: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/Record_number._data-string__'); + const schema = getComponentSchema('Record_number._data-string__', currentSpec); + expect(schema).to.be.deep.eq({ + additionalProperties: { + properties: { + data: { + default: undefined, + description: undefined, + example: undefined, + format: undefined, + type: 'string', + }, + }, + required: ['data'], + type: 'object', + }, + default: undefined, + description: 'Construct a type with a set of properties K of type T', + example: undefined, + format: undefined, + properties: {}, + type: 'object', + }); + }, + emptyRecord: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/Record_string.never_'); + const schema = getComponentSchema('Record_string.never_', currentSpec); + expect(schema).to.be.deep.eq({ + default: undefined, + description: 'Construct a type with a set of properties K of type T', + example: undefined, + format: undefined, + properties: {}, + type: 'object', + }); + }, + modelsObjectIndirect: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/TestSubModelContainer', `for property ${propertyName}.$ref`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + }, + modelsObjectIndirectNS: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/TestSubModelContainerNamespace.TestSubModelContainer', `for property ${propertyName}.$ref`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + }, + modelsObjectIndirectNS2: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/TestSubModelContainerNamespace.InnerNamespace.TestSubModelContainer2', `for property ${propertyName}.$ref`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + }, + modelsObjectIndirectNS_Alias: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/TestSubModelContainerNamespace_TestSubModelContainer', `for property ${propertyName}.$ref`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + }, + modelsObjectIndirectNS2_Alias: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/TestSubModelContainerNamespace_InnerNamespace_TestSubModelContainer2', `for property ${propertyName}.$ref`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + }, + modelsArrayIndirect: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/TestSubArrayModelContainer', `for property ${propertyName}.$ref`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + }, + modelsEnumIndirect: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/TestSubEnumModelContainer', `for property ${propertyName}.$ref`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + }, + typeAliasCase1: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/TypeAliasModelCase1', `for property ${propertyName}.$ref`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + }, + TypeAliasCase2: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/TypeAliasModelCase2', `for property ${propertyName}.$ref`); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + expect(propertySchema.nullable).to.eq(undefined, `for property ${propertyName}.nullable`); + }, + genericMultiNested: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/GenericRequest_GenericRequest_TypeAliasModel1__', `for property ${propertyName}.$ref`); + }, + genericNestedArrayKeyword1: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/GenericRequest_Array_TypeAliasModel1__', `for property ${propertyName}.$ref`); + }, + genericNestedArrayCharacter1: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/GenericRequest_TypeAliasModel1-Array_', `for property ${propertyName}.$ref`); + }, + genericNestedArrayKeyword2: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/GenericRequest_Array_TypeAliasModel2__', `for property ${propertyName}.$ref`); + }, + genericNestedArrayCharacter2: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/GenericRequest_TypeAliasModel2-Array_', `for property ${propertyName}.$ref`); + }, + defaultGenericModel: (propertyName, propertySchema) => { + expect(propertySchema.$ref).to.eq('#/components/schemas/GenericModel', `for property ${propertyName}.$ref`); + + const definition = getComponentSchema('GenericModel', currentSpec); + expect(definition.properties!.result.type).to.deep.equal('string'); + // string | string reduced to just string after removal of duplicate + // types when generating union spec + expect(definition.properties!.union.type).to.deep.equal('string'); + expect(definition.properties!.nested.$ref).to.deep.equal('#/components/schemas/GenericRequest_string_'); + }, + and: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.include( + { + allOf: [{ $ref: '#/components/schemas/TypeAliasModel1' }, { $ref: '#/components/schemas/TypeAliasModel2' }], + }, + `for property ${propertyName}.$ref`, + ); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + }, + referenceAnd: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.include( + { + $ref: '#/components/schemas/TypeAliasModelCase1', + }, + `for property ${propertyName}.$ref`, + ); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + }, + or: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.include( + { + anyOf: [{ $ref: '#/components/schemas/TypeAliasModel1' }, { $ref: '#/components/schemas/TypeAliasModel2' }], + }, + `for property ${propertyName}.$ref`, + ); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + }, + mixedUnion: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.include( + { + anyOf: [{ type: 'string' }, { $ref: '#/components/schemas/TypeAliasModel1' }], + }, + `for property ${propertyName}.$ref`, + ); + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + }, + typeAliases: (propertyName, propertySchema) => { + expect(propertyName).to.equal('typeAliases'); + expect(propertySchema).to.deep.equal({ + default: undefined, + description: undefined, + format: undefined, + example: undefined, + properties: { + word: { $ref: '#/components/schemas/Word', description: undefined, format: undefined, example: undefined }, + fourtyTwo: { $ref: '#/components/schemas/FourtyTwo', description: undefined, format: undefined, example: undefined }, + dateAlias: { $ref: '#/components/schemas/DateAlias', description: undefined, format: undefined, example: undefined }, + unionAlias: { $ref: '#/components/schemas/UnionAlias', description: undefined, format: undefined, example: undefined }, + intersectionAlias: { $ref: '#/components/schemas/IntersectionAlias', description: undefined, format: undefined, example: undefined }, + nOLAlias: { $ref: '#/components/schemas/NolAlias', description: undefined, format: undefined, example: undefined }, + genericAlias: { $ref: '#/components/schemas/GenericAlias_string_', description: undefined, format: undefined, example: undefined }, + genericAlias2: { $ref: '#/components/schemas/GenericAlias_Model_', description: undefined, format: undefined, example: undefined }, + forwardGenericAlias: { $ref: '#/components/schemas/ForwardGenericAlias_boolean.TypeAliasModel1_', description: undefined, format: undefined, example: undefined }, + }, + required: ['forwardGenericAlias', 'genericAlias2', 'genericAlias', 'nOLAlias', 'intersectionAlias', 'unionAlias', 'fourtyTwo', 'word'], + type: 'object', + }); + + const wordSchema = getComponentSchema('Word', currentSpec); + expect(wordSchema).to.deep.eq({ type: 'string', description: 'A Word shall be a non-empty sting', example: undefined, default: undefined, minLength: 1, format: 'password' }); + + const fourtyTwoSchema = getComponentSchema('FourtyTwo', currentSpec); + expect(fourtyTwoSchema).to.deep.eq({ + type: 'integer', + format: 'int32', + description: 'The number 42 expressed through OpenAPI', + example: 42, + minimum: 42, + maximum: 42, + default: 42, + }); + + const dateAliasSchema = getComponentSchema('DateAlias', currentSpec); + expect(dateAliasSchema).to.deep.eq({ type: 'string', format: 'date', description: undefined, example: undefined, default: undefined }); + + const unionAliasSchema = getComponentSchema('UnionAlias', currentSpec); + expect(unionAliasSchema).to.deep.eq({ + anyOf: [{ $ref: '#/components/schemas/TypeAliasModelCase2' }, { $ref: '#/components/schemas/TypeAliasModel2' }], + description: undefined, + example: undefined, + default: undefined, + format: undefined, + }); + + const intersectionAliasSchema = getComponentSchema('IntersectionAlias', currentSpec); + expect(intersectionAliasSchema).to.deep.eq({ + allOf: [ + { + properties: { + value1: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined }, + value2: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined }, + }, + required: ['value2', 'value1'], + type: 'object', + }, + { $ref: '#/components/schemas/TypeAliasModel1' }, + ], + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }); + + const nolAliasSchema = getComponentSchema('NolAlias', currentSpec); + expect(nolAliasSchema).to.deep.eq({ + properties: { + value1: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined }, + value2: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined }, + }, + required: ['value2', 'value1'], + type: 'object', + description: undefined, + example: undefined, + default: undefined, + format: undefined, + }); + + const genericAliasStringSchema = getComponentSchema('GenericAlias_string_', currentSpec); + expect(genericAliasStringSchema).to.deep.eq({ type: 'string', default: undefined, description: undefined, example: undefined, format: undefined }); + + const genericAliasModelSchema = getComponentSchema('GenericAlias_Model_', currentSpec); + expect(genericAliasModelSchema).to.deep.eq({ $ref: '#/components/schemas/Model', default: undefined, description: undefined, example: undefined, format: undefined }); + + const forwardGenericAliasBooleanAndTypeAliasModel1Schema = getComponentSchema('ForwardGenericAlias_boolean.TypeAliasModel1_', currentSpec); + expect(forwardGenericAliasBooleanAndTypeAliasModel1Schema).to.deep.eq({ + anyOf: [{ $ref: '#/components/schemas/GenericAlias_TypeAliasModel1_' }, { type: 'boolean' }], + description: undefined, + example: undefined, + default: undefined, + format: undefined, + }); + + expect(getComponentSchema('GenericAlias_TypeAliasModel1_', currentSpec)).to.deep.eq({ + $ref: '#/components/schemas/TypeAliasModel1', + description: undefined, + example: undefined, + default: undefined, + format: undefined, + }); + }, + advancedTypeAliases: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.eq( + { + properties: { + omit: { $ref: '#/components/schemas/Omit_ErrorResponseModel.status_', description: undefined, format: undefined, example: undefined }, + omitHidden: { $ref: '#/components/schemas/Omit_PrivateModel.stringPropDec1_', description: undefined, format: undefined, example: undefined }, + partial: { $ref: '#/components/schemas/Partial_Account_', description: undefined, format: undefined, example: undefined }, + excludeToEnum: { $ref: '#/components/schemas/Exclude_EnumUnion.EnumNumberValue_', description: undefined, format: undefined, example: undefined }, + excludeToAlias: { $ref: '#/components/schemas/Exclude_ThreeOrFour.TypeAliasModel3_', description: undefined, format: undefined, example: undefined }, + excludeLiteral: { + $ref: '#/components/schemas/Exclude_keyofTestClassModel.account-or-defaultValue2-or-indexedTypeToInterface-or-indexedTypeToClass-or-indexedTypeToAlias-or-indexedResponseObject-or-arrayUnion-or-objectUnion_', + description: undefined, + format: undefined, + example: undefined, + }, + excludeToInterface: { $ref: '#/components/schemas/Exclude_OneOrTwo.TypeAliasModel1_', description: undefined, format: undefined, example: undefined }, + excludeTypeToPrimitive: { $ref: '#/components/schemas/NonNullable_number-or-null_', description: undefined, format: undefined, example: undefined }, + pick: { $ref: '#/components/schemas/Pick_ThingContainerWithTitle_string_.list_', description: undefined, format: undefined, example: undefined }, + readonlyClass: { $ref: '#/components/schemas/Readonly_TestClassModel_', description: undefined, format: undefined, example: undefined }, + defaultArgs: { $ref: '#/components/schemas/DefaultTestModel', description: undefined, format: undefined, example: undefined }, + heritageCheck: { $ref: '#/components/schemas/HeritageTestModel', description: undefined, format: undefined, example: undefined }, + heritageCheck2: { $ref: '#/components/schemas/HeritageTestModel2', description: undefined, format: undefined, example: undefined }, + }, + type: 'object', + default: undefined, + description: undefined, + format: undefined, + example: undefined, + }, + `for property ${propertyName}`, + ); + + const getterClass = getComponentSchema('GetterClass', currentSpec); + expect(getterClass).to.deep.eq({ + allOf: [ + { + $ref: '#/components/schemas/NonFunctionProperties_GetterClass_', + }, + { + properties: { + foo: { + type: 'string', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + }, + required: ['foo'], + type: 'object', + }, + ], + default: undefined, + example: undefined, + format: undefined, + description: undefined, + }); + const getterClass2 = getComponentSchema('NonFunctionProperties_GetterClass_', currentSpec); + expect(getterClass2).to.deep.eq({ + $ref: '#/components/schemas/Pick_GetterClass.NonFunctionPropertyNames_GetterClass__', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }); + const getterClass3 = getComponentSchema('Pick_GetterClass.NonFunctionPropertyNames_GetterClass__', currentSpec); + expect(getterClass3).to.deep.eq({ + default: undefined, + description: 'From T, pick a set of properties whose keys are in the union K', + example: undefined, + format: undefined, + properties: { + a: { + type: 'string', + enum: ['b'], + nullable: false, + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + }, + required: ['a'], + type: 'object', + }); + + const getterInterface = getComponentSchema('GetterInterface', currentSpec); + expect(getterInterface).to.deep.eq({ + properties: { + foo: { + type: 'string', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + }, + required: ['foo'], + type: 'object', + default: undefined, + example: undefined, + format: undefined, + description: undefined, + }); + + const getterInterfaceHerited = getComponentSchema('GetterInterfaceHerited', currentSpec); + expect(getterInterfaceHerited).to.deep.eq({ + properties: { + foo: { + type: 'string', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + }, + required: ['foo'], + type: 'object', + default: undefined, + example: undefined, + format: undefined, + description: undefined, + }); + + const omit = getComponentSchema('Omit_ErrorResponseModel.status_', currentSpec); + expect(omit).to.deep.eq( + { + $ref: '#/components/schemas/Pick_ErrorResponseModel.Exclude_keyofErrorResponseModel.status__', + description: 'Construct a type with the properties of T except for those in type K.', + default: undefined, + example: undefined, + format: undefined, + }, + `for a schema linked by property ${propertyName}`, + ); + + const omitReference = getComponentSchema('Pick_ErrorResponseModel.Exclude_keyofErrorResponseModel.status__', currentSpec); + expect(omitReference).to.deep.eq( + { + properties: { message: { type: 'string', default: undefined, description: undefined, format: undefined, minLength: 2, example: undefined } }, + required: ['message'], + type: 'object', + description: 'From T, pick a set of properties whose keys are in the union K', + default: undefined, + example: undefined, + format: undefined, + }, + `for a schema linked by property ${propertyName}`, + ); + + const omitHidden = getComponentSchema('Omit_PrivateModel.stringPropDec1_', currentSpec); + expect(omitHidden).to.deep.eq( + { + $ref: '#/components/schemas/Pick_PrivateModel.Exclude_keyofPrivateModel.stringPropDec1__', + description: 'Construct a type with the properties of T except for those in type K.', + default: undefined, + example: undefined, + format: undefined, + }, + `for a schema linked by property ${propertyName}`, + ); + + const omitHiddenReference = getComponentSchema('Pick_PrivateModel.Exclude_keyofPrivateModel.stringPropDec1__', currentSpec); + expect(omitHiddenReference).to.deep.eq( + { + properties: { + id: { type: 'number', format: 'double', default: undefined, description: undefined, example: undefined }, + stringPropDec2: { type: 'string', default: undefined, description: undefined, format: undefined, minLength: 2, example: undefined }, + }, + required: ['stringPropDec2', 'id'], + type: 'object', + description: 'From T, pick a set of properties whose keys are in the union K', + default: undefined, + example: undefined, + format: undefined, + }, + `for a schema linked by property ${propertyName}`, + ); + + const partial = getComponentSchema('Partial_Account_', currentSpec); + expect(partial).to.deep.eq( + { + properties: { id: { type: 'number', format: 'double', default: undefined, example: undefined, description: undefined } }, + type: 'object', + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for a schema linked by property ${propertyName}`, + ); + + const excludeToEnum = getComponentSchema('Exclude_EnumUnion.EnumNumberValue_', currentSpec); + expect(excludeToEnum).to.deep.eq( + { + $ref: '#/components/schemas/EnumIndexValue', + description: 'Exclude from T those types that are assignable to U', + default: undefined, + example: undefined, + format: undefined, + }, + `for a schema linked by property ${propertyName}`, + ); + + const excludeToAlias = getComponentSchema('Exclude_ThreeOrFour.TypeAliasModel3_', currentSpec); + expect(excludeToAlias).to.deep.eq( + { + $ref: '#/components/schemas/TypeAlias4', + description: 'Exclude from T those types that are assignable to U', + default: undefined, + example: undefined, + format: undefined, + }, + `for a schema linked by property ${propertyName}`, + ); + + const excludeToAliasTypeAlias4 = getComponentSchema('TypeAlias4', currentSpec); + expect(excludeToAliasTypeAlias4).to.deep.eq( + { + properties: { value4: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined } }, + required: ['value4'], + type: 'object', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for a schema linked by property ${propertyName}`, + ); + + const excludeLiteral = getComponentSchema( + 'Exclude_keyofTestClassModel.account-or-defaultValue2-or-indexedTypeToInterface-or-indexedTypeToClass-or-indexedTypeToAlias-or-indexedResponseObject-or-arrayUnion-or-objectUnion_', + currentSpec, + ); + expect(excludeLiteral).to.deep.eq( + { + default: undefined, + description: 'Exclude from T those types that are assignable to U', + enum: [ + 'id', + 'enumKeys', + 'keyInterface', + 'indexedType', + 'indexedResponse', + 'publicStringProperty', + 'optionalPublicStringProperty', + 'emailPattern', + 'stringProperty', + 'deprecated1', + 'deprecated2', + 'extensionTest', + 'extensionComment', + 'stringExample', + 'objectExample', + 'publicConstructorVar', + 'readonlyConstructorArgument', + 'optionalPublicConstructorVar', + 'deprecatedPublicConstructorVar', + 'deprecatedPublicConstructorVar2', + 'myIgnoredMethod', + 'defaultValue1', + ], + example: undefined, + format: undefined, + type: 'string', + }, + `for a schema linked by property ${propertyName}`, + ); + + const excludeToInterface = getComponentSchema('Exclude_OneOrTwo.TypeAliasModel1_', currentSpec); + expect(excludeToInterface).to.deep.eq( + { + $ref: '#/components/schemas/TypeAliasModel2', + description: 'Exclude from T those types that are assignable to U', + default: undefined, + example: undefined, + format: undefined, + }, + `for a schema linked by property ${propertyName}`, + ); + + const excludeTypeToPrimitive = getComponentSchema('NonNullable_number-or-null_', currentSpec); + + if (['4.7', '4.6'].includes(versionMajorMinor)) { + expect(excludeTypeToPrimitive).to.deep.eq( + { + type: 'number', + format: 'double', + default: undefined, + example: undefined, + description: 'Exclude null and undefined from T', + }, + `for a schema linked by property ${propertyName}`, + ); + } else { + expect(excludeTypeToPrimitive).to.deep.eq({ + allOf: [ + { + format: 'double', + nullable: true, + type: 'number', + }, + { + properties: {}, + type: 'object', + }, + ], + description: 'Exclude null and undefined from T', + default: undefined, + example: undefined, + format: undefined, + }); + } + + const pick = getComponentSchema('Pick_ThingContainerWithTitle_string_.list_', currentSpec); + expect(pick).to.deep.eq( + { + properties: { + list: { + items: { type: 'number', format: 'double' }, + type: 'array', + default: undefined, + description: undefined, + format: undefined, + example: undefined, + }, + }, + required: ['list'], + type: 'object', + description: 'From T, pick a set of properties whose keys are in the union K', + default: undefined, + example: undefined, + format: undefined, + }, + `for a schema linked by property ${propertyName}`, + ); + + const customRecord = getComponentSchema('Record_id.string_', currentSpec); + expect(customRecord).to.deep.eq({ + default: undefined, + description: 'Construct a type with a set of properties K of type T', + format: undefined, + example: undefined, + properties: { + id: { + default: undefined, + description: undefined, + example: undefined, + format: undefined, + type: 'string', + }, + }, + required: ['id'], + type: 'object', + }); + + const readonlyClassSchema = getComponentSchema('Readonly_TestClassModel_', currentSpec); + expect(readonlyClassSchema).to.deep.eq( + { + properties: { + defaultValue1: { type: 'string', default: 'Default Value 1', description: undefined, format: undefined, example: undefined }, + enumKeys: { + default: undefined, + description: undefined, + enum: ['OK', 'KO'], + example: undefined, + format: undefined, + type: 'string', + }, + id: { type: 'number', format: 'double', default: undefined, description: undefined, example: undefined }, + indexedResponse: { + $ref: '#/components/schemas/Record_id.string_', + description: undefined, + example: undefined, + format: undefined, + }, + indexedResponseObject: { + $ref: '#/components/schemas/Record_id._myProp1-string__', + description: undefined, + example: undefined, + format: undefined, + }, + indexedType: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined }, + indexedTypeToClass: { $ref: '#/components/schemas/IndexedClass', description: undefined, format: undefined, example: undefined }, + indexedTypeToInterface: { $ref: '#/components/schemas/IndexedInterface', description: undefined, format: undefined, example: undefined }, + indexedTypeToAlias: { $ref: '#/components/schemas/IndexedInterface', description: undefined, format: undefined, example: undefined }, + arrayUnion: { + default: undefined, + description: undefined, + enum: ['foo', 'bar'], + example: undefined, + format: undefined, + type: 'string', + }, + objectUnion: { + default: undefined, + description: undefined, + enum: ['foo', 'bar'], + example: undefined, + format: undefined, + type: 'string', + }, + keyInterface: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined, enum: ['id'], nullable: false }, + optionalPublicConstructorVar: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined }, + readonlyConstructorArgument: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined }, + publicConstructorVar: { type: 'string', default: undefined, description: 'This is a description for publicConstructorVar', format: undefined, example: undefined }, + stringProperty: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined }, + emailPattern: { type: 'string', default: undefined, description: undefined, format: 'email', pattern: '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$', example: undefined }, + optionalPublicStringProperty: { type: 'string', minLength: 0, maxLength: 10, default: undefined, description: undefined, format: undefined, example: undefined }, + publicStringProperty: { + type: 'string', + minLength: 3, + maxLength: 20, + pattern: '^[a-zA-Z]+$', + default: undefined, + description: 'This is a description of a public string property', + format: undefined, + example: 'classPropExample', + title: 'Example title', + }, + defaultValue2: { type: 'string', default: 'Default Value 2', description: undefined, format: undefined, example: undefined }, + account: { $ref: '#/components/schemas/Account', format: undefined, description: undefined, example: undefined }, + deprecated1: { type: 'boolean', default: undefined, description: undefined, format: undefined, example: undefined, deprecated: true }, + deprecated2: { type: 'boolean', default: undefined, description: undefined, format: undefined, example: undefined, deprecated: true }, + extensionTest: { type: 'boolean', default: undefined, description: undefined, format: undefined, example: undefined, 'x-key-1': 'value-1', 'x-key-2': 'value-2' }, + extensionComment: { type: 'boolean', default: undefined, description: undefined, format: undefined, example: undefined, 'x-key-1': 'value-1', 'x-key-2': 'value-2' }, + stringExample: { type: 'string', default: undefined, description: undefined, format: undefined, example: 'stringValue' }, + objectExample: { + type: 'object', + default: undefined, + description: undefined, + format: undefined, + example: { + id: 1, + label: 'labelValue', + }, + properties: { + id: { + default: undefined, + description: undefined, + example: undefined, + format: 'double', + type: 'number', + }, + label: { + default: undefined, + description: undefined, + example: undefined, + format: undefined, + type: 'string', + }, + }, + required: ['label', 'id'], + }, + deprecatedPublicConstructorVar: { type: 'boolean', default: undefined, description: undefined, format: undefined, example: undefined, deprecated: true }, + deprecatedPublicConstructorVar2: { type: 'boolean', default: undefined, description: undefined, format: undefined, example: undefined, deprecated: true }, + }, + required: ['account', 'enumKeys', 'publicStringProperty', 'stringProperty', 'publicConstructorVar', 'readonlyConstructorArgument', 'id'], + type: 'object', + description: 'Make all properties in T readonly', + default: undefined, + example: undefined, + format: undefined, + }, + `for schema linked by property ${propertyName}`, + ); + + const defaultArgs = getComponentSchema('DefaultTestModel', currentSpec); + expect(defaultArgs).to.deep.eq( + { + description: undefined, + properties: { + t: { $ref: '#/components/schemas/GenericRequest_Word_', description: undefined, format: undefined, example: undefined }, + u: { $ref: '#/components/schemas/DefaultArgs_Omit_ErrorResponseModel.status__', description: undefined, format: undefined, example: undefined }, + }, + required: ['t', 'u'], + type: 'object', + additionalProperties: currentSpec.specName === 'specWithNoImplicitExtras' ? false : true, + }, + `for schema linked by property ${propertyName}`, + ); + + const heritageCheck = getComponentSchema('HeritageTestModel', currentSpec); + expect(heritageCheck).to.deep.eq( + { + properties: { + value4: { type: 'string', description: undefined, format: undefined, example: undefined, default: undefined }, + name: { type: 'string', description: undefined, format: undefined, example: undefined, default: undefined }, + }, + required: ['value4'], + type: 'object', + additionalProperties: currentSpec.specName === 'specWithNoImplicitExtras' ? false : true, + description: undefined, + }, + `for schema linked by property ${propertyName}`, + ); + + const heritageCheck2 = getComponentSchema('HeritageTestModel2', currentSpec); + expect(heritageCheck2).to.deep.eq( + { + properties: { + value: { type: 'string', description: undefined, format: undefined, example: undefined, default: undefined }, + }, + required: ['value'], + type: 'object', + additionalProperties: currentSpec.specName === 'specWithNoImplicitExtras' ? false : true, + description: undefined, + }, + `for schema linked by property ${propertyName}`, + ); + }, + nullableTypes: (propertyName, propertySchema) => { + expect(propertyName).to.equal('nullableTypes'); + expect(propertySchema).to.deep.equal({ + default: undefined, + description: undefined, + example: { + justNull: null, + maybeString: null, + numberOrNull: null, + wordOrNull: null, + }, + format: undefined, + properties: { + maybeString: { $ref: '#/components/schemas/Maybe_string_', description: undefined, format: undefined, example: undefined }, + wordOrNull: { $ref: '#/components/schemas/Maybe_Word_', description: undefined, format: undefined, example: undefined }, + numberOrNull: { + default: undefined, + description: undefined, + example: undefined, + type: 'integer', + format: 'int32', + minimum: 5, + nullable: true, + }, + justNull: { + default: undefined, + description: undefined, + example: undefined, + enum: [null], + format: undefined, + nullable: true, + type: 'number', + }, + }, + required: ['justNull', 'maybeString', 'wordOrNull', 'numberOrNull'], + type: 'object', + }); + + const maybeString = getComponentSchema('Maybe_string_', currentSpec); + expect(maybeString).to.deep.eq( + { + default: undefined, + description: undefined, + example: undefined, + format: undefined, + nullable: true, + type: 'string', + }, + `for schema linked by property ${propertyName}`, + ); + + const maybeWord = getComponentSchema('Maybe_Word_', currentSpec); + expect(maybeWord).to.deep.eq( + { allOf: [{ $ref: '#/components/schemas/Word' }], description: undefined, default: undefined, example: undefined, format: undefined, nullable: true }, + `for schema linked by property ${propertyName}`, + ); + }, + templateLiteralString: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.eq({ $ref: '#/components/schemas/TemplateLiteralString', description: undefined, example: undefined, format: undefined }); + + const tlsSchema = getComponentSchema('TemplateLiteralString', currentSpec); + + expect(tlsSchema).to.deep.eq({ $ref: '#/components/schemas/OrderOptions_ParameterTestModel_', default: undefined, example: undefined, format: undefined, description: undefined }); + + const orderOptionsSchema = getComponentSchema('OrderOptions_ParameterTestModel_', currentSpec); + + expect(orderOptionsSchema).to.deep.eq( + { + default: undefined, + description: undefined, + enum: [ + 'firstname:asc', + 'lastname:asc', + 'age:asc', + 'weight:asc', + 'human:asc', + 'gender:asc', + 'nicknames:asc', + 'firstname:desc', + 'lastname:desc', + 'age:desc', + 'weight:desc', + 'human:desc', + 'gender:desc', + 'nicknames:desc', + ], + example: undefined, + format: undefined, + type: 'string', + nullable: false, + }, + `for property ${propertyName}`, + ); + }, + inlineTLS: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.eq( + { + default: undefined, + description: undefined, + enum: ['ASC', 'DESC'], + example: undefined, + format: undefined, + type: 'string', + nullable: false, + }, + `for property ${propertyName}`, + ); + }, + inlineMappedType: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.equal( + { + properties: { + 'lastname:asc': { + type: 'boolean', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + 'age:asc': { + type: 'boolean', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + 'weight:asc': { + type: 'boolean', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + 'human:asc': { + type: 'boolean', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + 'gender:asc': { + type: 'boolean', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + 'nicknames:asc': { + type: 'boolean', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + 'firstname:desc': { + type: 'boolean', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + 'lastname:desc': { + type: 'boolean', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + 'age:desc': { + type: 'boolean', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + 'weight:desc': { + type: 'boolean', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + 'human:desc': { + type: 'boolean', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + 'gender:desc': { + type: 'boolean', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + 'nicknames:desc': { + type: 'boolean', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + }, + required: [ + 'lastname:asc', + 'age:asc', + 'weight:asc', + 'human:asc', + 'gender:asc', + 'nicknames:asc', + 'firstname:desc', + 'lastname:desc', + 'age:desc', + 'weight:desc', + 'human:desc', + 'gender:desc', + 'nicknames:desc', + ], + type: 'object', + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }, + `for property ${propertyName}`, + ); + }, + inlineMappedTypeRemapped: (propertyName, propertySchema) => { + expect(Object.keys(propertySchema.properties || {})).to.have.members( + ['FirstnameProp', 'LastnameProp', 'AgeProp', 'WeightProp', 'HumanProp', 'GenderProp', 'NicknamesProp'], + `for property ${propertyName}`, + ); + }, + stringAndBoolArray: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.eq({ + items: { + anyOf: [{ type: 'string' }, { type: 'boolean' }], + }, + type: 'array', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }); + }, + extensionComment: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.eq( + { + type: 'boolean', + default: undefined, + description: undefined, + format: undefined, + example: undefined, + 'x-key-1': 'value-1', + 'x-key-2': 'value-2', + }, + `for property ${propertyName}`, + ); + }, + keyofLiteral: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.eq( + { + type: 'string', + enum: ['type1', 'type2'], + default: undefined, + description: undefined, + format: undefined, + nullable: false, + example: undefined, + }, + `for property ${propertyName}`, + ); + }, + namespaces: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.eq( + { + properties: { + typeHolder2: { + $ref: '#/components/schemas/Namespace2.TypeHolder', + description: undefined, + example: undefined, + format: undefined, + }, + inModule: { + $ref: '#/components/schemas/Namespace2.Namespace2.NamespaceType', + description: undefined, + example: undefined, + format: undefined, + }, + typeHolder1: { + $ref: '#/components/schemas/Namespace1.TypeHolder', + description: undefined, + example: undefined, + format: undefined, + }, + inNamespace1: { + $ref: '#/components/schemas/Namespace1.NamespaceType', + description: undefined, + example: undefined, + format: undefined, + }, + simple: { + $ref: '#/components/schemas/NamespaceType', + description: undefined, + example: undefined, + format: undefined, + }, + }, + required: ['typeHolder2', 'inModule', 'typeHolder1', 'inNamespace1', 'simple'], + type: 'object', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}`, + ); + + const typeHolder2Schema = getComponentSchema('Namespace2.TypeHolder', currentSpec); + expect(typeHolder2Schema).to.deep.eq( + { + properties: { + inModule: { + $ref: '#/components/schemas/Namespace2.Namespace2.NamespaceType', + description: undefined, + example: undefined, + format: undefined, + }, + inNamespace2: { + $ref: '#/components/schemas/Namespace2.NamespaceType', + description: undefined, + example: undefined, + format: undefined, + }, + }, + required: ['inModule', 'inNamespace2'], + type: 'object', + additionalProperties: currentSpec.specName === 'specWithNoImplicitExtras' ? false : true, + description: undefined, + }, + `for property ${propertyName}.typeHolder2`, + ); + + const namespace2_namespace2_namespaceTypeSchema = getComponentSchema('Namespace2.Namespace2.NamespaceType', currentSpec); + expect(namespace2_namespace2_namespaceTypeSchema).to.deep.eq( + { + properties: { + inModule: { + type: 'string', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + other: { + $ref: '#/components/schemas/Namespace2.Namespace2.NamespaceType', + description: undefined, + example: undefined, + format: undefined, + }, + }, + required: ['inModule'], + type: 'object', + description: undefined, + additionalProperties: currentSpec.specName === 'specWithNoImplicitExtras' ? false : true, + }, + `for property ${propertyName}.typeHolder2.inModule`, + ); + + const typeHolderSchema = getComponentSchema('Namespace1.TypeHolder', currentSpec); + expect(typeHolderSchema).to.deep.eq( + { + properties: { + inNamespace1_1: { + $ref: '#/components/schemas/Namespace1.NamespaceType', + description: undefined, + example: undefined, + format: undefined, + }, + inNamespace1_2: { + $ref: '#/components/schemas/Namespace1.NamespaceType', + description: undefined, + example: undefined, + format: undefined, + }, + }, + required: ['inNamespace1_1', 'inNamespace1_2'], + type: 'object', + additionalProperties: currentSpec.specName === 'specWithNoImplicitExtras' ? false : true, + description: undefined, + }, + `for property ${propertyName}.typeHolder1`, + ); + + const namespace1_namespaceTypeSchema = getComponentSchema('Namespace1.NamespaceType', currentSpec); + expect(namespace1_namespaceTypeSchema).to.deep.eq( + { + properties: { + inFirstNamespace: { + type: 'string', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + inFirstNamespace2: { + type: 'string', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + }, + required: ['inFirstNamespace', 'inFirstNamespace2'], + type: 'object', + description: undefined, + additionalProperties: currentSpec.specName === 'specWithNoImplicitExtras' ? false : true, + }, + `for property ${propertyName}.typeHolder1.inNamespace1_1`, + ); + + const namespace2_namespaceTypeSchema = getComponentSchema('Namespace2.NamespaceType', currentSpec); + expect(namespace2_namespaceTypeSchema).to.deep.eq( + { + properties: { + inSecondNamespace: { + type: 'string', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + }, + required: ['inSecondNamespace'], + type: 'object', + description: undefined, + additionalProperties: currentSpec.specName === 'specWithNoImplicitExtras' ? false : true, + }, + `for property ${propertyName}.typeHolder2.inNamespace2`, + ); + + const namespaceTypeSchema = getComponentSchema('NamespaceType', currentSpec); + expect(namespaceTypeSchema).to.deep.eq( + { + type: 'string', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.simple`, + ); + }, + defaults: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.eq( + { + default: undefined, + description: undefined, + example: undefined, + format: undefined, + properties: { + basic: { + $ref: '#/components/schemas/DefaultsClass', + description: undefined, + example: undefined, + format: undefined, + }, + defaultNull: { + default: null, + description: undefined, + example: undefined, + format: undefined, + nullable: true, + type: 'string', + }, + defaultObject: { + default: { + a: 'a', + b: 2, + }, + description: undefined, + example: undefined, + format: undefined, + properties: { + a: { + default: undefined, + description: undefined, + example: undefined, + format: undefined, + type: 'string', + }, + b: { + default: undefined, + description: undefined, + example: undefined, + format: 'double', + type: 'number', + }, + }, + required: ['b', 'a'], + type: 'object', + }, + defaultUndefined: { + default: undefined, + description: undefined, + example: undefined, + format: undefined, + type: 'string', + }, + replacedTypes: { + $ref: '#/components/schemas/ReplaceTypes_DefaultsClass.boolean.string_', + description: undefined, + example: undefined, + format: undefined, + }, + comments: { + default: 4, + description: undefined, + example: undefined, + format: undefined, + }, + jsonCharacters: { + default: { '\\': '\n' }, + description: undefined, + example: undefined, + format: undefined, + }, + stringEscapeCharacters: { + default: '`"\'"\'\n\t\r\b\fgx\\', + description: undefined, + example: undefined, + format: undefined, + }, + }, + required: ['defaultNull', 'replacedTypes', 'basic'], + type: 'object', + }, + `for property ${propertyName}`, + ); + const basicSchema = getComponentSchema('DefaultsClass', currentSpec); + expect(basicSchema).to.deep.eq( + { + properties: { + boolValue1: { + type: 'boolean', + default: true, + description: undefined, + example: undefined, + format: undefined, + }, + boolValue2: { + type: 'boolean', + default: true, + description: undefined, + example: undefined, + format: undefined, + }, + boolValue3: { + type: 'boolean', + default: false, + description: undefined, + example: undefined, + format: undefined, + }, + boolValue4: { + type: 'boolean', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + }, + type: 'object', + required: undefined, + description: undefined, + additionalProperties: currentSpec.specName === 'specWithNoImplicitExtras' ? false : true, + }, + `for property ${propertyName}.basic`, + ); + const replacedTypesSchema = getComponentSchema('ReplaceTypes_DefaultsClass.boolean.string_', currentSpec); + expect(replacedTypesSchema).to.deep.eq( + { + properties: { + boolValue1: { + type: 'string', + default: true, + description: undefined, + example: undefined, + format: undefined, + }, + boolValue2: { + type: 'string', + default: true, + description: undefined, + example: undefined, + format: undefined, + }, + boolValue3: { + type: 'string', + default: false, + description: undefined, + example: undefined, + format: undefined, + }, + boolValue4: { + type: 'string', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + }, + type: 'object', + description: undefined, + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.replacedTypes`, + ); + }, + jsDocTypeNames: (propertyName, propertySchema) => { + expect(propertySchema?.properties?.simple?.$ref).to.eq('#/components/schemas/Partial__a-string__', `for property ${propertyName}`); + expect(propertySchema?.properties?.commented?.$ref).to.eq('#/components/schemas/Partial__a_description-comment_-string__', `for property ${propertyName}`); + expect(propertySchema?.properties?.multilineCommented?.$ref).to.eq('#/components/schemas/Partial__a_description-multiline_92_ncomment_-string__', `for property ${propertyName}`); + expect(propertySchema?.properties?.defaultValue?.$ref).to.eq('#/components/schemas/Partial__a_default-true_-string__', `for property ${propertyName}`); + expect(propertySchema?.properties?.deprecated?.$ref).to.eq('#/components/schemas/Partial__a_deprecated-true_-string__', `for property ${propertyName}`); + expect(propertySchema?.properties?.validators?.$ref).to.eq('#/components/schemas/Partial__a_validators_58__minLength_58__value_58_3___-string__', `for property ${propertyName}`); + expect(propertySchema?.properties?.examples?.$ref).to.eq('#/components/schemas/Partial__a_example-example_-string__', `for property ${propertyName}`); + expect(propertySchema?.properties?.extensions?.$ref).to.eq('#/components/schemas/Partial__a_extensions_58__91__key-x-key-1.value-value-1__93__-string__', `for property ${propertyName}`); + expect(propertySchema?.properties?.ignored?.$ref).to.eq('#/components/schemas/Partial__a_ignored-true_-string__', `for property ${propertyName}`); + + expect(propertySchema?.properties?.indexedSimple?.$ref).to.eq('#/components/schemas/Partial___91_a-string_93__58_string__', `for property ${propertyName}`); + expect(propertySchema?.properties?.indexedCommented?.$ref).to.eq('#/components/schemas/Partial___91_a-string_93__58_string__', `for property ${propertyName}`); + expect(propertySchema?.properties?.indexedMultilineCommented?.$ref).to.eq('#/components/schemas/Partial___91_a-string_93__58_string__', `for property ${propertyName}`); + expect(propertySchema?.properties?.indexedDefaultValue?.$ref).to.eq('#/components/schemas/Partial___91_a-string_93__58_string__', `for property ${propertyName}`); + expect(propertySchema?.properties?.indexedDeprecated?.$ref).to.eq('#/components/schemas/Partial___91_a-string_93__58_string__', `for property ${propertyName}`); + expect(propertySchema?.properties?.indexedValidators?.$ref).to.eq('#/components/schemas/Partial___91_a-string_93__58_string__', `for property ${propertyName}`); + expect(propertySchema?.properties?.indexedExamples?.$ref).to.eq('#/components/schemas/Partial___91_a-string_93__58_string__', `for property ${propertyName}`); + expect(propertySchema?.properties?.indexedExtensions?.$ref).to.eq('#/components/schemas/Partial___91_a-string_93__58_string__', `for property ${propertyName}`); + expect(propertySchema?.properties?.indexedIgnored?.$ref).to.eq('#/components/schemas/Partial___91_a-string_93__58_string__', `for property ${propertyName}`); + + expect(Object.keys(propertySchema?.properties || {}).length).to.eq(18, `for property ${propertyName}`); + + const simpleSchema = getComponentSchema('Partial__a-string__', currentSpec); + expect(simpleSchema).to.deep.eq( + { + properties: { + a: { + type: 'string', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + }, + type: 'object', + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.simple`, + ); + const commentedSchema = getComponentSchema('Partial__a_description-comment_-string__', currentSpec); + expect(commentedSchema).to.deep.eq( + { + properties: { + a: { + type: 'string', + description: 'comment', + default: undefined, + example: undefined, + format: undefined, + }, + }, + type: 'object', + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.commented`, + ); + const multilineCommentedSchema = getComponentSchema('Partial__a_description-multiline_92_ncomment_-string__', currentSpec); + const expectedDescription = os.platform() === 'win32' ? 'multiline\r\ncomment' : `multiline\ncomment`; + expect(multilineCommentedSchema).to.deep.eq( + { + properties: { + a: { + type: 'string', + description: expectedDescription, + default: undefined, + example: undefined, + format: undefined, + }, + }, + type: 'object', + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.multilineCommented`, + ); + const defaultValueSchema = getComponentSchema('Partial__a_default-true_-string__', currentSpec); + expect(defaultValueSchema).to.deep.eq( + { + properties: { + a: { + type: 'string', + default: 'true', + description: undefined, + example: undefined, + format: undefined, + }, + }, + type: 'object', + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.defaultValue`, + ); + const deprecatedSchema = getComponentSchema('Partial__a_deprecated-true_-string__', currentSpec); + expect(deprecatedSchema).to.deep.eq( + { + properties: { + a: { + type: 'string', + deprecated: true, + description: undefined, + default: undefined, + example: undefined, + format: undefined, + }, + }, + type: 'object', + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.deprecated`, + ); + const validatorsSchema = getComponentSchema('Partial__a_validators_58__minLength_58__value_58_3___-string__', currentSpec); + expect(validatorsSchema).to.deep.eq( + { + properties: { + a: { + type: 'string', + minLength: 3, + description: undefined, + default: undefined, + example: undefined, + format: undefined, + }, + }, + type: 'object', + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.validators`, + ); + const examplesSchema = getComponentSchema('Partial__a_example-example_-string__', currentSpec); + expect(examplesSchema).to.deep.eq( + { + default: undefined, + description: 'Make all properties in T optional', + example: undefined, + format: undefined, + properties: { + a: { + default: undefined, + description: undefined, + example: 'example', + format: undefined, + type: 'string', + }, + }, + type: 'object', + }, + `for property ${propertyName}.examples`, + ); + const extensionsSchema = getComponentSchema('Partial__a_extensions_58__91__key-x-key-1.value-value-1__93__-string__', currentSpec); + expect(extensionsSchema).to.deep.eq( + { + properties: { + a: { + type: 'string', + 'x-key-1': 'value-1', + description: undefined, + default: undefined, + example: undefined, + format: undefined, + }, + }, + type: 'object', + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.extensions`, + ); + const ignoredSchema = getComponentSchema('Partial__a_ignored-true_-string__', currentSpec); + expect(ignoredSchema).to.deep.eq( + { + properties: {}, + type: 'object', + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.ignored`, + ); + const indexedSchema = getComponentSchema('Partial___91_a-string_93__58_string__', currentSpec); + expect(indexedSchema).to.deep.eq( + { + properties: {}, + additionalProperties: { + type: 'string', + }, + type: 'object', + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.indexedSimple`, + ); + }, + jsdocMap: (propertyName, propertySchema) => { + expect(propertySchema?.properties?.omitted?.$ref).to.eq('#/components/schemas/Omit_JsDocced.notRelevant_', `for property ${propertyName}`); + expect(propertySchema?.properties?.partial?.$ref).to.eq('#/components/schemas/Partial_JsDocced_', `for property ${propertyName}`); + expect(propertySchema?.properties?.replacedTypes?.$ref).to.eq('#/components/schemas/ReplaceStringAndNumberTypes_JsDocced_', `for property ${propertyName}`); + expect(propertySchema?.properties?.doubleReplacedTypes?.$ref).to.eq( + '#/components/schemas/ReplaceStringAndNumberTypes_ReplaceStringAndNumberTypes_JsDocced__', + `for property ${propertyName}`, + ); + expect(propertySchema?.properties?.postfixed?.$ref).to.eq('#/components/schemas/Postfixed_JsDocced._PostFix_', `for property ${propertyName}`); + expect(propertySchema?.properties?.values?.$ref).to.eq('#/components/schemas/Values_JsDocced_', `for property ${propertyName}`); + expect(propertySchema?.properties?.typesValues?.$ref).to.eq('#/components/schemas/InternalTypes_Values_JsDocced__', `for property ${propertyName}`); + expect(propertySchema?.properties?.onlyOneValue).to.deep.eq( + { + type: 'number', + format: 'double', + default: undefined, + description: undefined, + example: undefined, + }, + `for property ${propertyName}.onlyOneValue`, + ); + expect(propertySchema?.properties?.synonym?.$ref).to.eq('#/components/schemas/JsDoccedSynonym', `for property ${propertyName}`); + expect(propertySchema?.properties?.synonym2?.$ref).to.eq('#/components/schemas/JsDoccedSynonym2', `for property ${propertyName}`); + + expect(Object.keys(propertySchema?.properties || {}).length).to.eq(10, `for property ${propertyName}`); + + const omittedSchema = getComponentSchema('Omit_JsDocced.notRelevant_', currentSpec); + expect(omittedSchema).to.deep.eq( + { + $ref: '#/components/schemas/Pick_JsDocced.Exclude_keyofJsDocced.notRelevant__', + description: 'Construct a type with the properties of T except for those in type K.', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.omitted`, + ); + const omittedSchema2 = getComponentSchema('Pick_JsDocced.Exclude_keyofJsDocced.notRelevant__', currentSpec); + expect(omittedSchema2).to.deep.eq( + { + properties: { + stringValue: { + type: 'string', + default: 'def', + maxLength: 3, + description: undefined, + example: undefined, + format: undefined, + }, + numberValue: { + type: 'integer', + format: 'int32', + default: 6, + description: undefined, + example: undefined, + }, + }, + type: 'object', + description: 'From T, pick a set of properties whose keys are in the union K', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.omitted`, + ); + const partialSchema = getComponentSchema('Partial_JsDocced_', currentSpec); + expect(partialSchema).to.deep.eq( + { + properties: { + stringValue: { + type: 'string', + default: 'def', + maxLength: 3, + format: undefined, + example: undefined, + description: undefined, + }, + numberValue: { + type: 'integer', + format: 'int32', + default: 6, + example: undefined, + description: undefined, + }, + }, + type: 'object', + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.partial`, + ); + const replacedTypesSchema = getComponentSchema('ReplaceStringAndNumberTypes_JsDocced_', currentSpec); + expect(replacedTypesSchema).to.deep.eq( + { + $ref: '#/components/schemas/ReplaceTypes_JsDocced.string.number_', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.replacedTypes`, + ); + const replacedTypes2Schema = getComponentSchema('ReplaceTypes_JsDocced.string.number_', currentSpec); + expect(replacedTypes2Schema).to.deep.eq( + { + properties: { + stringValue: { + type: 'number', + format: 'double', + default: 'def', + maxLength: 3, + description: undefined, + example: undefined, + }, + numberValue: { + type: 'string', + default: 6, + description: undefined, + example: undefined, + format: undefined, + }, + }, + type: 'object', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.replacedTypes`, + ); + const doubleReplacedTypesSchema = getComponentSchema('ReplaceStringAndNumberTypes_ReplaceStringAndNumberTypes_JsDocced__', currentSpec); + expect(doubleReplacedTypesSchema).to.deep.eq( + { + $ref: '#/components/schemas/ReplaceTypes_ReplaceStringAndNumberTypes_JsDocced_.string.number_', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.doubleReplacedTypes`, + ); + const doubleReplacedTypes2Schema = getComponentSchema('ReplaceTypes_ReplaceStringAndNumberTypes_JsDocced_.string.number_', currentSpec); + expect(doubleReplacedTypes2Schema).to.deep.eq( + { + properties: { + stringValue: { + type: 'string', + default: 'def', + maxLength: 3, + description: undefined, + example: undefined, + format: undefined, + }, + numberValue: { + type: 'integer', + format: 'int32', + default: 6, + description: undefined, + example: undefined, + }, + }, + type: 'object', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.doubleReplacedTypes`, + ); + const postfixedSchema = getComponentSchema('Postfixed_JsDocced._PostFix_', currentSpec); + expect(postfixedSchema).to.deep.eq( + { + properties: { + stringValue_PostFix: { + type: 'string', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + numberValue_PostFix: { + type: 'number', + format: 'double', + default: undefined, + description: undefined, + example: undefined, + }, + }, + required: ['stringValue_PostFix', 'numberValue_PostFix'], + type: 'object', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.postfixed`, + ); + const valuesSchema = getComponentSchema('Values_JsDocced_', currentSpec); + expect(valuesSchema).to.deep.eq( + { + properties: { + stringValue: { + properties: { + value: { + type: 'string', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + }, + required: ['value'], + type: 'object', + default: 'def', + maxLength: 3, + description: undefined, + example: undefined, + format: undefined, + }, + numberValue: { + properties: { + value: { + type: 'number', + format: 'double', + default: undefined, + description: undefined, + example: undefined, + }, + }, + required: ['value'], + type: 'object', + default: 6, + description: undefined, + example: undefined, + format: undefined, + }, + }, + type: 'object', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.values`, + ); + const typesValuesSchema = getComponentSchema('InternalTypes_Values_JsDocced__', currentSpec); + expect(typesValuesSchema).to.deep.eq( + { + properties: { + stringValue: { + type: 'string', + default: 'def', + maxLength: 3, + description: undefined, + example: undefined, + format: undefined, + }, + numberValue: { + type: 'integer', + format: 'int32', + default: 6, + description: undefined, + example: undefined, + }, + }, + type: 'object', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.typesValues`, + ); + + const synonymSchema = getComponentSchema('JsDoccedSynonym', currentSpec); + expect(synonymSchema).to.deep.eq( + { + properties: { + stringValue: { + type: 'string', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + numberValue: { + type: 'number', + format: 'double', + default: undefined, + description: undefined, + example: undefined, + }, + }, + required: ['stringValue', 'numberValue'], + type: 'object', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.synonym`, + ); + const synonym2Schema = getComponentSchema('JsDoccedSynonym2', currentSpec); + expect(synonym2Schema).to.deep.eq( + { + properties: { + stringValue: { + type: 'string', + default: 'def', + maxLength: 3, + description: undefined, + example: undefined, + format: undefined, + }, + numberValue: { + type: 'integer', + format: 'int32', + default: 6, + description: undefined, + example: undefined, + }, + }, + type: 'object', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.synonym2`, + ); + }, + duplicatedDefinitions: (propertyName, propertySchema) => { + expect(propertySchema?.properties?.interfaces?.$ref).to.eq('#/components/schemas/DuplicatedInterface', `for property ${propertyName}`); + expect(propertySchema?.properties?.enums?.$ref).to.eq('#/components/schemas/DuplicatedEnum', `for property ${propertyName}`); + expect(propertySchema?.properties?.enumMember?.$ref).to.eq('#/components/schemas/DuplicatedEnum.C', `for property ${propertyName}`); + expect(propertySchema?.properties?.namespaceMember?.$ref).to.eq('#/components/schemas/DuplicatedEnum.D', `for property ${propertyName}`); + + expect(Object.keys(propertySchema?.properties || {}).length).to.eq(4, `for property ${propertyName}`); + + const interfacesSchema = getComponentSchema('DuplicatedInterface', currentSpec); + expect(interfacesSchema).to.deep.eq( + { + properties: { + a: { + type: 'string', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + b: { + type: 'string', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + }, + required: ['a', 'b'], + type: 'object', + additionalProperties: currentSpec.specName === 'specWithNoImplicitExtras' ? false : true, + description: undefined, + }, + `for property ${propertyName}.interfaces`, + ); + const enumsSchema = getComponentSchema('DuplicatedEnum', currentSpec); + expect(enumsSchema).to.deep.eq( + { + enum: ['AA', 'BB', 'CC'], + type: 'string', + description: undefined, + }, + `for property ${propertyName}.enums`, + ); + const enumMemberSchema = getComponentSchema('DuplicatedEnum.C', currentSpec); + expect(enumMemberSchema).to.deep.eq( + { + enum: ['CC'], + type: 'string', + description: undefined, + }, + `for property ${propertyName}.enumMember`, + ); + const namespaceMemberSchema = getComponentSchema('DuplicatedEnum.D', currentSpec); + expect(namespaceMemberSchema).to.deep.eq( + { + enum: ['DD'], + type: 'string', + nullable: false, + description: undefined, + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.namespaceMember`, + ); + }, + mappeds: (propertyName, propertySchema) => { + expect(propertySchema?.properties?.unionMap?.$ref).to.eq('#/components/schemas/Partial__a-string_-or-_b-number__', `for property ${propertyName}`); + expect(propertySchema?.properties?.indexedUnionMap?.$ref).to.eq('#/components/schemas/Partial__a-string_-or-__91_b-string_93__58_number__', `for property ${propertyName}`); + expect(propertySchema?.properties?.doubleIndexedUnionMap?.$ref).to.eq( + '#/components/schemas/Partial___91_a-string_93__58_string_-or-__91_b-string_93__58_number__', + `for property ${propertyName}`, + ); + expect(propertySchema?.properties?.intersectionMap?.$ref).to.eq('#/components/schemas/Partial__a-string_-and-_b-number__', `for property ${propertyName}`); + expect(propertySchema?.properties?.indexedIntersectionMap?.$ref).to.eq('#/components/schemas/Partial__a-string_-and-__91_b-string_93__58_number__', `for property ${propertyName}`); + expect(propertySchema?.properties?.doubleIndexedIntersectionMap?.$ref).to.eq( + '#/components/schemas/Partial___91_a-string_93__58_string_-and-__91_b-number_93__58_number__', + `for property ${propertyName}`, + ); + expect(propertySchema?.properties?.parenthesizedMap?.$ref).to.eq('#/components/schemas/Partial__a-string_-or-_40__b-string_-and-_c-string__41__', `for property ${propertyName}`); + expect(propertySchema?.properties?.parenthesizedMap2?.$ref).to.eq('#/components/schemas/Partial__40__a-string_-or-_b-string__41_-and-_c-string__', `for property ${propertyName}`); + expect(propertySchema?.properties?.undefinedMap?.$ref).to.eq('#/components/schemas/Partial_undefined_', `for property ${propertyName}`); + expect(propertySchema?.properties?.nullMap?.$ref).to.eq('#/components/schemas/Partial_null_', `for property ${propertyName}`); + + expect(Object.keys(propertySchema?.properties || {}).length).to.eq(10, `for property ${propertyName}`); + + const unionMapSchema = getComponentSchema('Partial__a-string_-or-_b-number__', currentSpec); + expect(unionMapSchema).to.deep.eq( + { + anyOf: [ + { + properties: { + a: { + type: 'string', + }, + }, + type: 'object', + }, + { + properties: { + b: { + format: 'double', + type: 'number', + }, + }, + type: 'object', + }, + ], + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.unionMap`, + ); + const indexedUnionMapSchema = getComponentSchema('Partial__a-string_-or-__91_b-string_93__58_number__', currentSpec); + expect(indexedUnionMapSchema).to.deep.eq( + { + anyOf: [ + { + properties: { + a: { + type: 'string', + }, + }, + type: 'object', + }, + { + additionalProperties: { + format: 'double', + type: 'number', + }, + properties: {}, + type: 'object', + }, + ], + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.indexedUnionMap`, + ); + const doubleIndexedUnionMapSchema = getComponentSchema('Partial___91_a-string_93__58_string_-or-__91_b-string_93__58_number__', currentSpec); + expect(doubleIndexedUnionMapSchema).to.deep.eq( + { + anyOf: [ + { + additionalProperties: { + type: 'string', + }, + properties: {}, + type: 'object', + }, + { + additionalProperties: { + format: 'double', + type: 'number', + }, + properties: {}, + type: 'object', + }, + ], + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.doubleIndexedUnionMap`, + ); + const intersectionMapSchema = getComponentSchema('Partial__a-string_-and-_b-number__', currentSpec); + expect(intersectionMapSchema).to.deep.eq( + { + properties: { + a: { + type: 'string', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + b: { + type: 'number', + format: 'double', + default: undefined, + description: undefined, + example: undefined, + }, + }, + type: 'object', + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.intersectionMap`, + ); + const indexedIntersectionMapSchema = getComponentSchema('Partial__a-string_-and-__91_b-string_93__58_number__', currentSpec); + expect(indexedIntersectionMapSchema).to.deep.eq( + { + properties: { + a: { + type: 'string', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + }, + additionalProperties: { + type: 'number', + format: 'double', + }, + type: 'object', + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.indexedIntersectionMap`, + ); + const doubleIndexedIntersectionMapSchema = getComponentSchema('Partial___91_a-string_93__58_string_-and-__91_b-number_93__58_number__', currentSpec); + expect(doubleIndexedIntersectionMapSchema).to.deep.eq( + { + properties: {}, + additionalProperties: { + anyOf: [ + { + type: 'string', + }, + { + format: 'double', + type: 'number', + }, + ], + }, + type: 'object', + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.doubleIndexedIntersectionMap`, + ); + const parenthesizedMapSchema = getComponentSchema('Partial__a-string_-or-_40__b-string_-and-_c-string__41__', currentSpec); + expect(parenthesizedMapSchema).to.deep.eq( + { + anyOf: [ + { + properties: { + a: { + type: 'string', + }, + }, + type: 'object', + }, + { + properties: { + b: { + type: 'string', + }, + c: { + type: 'string', + }, + }, + type: 'object', + }, + ], + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.parenthesizedMap`, + ); + const parenthesizedMap2Schema = getComponentSchema('Partial__40__a-string_-or-_b-string__41_-and-_c-string__', currentSpec); + expect(parenthesizedMap2Schema).to.deep.eq( + { + anyOf: [ + { + properties: { + a: { + type: 'string', + }, + c: { + type: 'string', + }, + }, + type: 'object', + }, + { + properties: { + b: { + type: 'string', + }, + c: { + type: 'string', + }, + }, + type: 'object', + }, + ], + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.parenthesizedMap2`, + ); + const undefinedMapSchema = getComponentSchema('Partial_undefined_', currentSpec); + expect(undefinedMapSchema).to.deep.eq( + { + default: undefined, + description: 'Make all properties in T optional', + example: undefined, + format: undefined, + }, + `for property ${propertyName}.undefinedMap`, + ); + const nullMapSchema = getComponentSchema('Partial_null_', currentSpec); + expect(nullMapSchema).to.deep.eq( + { + enum: [null], + type: 'number', + nullable: true, + default: undefined, + description: 'Make all properties in T optional', + example: undefined, + format: undefined, + }, + `for property ${propertyName}.nullMap`, + ); + }, + conditionals: (propertyName, propertySchema) => { + expect(propertySchema?.properties?.simpeConditional).to.deep.eq( + { + type: 'number', + format: 'double', + default: undefined, + description: undefined, + example: undefined, + }, + `for property ${propertyName}`, + ); + expect(propertySchema?.properties?.simpeFalseConditional).to.deep.eq( + { + type: 'boolean', + format: undefined, + default: undefined, + description: undefined, + example: undefined, + }, + `for property ${propertyName}`, + ); + expect(propertySchema?.properties?.typedConditional?.$ref).to.eq('#/components/schemas/Conditional_string.string.number.boolean_', `for property ${propertyName}`); + expect(propertySchema?.properties?.typedFalseConditional?.$ref).to.eq('#/components/schemas/Conditional_string.number.number.boolean_', `for property ${propertyName}`); + expect(propertySchema?.properties?.dummyConditional?.$ref).to.eq('#/components/schemas/Dummy_Conditional_string.string.number.boolean__', `for property ${propertyName}`); + expect(propertySchema?.properties?.dummyFalseConditional?.$ref).to.eq('#/components/schemas/Dummy_Conditional_string.number.number.boolean__', `for property ${propertyName}`); + expect(propertySchema?.properties?.mappedConditional?.$ref).to.eq('#/components/schemas/Partial_stringextendsstring_63__a-number_-never_', `for property ${propertyName}`); + expect(propertySchema?.properties?.mappedTypedConditional?.$ref).to.eq('#/components/schemas/Partial_Conditional_string.string._a-number_.never__', `for property ${propertyName}`); + + expect(Object.keys(propertySchema?.properties || {}).length).to.eq(8, `for property ${propertyName}`); + + const typedConditionalSchema = getComponentSchema('Conditional_string.string.number.boolean_', currentSpec); + expect(typedConditionalSchema).to.deep.eq( + { + type: 'number', + format: 'double', + default: undefined, + description: undefined, + example: undefined, + }, + `for property ${propertyName}.typedConditional`, + ); + const typedFalseConditionalSchema = getComponentSchema('Conditional_string.number.number.boolean_', currentSpec); + expect(typedFalseConditionalSchema).to.deep.eq( + { + type: 'boolean', + format: undefined, + default: undefined, + description: undefined, + example: undefined, + }, + `for property ${propertyName}.typedFalseConditional`, + ); + const dummyConditionalSchema = getComponentSchema('Dummy_Conditional_string.string.number.boolean__', currentSpec); + expect(dummyConditionalSchema?.$ref).to.eq('#/components/schemas/Conditional_string.string.number.boolean_', `for property ${propertyName}.dummyConditional`); + const dummyFalseConditionalSchema = getComponentSchema('Dummy_Conditional_string.number.number.boolean__', currentSpec); + expect(dummyFalseConditionalSchema?.$ref).to.eq('#/components/schemas/Conditional_string.number.number.boolean_', `for property ${propertyName}.dummyFalseConditional`); + const mappedConditionalSchema = getComponentSchema('Partial_stringextendsstring_63__a-number_-never_', currentSpec); + expect(mappedConditionalSchema).to.deep.eq( + { + properties: { + a: { + type: 'number', + format: 'double', + default: undefined, + description: undefined, + example: undefined, + }, + }, + type: 'object', + description: 'Make all properties in T optional', + example: undefined, + default: undefined, + format: undefined, + }, + `for property ${propertyName}.mappedConditional`, + ); + const mappedTypedConditionalSchema = getComponentSchema('Partial_Conditional_string.string._a-number_.never__', currentSpec); + expect(mappedTypedConditionalSchema).to.deep.eq(mappedConditionalSchema, `for property ${propertyName}.mappedTypedConditional`); + }, + typeOperators: (propertyName, propertySchema) => { + expect(propertySchema?.properties?.keysOfAny?.$ref).to.eq('#/components/schemas/KeysMember', `for property ${propertyName}`); + expect(propertySchema?.properties?.keysOfInterface?.$ref).to.eq('#/components/schemas/KeysMember_NestedTypeLiteral_', `for property ${propertyName}`); + expect(propertySchema?.properties?.simple).to.deep.eq( + { + type: 'string', + enum: ['a', 'b', 'e'], + nullable: false, + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.simple`, + ); + expect(propertySchema?.properties?.keyofItem).to.deep.eq( + { + type: 'string', + enum: ['c', 'd'], + nullable: false, + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.keyofItem`, + ); + expect(propertySchema?.properties?.keyofAnyItem).to.deep.eq( + { + anyOf: [ + { + type: 'string', + }, + { + format: 'double', + type: 'number', + }, + ], + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.keyofAnyItem`, + ); + expect(propertySchema?.properties?.keyofAny).to.deep.eq(propertySchema?.properties?.keyofAnyItem, `for property ${propertyName}.keyofAny`); + expect(propertySchema?.properties?.stringLiterals).to.deep.eq( + { + type: 'string', + enum: ['A', 'B', 'C'], + nullable: false, + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.stringLiterals`, + ); + expect(propertySchema?.properties?.stringAndNumberLiterals).to.deep.eq( + { + anyOf: [ + { + enum: ['A', 'B'], + type: 'string', + }, + { + enum: [3], + type: 'number', + }, + ], + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.stringAndNumberLiterals`, + ); + expect(propertySchema?.properties?.keyofEnum).to.deep.eq( + { + type: 'string', + enum: ['A', 'B', 'C'], + nullable: false, + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.keyofEnum`, + ); + expect(propertySchema?.properties?.numberAndStringKeys).to.deep.eq( + { + anyOf: [ + { + enum: ['a'], + type: 'string', + }, + { + enum: [3, 4], + type: 'number', + }, + ], + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.numberAndStringKeys`, + ); + expect(propertySchema?.properties?.oneStringKeyInterface).to.deep.eq( + { + type: 'string', + enum: ['a'], + nullable: false, + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.oneStringKeyInterface`, + ); + expect(propertySchema?.properties?.oneNumberKeyInterface).to.deep.eq( + { + type: 'number', + enum: [3], + nullable: false, + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.oneNumberKeyInterface`, + ); + expect(propertySchema?.properties?.indexStrings).to.deep.eq( + { + anyOf: [ + { + type: 'string', + }, + { + format: 'double', + type: 'number', + }, + ], + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.indexStrings`, + ); + expect(propertySchema?.properties?.indexNumbers).to.deep.eq( + { + type: 'number', + format: 'double', + default: undefined, + description: undefined, + example: undefined, + }, + `for property ${propertyName}.indexNumbers`, + ); + + expect(Object.keys(propertySchema?.properties || {}).length).to.eq(14, `for property ${propertyName}`); + + const keysOfAnySchema = getComponentSchema('KeysMember', currentSpec); + expect(keysOfAnySchema).to.deep.eq( + { + properties: { + keys: { + anyOf: [ + { + type: 'string', + }, + { + format: 'double', + type: 'number', + }, + ], + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + }, + required: ['keys'], + type: 'object', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.keysOfAny`, + ); + + const keysOfInterfaceSchema = getComponentSchema('KeysMember_NestedTypeLiteral_', currentSpec); + expect(keysOfInterfaceSchema).to.deep.eq( + { + properties: { + keys: { + type: 'string', + enum: ['a', 'b', 'e'], + nullable: false, + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + }, + required: ['keys'], + type: 'object', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.keysOfInterface`, + ); + }, + nestedTypes: (propertyName, propertySchema) => { + expect(propertySchema?.properties?.multiplePartial?.$ref).to.eq('#/components/schemas/Partial_Partial__a-string___', `for property ${propertyName}`); + expect(propertySchema?.properties?.separateField?.$ref).to.eq('#/components/schemas/Partial_SeparateField_Partial__a-string--b-string__.a__', `for property ${propertyName}`); + expect(propertySchema?.properties?.separateField2?.$ref).to.eq('#/components/schemas/Partial_SeparateField_Partial__a-string--b-string__.a-or-b__', `for property ${propertyName}`); + expect(propertySchema?.properties?.separateField3?.$ref).to.eq('#/components/schemas/Partial_SeparateField_Partial__a-string--b-number__.a-or-b__', `for property ${propertyName}`); + + expect(Object.keys(propertySchema?.properties || {}).length).to.eq(4, `for property ${propertyName}`); + + const multiplePartialSchema = getComponentSchema('Partial_Partial__a-string___', currentSpec); + expect(multiplePartialSchema).to.deep.eq( + { + properties: { + a: { + type: 'string', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + }, + type: 'object', + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.multiplePartial`, + ); + const separateFieldSchema = getComponentSchema('Partial_SeparateField_Partial__a-string--b-string__.a__', currentSpec); + expect(separateFieldSchema).to.deep.eq( + { + properties: { + omitted: { + $ref: '#/components/schemas/Omit_Partial__a-string--b-string__.a_', + description: undefined, + example: undefined, + format: undefined, + }, + field: { + type: 'string', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + }, + type: 'object', + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.separateField`, + ); + const separateFieldInternalSchema = getComponentSchema('Omit_Partial__a-string--b-string__.a_', currentSpec); + expect(separateFieldInternalSchema).to.deep.eq( + { + $ref: '#/components/schemas/Pick_Partial__a-string--b-string__.Exclude_keyofPartial__a-string--b-string__.a__', + description: 'Construct a type with the properties of T except for those in type K.', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.separateField.omitted`, + ); + + const separateFieldInternal2Schema = getComponentSchema('Pick_Partial__a-string--b-string__.Exclude_keyofPartial__a-string--b-string__.a__', currentSpec); + expect(separateFieldInternal2Schema).to.deep.eq( + { + properties: { + b: { + type: 'string', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + }, + type: 'object', + description: 'From T, pick a set of properties whose keys are in the union K', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.separateField.omitted`, + ); + + const separateField2Schema = getComponentSchema('Partial_SeparateField_Partial__a-string--b-string__.a-or-b__', currentSpec); + expect(separateField2Schema).to.deep.eq( + { + properties: { + omitted: { + $ref: '#/components/schemas/Omit_Partial__a-string--b-string__.a-or-b_', + description: undefined, + example: undefined, + format: undefined, + }, + field: { + type: 'string', + default: undefined, + description: undefined, + example: undefined, + format: undefined, + }, + }, + type: 'object', + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.separateField2`, + ); + const separateField2InternalSchema = getComponentSchema('Omit_Partial__a-string--b-string__.a-or-b_', currentSpec); + expect(separateField2InternalSchema?.$ref).to.eq( + '#/components/schemas/Pick_Partial__a-string--b-string__.Exclude_keyofPartial__a-string--b-string__.a-or-b__', + `for property ${propertyName}.separateField2.omitted`, + ); + const separateField2Internal2Schema = getComponentSchema('Pick_Partial__a-string--b-string__.Exclude_keyofPartial__a-string--b-string__.a-or-b__', currentSpec); + expect(separateField2Internal2Schema).to.deep.eq( + { + properties: {}, + type: 'object', + description: 'From T, pick a set of properties whose keys are in the union K', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.separateField2.omitted`, + ); + + const separateField3Schema = getComponentSchema('Partial_SeparateField_Partial__a-string--b-number__.a-or-b__', currentSpec); + expect(separateField3Schema).to.deep.eq( + { + properties: { + omitted: { + $ref: '#/components/schemas/Omit_Partial__a-string--b-number__.a-or-b_', + description: undefined, + example: undefined, + format: undefined, + }, + field: { + anyOf: [ + { + type: 'string', + }, + { + format: 'double', + type: 'number', + }, + ], + description: undefined, + default: undefined, + example: undefined, + format: undefined, + }, + }, + type: 'object', + description: 'Make all properties in T optional', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.separateField3`, + ); + const separateField3InternalSchema = getComponentSchema('Omit_Partial__a-string--b-number__.a-or-b_', currentSpec); + expect(separateField3InternalSchema?.$ref).to.eq( + '#/components/schemas/Pick_Partial__a-string--b-number__.Exclude_keyofPartial__a-string--b-number__.a-or-b__', + `for property ${propertyName}.separateField3.omitted`, + ); + const separateField3Internal2Schema = getComponentSchema('Pick_Partial__a-string--b-number__.Exclude_keyofPartial__a-string--b-number__.a-or-b__', currentSpec); + expect(separateField3Internal2Schema).to.deep.eq( + { + properties: {}, + type: 'object', + description: 'From T, pick a set of properties whose keys are in the union K', + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}.separateField3.omitted`, + ); + }, + nullableStringLiteral: (propertyName, propertySchema) => { + expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); + expect(propertySchema.nullable).to.eq(true, `for property ${propertyName}[x-nullable]`); + + expect(propertySchema).to.deep.eq({ + type: 'string', + enum: ['NULLABLE_LIT_1', 'NULLABLE_LIT_2', null], + nullable: true, + description: undefined, + example: undefined, + format: undefined, + default: undefined, + }); + }, + computedKeys: (propertyName, propertySchema) => { + expect(propertyName).to.eq('computedKeys'); + expect(propertySchema?.properties![EnumDynamicPropertyKey.STRING_KEY]).to.deep.eq( + { + type: 'string', + description: undefined, + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}`, + ); + expect(propertySchema?.properties![EnumDynamicPropertyKey.NUMBER_KEY]).to.deep.eq( + { + type: 'string', + description: undefined, + default: undefined, + example: undefined, + format: undefined, + }, + `for property ${propertyName}`, + ); + }, + }; + + const testModel = currentSpec.spec.components.schemas[interfaceModelName]; + (Object.keys(assertionsPerProperty) as Array).forEach(aPropertyName => { + if (!testModel) { + throw new Error(`There was no schema generated for the ${currentSpec.specName}`); + } + const propertySchema = testModel.properties![aPropertyName]; + if (!propertySchema) { + throw new Error(`There was no ${aPropertyName} schema generated for the ${currentSpec.specName}`); + } + it(`should produce a valid schema for the ${aPropertyName} property on ${interfaceModelName} for the ${currentSpec.specName}`, () => { + assertionsPerProperty[aPropertyName](aPropertyName, propertySchema); + }); + }); + + it('should make a choice about additionalProperties', () => { + if (currentSpec.specName === 'specWithNoImplicitExtras') { + expect(testModel.additionalProperties).to.eq(false, forSpec(currentSpec)); + } else { + expect(testModel.additionalProperties).to.eq(true, forSpec(currentSpec)); + } + }); + + it('should have only created schemas for properties on the TypeScript interface', () => { + expect(Object.keys(assertionsPerProperty)).to.length( + Object.keys(testModel.properties!).length, + `because the swagger spec (${currentSpec.specName}) should only produce property schemas for properties that live on the TypeScript interface.`, + ); + }); + }); + }); + }); + + describe('Deprecated class properties', () => { + allSpecs.forEach(currentSpec => { + const modelName = 'TestClassModel'; + // Assert + if (!currentSpec.spec.components.schemas) { + throw new Error('spec.components.schemas should have been truthy'); + } + const definition = currentSpec.spec.components.schemas[modelName]; + + if (!definition.properties) { + throw new Error('Definition has no properties.'); + } + + const properties = definition.properties; + + describe(`for ${currentSpec.specName}`, () => { + it('should only mark deprecated properties as deprecated', () => { + const deprecatedPropertyNames = ['deprecated1', 'deprecated2', 'deprecatedPublicConstructorVar', 'deprecatedPublicConstructorVar2']; + Object.entries(properties).forEach(([propertyName, property]) => { + if (deprecatedPropertyNames.includes(propertyName)) { + expect(property.deprecated).to.eq(true, `for property ${propertyName}.deprecated`); + } else { + expect(property.deprecated).to.eq(undefined, `for property ${propertyName}.deprecated`); + } + }); + }); + }); + }); + }); + + describe('Extension class properties', () => { + allSpecs.forEach(currentSpec => { + const modelName = 'TestClassModel'; + // Assert + if (!currentSpec.spec.components.schemas) { + throw new Error('spec.components.schemas should have been truthy'); + } + const definition = currentSpec.spec.components.schemas[modelName]; + + if (!definition.properties) { + throw new Error('Definition has no properties.'); + } + + const properties = definition.properties; + + describe(`for ${currentSpec.specName}`, () => { + it('should put vendor extension on extension field with decorator', () => { + const extensionPropertyName = 'extensionTest'; + + Object.entries(properties).forEach(([propertyName, property]) => { + if (extensionPropertyName === propertyName) { + expect(property).to.have.property('x-key-1'); + expect(property).to.have.property('x-key-2'); + + expect(property['x-key-1']).to.deep.equal('value-1'); + expect(property['x-key-2']).to.deep.equal('value-2'); + } + }); + }); + + it('should put vendor extension on extension field with commetn', () => { + const extensionPropertyName = 'extensionComment'; + + Object.entries(properties).forEach(([propertyName, property]) => { + if (extensionPropertyName === propertyName) { + expect(property).to.have.property('x-key-1'); + expect(property).to.have.property('x-key-2'); + + expect(property['x-key-1']).to.deep.equal('value-1'); + expect(property['x-key-2']).to.deep.equal('value-2'); + } + }); + }); + }); + }); + }); + + describe('mixed Enums', () => { + it('should combine to metaschema', () => { + // Arrange + const schemaName = 'tooManyTypesEnum'; + const metadataForEnums: Tsoa.Metadata = { + controllers: [], + referenceTypeMap: { + [schemaName]: { + refName: schemaName, + dataType: 'refEnum', + enums: [1, 'two', 3, 'four'], + deprecated: false, + }, + }, + }; + const swaggerConfig: ExtendedSpecConfig = { + outputDirectory: 'mockOutputDirectory', + entryFile: 'mockEntryFile', + noImplicitAdditionalProperties: 'ignore', + }; + + // Act + const spec = new SpecGenerator31(metadataForEnums, swaggerConfig).GetSpec(); + + // Assert + expect(getComponentSchema(schemaName, { specName: 'specDefault', spec })).to.deep.eq({ + description: undefined, + anyOf: [ + { type: 'number', enum: [1, 3] }, + { type: 'string', enum: ['two', 'four'] }, + ], + }); + }); + }); + + describe('Extensions schema generation', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/methodController').Generate(); + const spec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + + if (!spec.paths) { + throw new Error('No spec info.'); + } + + const extensionPath = spec.paths['/MethodTest/Extension'].get; + + if (!extensionPath) { + throw new Error('extension method was not rendered'); + } + + // Verify that extensions are appended to the path + expect(extensionPath).to.have.property('x-attKey'); + expect(extensionPath).to.have.property('x-attKey1'); + expect(extensionPath).to.have.property('x-attKey2'); + expect(extensionPath).to.have.property('x-attKey3'); + expect(extensionPath).to.have.property('x-attKey4'); + expect(extensionPath).to.have.property('x-attKey5'); + expect(extensionPath).to.have.property('x-attKey6'); + expect(extensionPath).to.have.property('x-attKey7'); + expect(extensionPath).to.have.property('x-attKey8'); + + // Verify that extensions have correct values + expect(extensionPath['x-attKey']).to.deep.equal('attValue'); + expect(extensionPath['x-attKey1']).to.deep.equal(123); + expect(extensionPath['x-attKey2']).to.deep.equal(true); + expect(extensionPath['x-attKey3']).to.deep.equal(null); + expect(extensionPath['x-attKey4']).to.deep.equal({ test: 'testVal' }); + expect(extensionPath['x-attKey5']).to.deep.equal(['y0', 'y1', 123, true, null]); + expect(extensionPath['x-attKey6']).to.deep.equal([{ y0: 'yt0', y1: 'yt1', y2: 123, y3: true, y4: null }, { y2: 'yt2' }]); + expect(extensionPath['x-attKey7']).to.deep.equal({ test: ['testVal', 123, true, null] }); + expect(extensionPath['x-attKey8']).to.deep.equal({ test: { testArray: ['testVal1', true, null, ['testVal2', 'testVal3', 123, true, null]] } }); + }); + + describe('Inner interface', () => { + it('should generate the proper schema', () => { + const ref = specDefault.spec.paths['/GetTest/InnerInterface'].get?.responses['200'].content?.['application/json']['schema']?.['$ref']; + expect(ref).to.equal('#/components/schemas/InnerInterface'); + expect(getComponentSchema('InnerInterface', specDefault)).to.deep.equal({ + additionalProperties: true, + description: undefined, + properties: { + value: { + default: undefined, + description: undefined, + example: undefined, + format: undefined, + type: 'string', + }, + }, + required: undefined, + type: 'object', + }); + }); + }); + + describe('module declarations with namespaces', () => { + it('should generate the proper schema for a model declared in a namespace in a module', () => { + /* tslint:disable:no-string-literal */ + const ref = specDefault.spec.paths['/GetTest/ModuleRedeclarationAndNamespace'].get?.responses['200'].content?.['application/json']['schema']?.['$ref']; + /* tslint:enable:no-string-literal */ + expect(ref).to.equal('#/components/schemas/tsoaTest.TsoaTest.TestModel73'); + expect(getComponentSchema('tsoaTest.TsoaTest.TestModel73', specDefault)).to.deep.equal({ + additionalProperties: true, + description: undefined, + properties: { + value: { + default: undefined, + description: undefined, + example: undefined, + format: undefined, + type: 'string', + }, + }, + required: undefined, + type: 'object', + }); + }); + + it('should generate schema with namespace type casted object', () => { + const response = specDefault.spec.paths['/GetTest/NamespaceWithTypeCastedObject']?.get?.responses; + + expect(response).to.have.all.keys('200'); + }); + }); + + describe('@Res responses', () => { + const expectTestModelContent = (response?: Swagger.Response3) => { + expect(response?.content).to.deep.equal({ + 'application/json': { + schema: { + $ref: '#/components/schemas/TestModel', + }, + }, + }); + }; + + it('creates a single error response for a single res parameter', () => { + const responses = specDefault.spec.paths['/GetTest/Res']?.get?.responses; + + expect(responses).to.have.all.keys('204', '400'); + + expectTestModelContent(responses?.['400']); + }); + + it('creates multiple error responses for separate res parameters', () => { + const responses = specDefault.spec.paths['/GetTest/MultipleRes']?.get?.responses; + + expect(responses).to.have.all.keys('200', '400', '401'); + + expectTestModelContent(responses?.['400']); + expectTestModelContent(responses?.['401']); + }); + + it('creates multiple error responses for a combined res parameter', () => { + const responses = specDefault.spec.paths['/GetTest/MultipleStatusCodeRes']?.get?.responses; + + expect(responses).to.have.all.keys('204', '400', '500'); + + expectTestModelContent(responses?.['400']); + expectTestModelContent(responses?.['500']); + }); + + describe('With alias', () => { + it('creates a single error response for a single res parameter', () => { + const responses = specDefault.spec.paths['/GetTest/Res_Alias']?.get?.responses; + + expect(responses).to.have.all.keys('204', '400'); + + expectTestModelContent(responses?.['400']); + }); + + it('creates multiple error responses for separate res parameters', () => { + const responses = specDefault.spec.paths['/GetTest/MultipleRes_Alias']?.get?.responses; + + expect(responses).to.have.all.keys('200', '400', '401'); + + expectTestModelContent(responses?.['400']); + expectTestModelContent(responses?.['401']); + }); + + it('creates multiple error responses for a combined res parameter', () => { + const responses = specDefault.spec.paths['/GetTest/MultipleStatusCodeRes_Alias']?.get?.responses; + + expect(responses).to.have.all.keys('204', '400', '500'); + + expectTestModelContent(responses?.['400']); + expectTestModelContent(responses?.['500']); + }); + }); + }); + + describe('inline title tag generation', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/parameterController.ts').Generate(); + + it('should generate title tag for request', () => { + const currentSpec = new SpecGenerator31(metadata, { ...getDefaultExtendedOptions(), useTitleTagsForInlineObjects: true }).GetSpec(); + expect(currentSpec.paths['/ParameterTest/Inline1'].post?.responses['200'].content?.['application/json'].schema?.title).to.equal('Inline1Response'); + expect(currentSpec.paths['/ParameterTest/Inline1'].post?.requestBody?.content['application/json'].schema?.title).to.equal('Inline1RequestBody'); + }); + + it('should not generate title tag for request', () => { + const currentSpec = new SpecGenerator31(metadata, { ...getDefaultExtendedOptions(), useTitleTagsForInlineObjects: false }).GetSpec(); + expect(currentSpec.paths['/ParameterTest/Inline1'].post?.responses['200'].content?.['application/json'].schema?.title).to.equal(undefined); + expect(currentSpec.paths['/ParameterTest/Inline1'].post?.requestBody?.content['application/json'].schema?.title).to.equal(undefined); + }); + }); + + describe('should include valid params', () => { + it('should include query', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/parameterController.ts').Generate(); + const spec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + + const method = spec.paths['/ParameterTest/ParamaterQueryAnyType'].get?.parameters ?? []; + + expect(method).to.have.lengthOf(1); + const queryParam = method[0]; + expect(queryParam.in).to.equal('query'); + }); + + it('should include header', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/parameterController.ts').Generate(); + const spec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + + const method = spec.paths['/ParameterTest/ParameterHeaderStringType'].get?.parameters ?? []; + + expect(method).to.have.lengthOf(1); + const queryParam = method[0]; + expect(queryParam.in).to.equal('header'); + }); + + it('should include path', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/parameterController.ts').Generate(); + const spec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + + const method = spec.paths['/ParameterTest/Path/{test}'].get?.parameters ?? []; + + expect(method).to.have.lengthOf(1); + const queryParam = method[0]; + expect(queryParam.in).to.equal('path'); + }); + }); + + describe('should exclude @RequestProp', () => { + it('should exclude request-prop from method parameters', () => { + const metadata = new MetadataGenerator('./fixtures/controllers/parameterController.ts').Generate(); + const spec = new SpecGenerator31(metadata, getDefaultExtendedOptions()).GetSpec(); + + const method = spec.paths['/ParameterTest/RequestProps'].post?.parameters ?? []; + + expect(method).to.have.lengthOf(0); + + method.forEach(p => { + expect(p.in).to.not.equal('request-prop'); + }); + }); + }); + + describe('Tuple support', () => { + it('should generate prefixItems and items for fixed and variadic tuples', () => { + function resolveSchema(schema: Swagger.Schema31 | false, components: Record): Swagger.Schema31 { + if (schema === false) { + throw new Error('Schema was explicitly false, cannot resolve.'); + } + + if ('$ref' in schema) { + const refName = schema.$ref!.replace('#/components/schemas/', ''); + return components[refName]!; + } + return schema; + } + + const metadata = new MetadataGenerator('./fixtures/controllers/postController31.ts').Generate(); + const spec = new SpecGenerator31(metadata, defaultOptions).GetSpec(); + const components = spec.components?.schemas ?? {}; + + const fixedTupleSchema = components.StringAndNumberTuple; + const variadicTupleSchema = components.TupleWithRest; + + expect(fixedTupleSchema?.type).to.equal('array'); + expect(fixedTupleSchema?.prefixItems).to.be.an('array').with.lengthOf(2); + + const fixedFirst = resolveSchema(fixedTupleSchema.prefixItems![0], components); + const fixedSecond = resolveSchema(fixedTupleSchema.prefixItems![1], components); + expect(fixedFirst).to.deep.include({ type: 'string' }); + expect(fixedSecond).to.deep.include({ type: 'number' }); + expect(fixedTupleSchema).to.have.property('items'); + expect(fixedTupleSchema.items).to.be.false; + expect(fixedTupleSchema.minItems).to.equal(2); + expect(fixedTupleSchema.maxItems).to.equal(2); + + expect(variadicTupleSchema.type).to.equal('array'); + expect(variadicTupleSchema.prefixItems).to.be.an('array').with.lengthOf(1); + expect(variadicTupleSchema.minItems).to.equal(1); + expect(variadicTupleSchema).to.not.have.property('maxItems'); + + const variadicFirst = resolveSchema(variadicTupleSchema.prefixItems![0], components); + const variadicItems = resolveSchema(variadicTupleSchema.items!, components); + expect(variadicFirst).to.deep.include({ type: 'string' }); + expect(variadicItems).to.deep.include({ type: 'number' }); + }); + }); +}); diff --git a/tests/unit/utilities/specMerge.spec.ts b/tests/unit/utilities/specMerge.spec.ts index 9db843f0a..c36640144 100644 --- a/tests/unit/utilities/specMerge.spec.ts +++ b/tests/unit/utilities/specMerge.spec.ts @@ -11,7 +11,7 @@ describe('specMergins', () => { const defaultOptions: ExtendedSpecConfig = getDefaultExtendedOptions(); describe('recursive', () => { - const addedParameter = { + const addedParameter: Swagger.Parameter3 = { name: 'deepQueryParamObject', in: 'query', style: 'deepObject', @@ -27,7 +27,7 @@ describe('specMergins', () => { type: 'object', additionalProperties: true, }, - } as unknown as Swagger.QueryParameter; + }; const options: ExtendedSpecConfig = { ...defaultOptions, @@ -64,7 +64,7 @@ describe('specMergins', () => { }); describe('deepMerging', () => { - const addedParameter = { + const addedParameter: Swagger.Parameter3 = { name: 'appearance', in: 'query', style: 'deepObject', @@ -80,7 +80,7 @@ describe('specMergins', () => { type: 'object', additionalProperties: true, }, - } as unknown as Swagger.QueryParameter; + }; const options: ExtendedSpecConfig = { ...defaultOptions, diff --git a/tests/unit/utilities/verifyParameter.ts b/tests/unit/utilities/verifyParameter.ts index 488293f98..94329fce9 100644 --- a/tests/unit/utilities/verifyParameter.ts +++ b/tests/unit/utilities/verifyParameter.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { Swagger } from '@tsoa/runtime'; -export function VerifyPathableParameter(params: Swagger.Parameter[], paramValue: string, paramType: string, paramIn: string, formatType?: string) { +export function VerifyPathableParameter(params: Swagger.Parameter2[], paramValue: string, paramType: string, paramIn: string, formatType?: string) { const parameter = verifyParameter(params, paramValue, paramIn); expect(parameter.type).to.equal(paramType); if (formatType) { @@ -9,15 +9,7 @@ export function VerifyPathableParameter(params: Swagger.Parameter[], paramValue: } } -export function VerifyPathableStringParameter( - params: Swagger.PathParameter[] | Swagger.Parameter2[], - paramValue: string, - paramType: string, - paramIn: string, - min?: number, - max?: number, - pattern?: string, -) { +export function VerifyPathableStringParameter(params: Swagger.Parameter2[], paramValue: string, paramType: string, paramIn: string, min?: number, max?: number, pattern?: string) { const parameter = verifyParameter(params, paramValue, paramIn); expect(parameter.type).to.equal(paramType); if (min) { @@ -31,15 +23,7 @@ export function VerifyPathableStringParameter( } } -export function VerifyPathableNumberParameter( - params: Swagger.PathParameter[] | Swagger.Parameter2[], - paramValue: string, - paramType: string, - paramIn: string, - formatType?: string, - min?: number, - max?: number, -) { +export function VerifyPathableNumberParameter(params: Swagger.Parameter2[], paramValue: string, paramType: string, paramIn: string, formatType?: string, min?: number, max?: number) { const parameter = verifyParameter(params, paramValue, paramIn); expect(parameter.type).to.equal(paramType); if (formatType) { @@ -53,12 +37,12 @@ export function VerifyPathableNumberParameter( } } -export function VerifyBodyParameter(params: Swagger.Parameter[], paramValue: string, paramType: string, paramIn: string) { +export function VerifyBodyParameter(params: Swagger.Parameter2[], paramValue: string, paramType: string, paramIn: string) { const parameter = verifyParameter(params, paramValue, paramIn); expect(parameter.schema.$ref).to.equal(paramType); } -function verifyParameter(params: Swagger.Parameter[], paramValue: string, paramIn: string) { +function verifyParameter(params: Swagger.Parameter2[], paramValue: string, paramIn: string) { const parameter = params.filter(p => p.name === paramValue)[0]; expect(parameter, `Path parameter '${paramValue}' wasn't generated.`).to.exist; expect(parameter.in).to.equal(paramIn);