import * as ts from 'typescript' ; import { getDecoratorName , getDecoratorOptions , getDecoratorTextValue } from '../utils/decoratorUtils' ; import { MetadataGenerator , Parameter , Type } from './metadataGenerator' ; import { getCommonPrimitiveAndArrayUnionType , getLiteralValue , resolveType } from './resolveType' ; export class ParameterGenerator { constructor ( private readonly parameter , private readonly method , private readonly path , private readonly genericTypeMap ? ) { } public generate ( ) { const decoratorName = getDecoratorName ( this . parameter , identifier => this . supportParameterDecorator ( identifier . text ) ) ; switch ( decoratorName ) { case 'Param' : return this . getRequestParameter ( this . parameter ) ; case 'CookieParam' : return this . getCookieParameter ( this . parameter ) ; case 'FormParam' : return this . getFormParameter ( this . parameter ) ; case 'HeaderParam' : return this . getHeaderParameter ( this . parameter ) ; case 'QueryParam' : return this . getQueryParameter ( this . parameter ) ; case 'PathParam' : return this . getPathParameter ( this . parameter ) ; case 'FileParam' : return this . getFileParameter ( this . parameter ) ; case 'FilesParam' : return this . getFilesParameter ( this . parameter ) ; case 'Context' : case 'ContextRequest' : case 'ContextResponse' : case 'ContextNext' : case 'ContextLanguage' : case 'ContextAccept' : return this . getContextParameter ( this . parameter ) ; default : return this . getBodyParameter ( this . parameter ) ; } } private getCurrentLocation ( ) { const methodId = ( this . parameter . parent as ts . MethodDeclaration ) . name as ts . Identifier ; const controllerId = ( ( this . parameter . parent as ts . MethodDeclaration ) . parent as ts . ClassDeclaration ) . name as ts . Identifier ; return `${ controllerId . text }.${ methodId . text }` ; } private getRequestParameter ( parameter ) { const parameterName = ( parameter . name as ts . Identifier ) . text ; const type = this . getValidatedType ( parameter ) ; if ( ! this . supportsBodyParameters ( this . method ) ) { throw new Error ( `Param can't support '${ this . getCurrentLocation ( ) }' method.` ) ; } return { description : this . getParameterDescription ( parameter ) , in : 'param' , name : getDecoratorTextValue ( this . parameter , ident => ident . text === 'Param' ) || parameterName , parameterName : parameterName , required : ! parameter . questionToken , type : type } ; } private getContextParameter ( parameter ) { const parameterName = ( parameter . name as ts . Identifier ) . text ; return { description : this . getParameterDescription ( parameter ) , in : 'context' , name : parameterName , parameterName : parameterName , required : ! parameter . questionToken , type : { typeName : '' } } ; } private getFileParameter ( parameter ) { const parameterName = ( parameter . name as ts . Identifier ) . text ; if ( ! this . supportsBodyParameters ( this . method ) ) { throw new Error ( `FileParam can't support '${ this . getCurrentLocation ( ) }' method.` ) ; } return { description : this . getParameterDescription ( parameter ) , in : 'formData' , name : getDecoratorTextValue ( this . parameter , ident => ident . text === 'FileParam' ) || parameterName , parameterName : parameterName , required : ! parameter . questionToken , type : { typeName : 'file' } } ; } private getFilesParameter ( parameter ) { const parameterName = ( parameter . name as ts . Identifier ) . text ; if ( ! this . supportsBodyParameters ( this . method ) ) { throw new Error ( `FilesParam can't support '${ this . getCurrentLocation ( ) }' method.` ) ; } return { description : this . getParameterDescription ( parameter ) , in : 'formData' , name : getDecoratorTextValue ( this . parameter , ident => ident . text === 'FilesParam' ) || parameterName , parameterName : parameterName , required : ! parameter . questionToken , type : { typeName : 'file' } } ; } private getFormParameter ( parameter ) { const parameterName = ( parameter . name as ts . Identifier ) . text ; const type = this . getValidatedType ( parameter ) ; if ( ! this . supportsBodyParameters ( this . method ) ) { throw new Error ( `Form can't support '${ this . getCurrentLocation ( ) }' method.` ) ; } return { description : this . getParameterDescription ( parameter ) , in : 'formData' , name : getDecoratorTextValue ( this . parameter , ident => ident . text === 'FormParam' ) || parameterName , parameterName : parameterName , required : ! parameter . questionToken && ! parameter . initializer , type : type } ; } private getCookieParameter ( parameter ) { const parameterName = ( parameter . name as ts . Identifier ) . text ; return { description : this . getParameterDescription ( parameter ) , in : 'cookie' , name : getDecoratorTextValue ( this . parameter , ident => ident . text === 'CookieParam' ) || parameterName , parameterName : parameterName , required : ! parameter . questionToken && ! parameter . initializer , type : { typeName : '' } } ; } private getBodyParameter ( parameter ) { const parameterName = ( parameter . name as ts . Identifier ) . text ; const type = this . getValidatedType ( parameter ) ; if ( ! this . supportsBodyParameters ( this . method ) ) { throw new Error ( `Body can't support ${ this . method } method` ) ; } return { description : this . getParameterDescription ( parameter ) , in : 'body' , name : parameterName , parameterName : parameterName , required : ! parameter . questionToken && ! parameter . initializer , type : type } ; } private getHeaderParameter ( parameter ) { const parameterName = ( parameter . name as ts . Identifier ) . text ; const type = this . getValidatedType ( parameter ) ; if ( ! this . supportPathDataType ( type ) ) { throw new InvalidParameterException ( `Parameter '${ parameterName }' can't be passed as a header parameter in '${ this . getCurrentLocation ( ) }'.` ) ; } return { description : this . getParameterDescription ( parameter ) , in : 'header' , name : getDecoratorTextValue ( this . parameter , ident => ident . text === 'HeaderParam' ) || parameterName , parameterName : parameterName , required : ! parameter . questionToken && ! parameter . initializer , type : type } ; } private getQueryParameter ( parameter ) { const parameterName = ( parameter . name as ts . Identifier ) . text ; const parameterOptions = getDecoratorOptions ( this . parameter , ident => ident . text === 'QueryParam' ) || { } ; let type = this . getValidatedType ( parameter ) ; if ( ! this . supportQueryDataType ( type ) ) { const arrayType = getCommonPrimitiveAndArrayUnionType ( parameter . type ) ; if ( arrayType && this . supportQueryDataType ( arrayType ) ) { type = arrayType ; } else { throw new InvalidParameterException ( `Parameter '${ parameterName }' can't be passed as a query parameter in '${ this . getCurrentLocation ( ) }'.` ) ; } } return { collectionFormat : parameterOptions . collectionFormat , default : this . getDefaultValue ( parameter . initializer ) , description : this . getParameterDescription ( parameter ) , in : 'query' , name : getDecoratorTextValue ( this . parameter , ident => ident . text === 'QueryParam' ) || parameterName , parameterName : parameterName , required : ! parameter . questionToken && ! parameter . initializer , type : type } ; } private getPathParameter ( parameter ) { const parameterName = ( parameter . name as ts . Identifier ) . text ; const type = this . getValidatedType ( parameter ) ; const pathName = getDecoratorTextValue ( this . parameter , ident => ident . text === 'PathParam' ) || parameterName ; if ( ! this . supportPathDataType ( type ) ) { throw new InvalidParameterException ( `Parameter '${ parameterName }:${ type }' can't be passed as a path parameter in '${ this . getCurrentLocation ( ) }'.` ) ; } if ( ( ! this . path . includes ( `{${ pathName }}` ) ) && ( ! this . path . includes ( `:${ pathName }` ) ) ) { throw new Error ( `Parameter '${ parameterName }' can't match in path: '${ this . path }'` ) ; } return { description : this . getParameterDescription ( parameter ) , in : 'path' , name : pathName , parameterName : parameterName , required : true , type : type } ; } private getParameterDescription ( node ) { const symbol = MetadataGenerator . current . typeChecker . getSymbolAtLocation ( node . name ) ; if ( symbol ) { const comments = symbol . getDocumentationComment ( MetadataGenerator . current . typeChecker ) ; if ( comments . length ) { return ts . displayPartsToString ( comments ) ; } } return '' ; } private supportsBodyParameters ( method ) { return [ 'delete' , 'post' , 'put' , 'patch' ] . some ( m => m === method ) ; } private supportParameterDecorator ( decoratorName ) { return [ 'HeaderParam' , 'QueryParam' , 'Param' , 'FileParam' , 'PathParam' , 'FilesParam' , 'FormParam' , 'CookieParam' , 'Context' , 'ContextRequest' , 'ContextResponse' , 'ContextNext' , 'ContextLanguage' , 'ContextAccept' ] . some ( d => d === decoratorName ) ; } private supportPathDataType ( parameterType ) { return [ 'string' , 'integer' , 'long' , 'float' , 'double' , 'date' , 'datetime' , 'buffer' , 'boolean' , 'enum' ] . find ( t => t === parameterType . typeName ) ; } private supportQueryDataType ( parameterType ) { return [ 'string' , 'integer' , 'long' , 'float' , 'double' , 'date' , 'datetime' , 'buffer' , 'boolean' , 'enum' , 'array' ] . find ( t => t === parameterType . typeName ) ; } private getValidatedType ( parameter ) { if ( ! parameter . type ) { throw new Error ( `Parameter ${ parameter . name } doesn't have a valid type assigned in '${ this . getCurrentLocation ( ) }'.` ) ; } return resolveType ( parameter . type , this . genericTypeMap ) ; } private getDefaultValue ( initializer ? ) { if ( ! initializer ) { return ; } return getLiteralValue ( initializer ) ; } } class InvalidParameterException extends Error { }