import * as pathUtil from 'path' ; import * as ts from 'typescript' ; import { getDecorators } from '../utils/decoratorUtils' ; import { getJSDocDescription , getJSDocTag , isExistJSDocTag } from '../utils/jsDocUtils' ; import { normalizePath } from '../utils/pathUtils' ; import { EndpointGenerator } from './endpointGenerator' ; import { Method , ResponseData , ResponseType , Type } from './metadataGenerator' ; import { ParameterGenerator } from './parameterGenerator' ; import { resolveType } from './resolveType' ; export class MethodGenerator extends EndpointGenerator < ts . MethodDeclaration > { private method : string ; private path : string ; constructor ( node , private readonly controllerPath , private readonly genericTypeMap ? ) { super ( node , 'methods' ) ; this . processMethodDecorators ( ) ; } public isValid ( ) { return ! ! this . method ; } public getMethodName ( ) { const identifier = this . node . name as ts . Identifier ; return identifier . text ; } public generate ( ) { if ( ! this . isValid ( ) ) { throw new Error ( 'This isn\'t a valid controller method.' ) ; } this . debugger ( 'Generating Metadata for method %s' , this . getCurrentLocation ( ) ) ; const identifier = this . node . name as ts . Identifier ; const type = resolveType ( this . node . type , this . genericTypeMap ) ; const responses = this . mergeResponses ( this . getResponses ( this . genericTypeMap ) , this . getMethodSuccessResponse ( type ) ) ; const methodMetadata = { consumes : this . getDecoratorValues ( 'Consumes' ) , deprecated : isExistJSDocTag ( this . node , 'deprecated' ) , description : getJSDocDescription ( this . node ) , method : this . method , name : identifier . text , parameters : this . buildParameters ( ) , path : this . path , produces : ( this . getDecoratorValues ( 'Produces' ) ? this . getDecoratorValues ( 'Produces' ) : this . getDecoratorValues ( 'Accept' ) ) , responses : responses , security : this . getSecurity ( ) , summary : getJSDocTag ( this . node , 'summary' ) , tags : this . getDecoratorValues ( 'Tags' ) , type : type } ; this . debugger ( 'Generated Metadata for method %s: %j' , this . getCurrentLocation ( ) , methodMetadata ) ; return methodMetadata ; } protected getCurrentLocation ( ) { const methodId = this . node . name as ts . Identifier ; const controllerId = ( this . node . parent as ts . ClassDeclaration ) . name as ts . Identifier ; return `${ controllerId . text }.${ methodId . text }` ; } private buildParameters ( ) { this . debugger ( 'Processing method %s parameters.' , this . getCurrentLocation ( ) ) ; const parameters = this . node . parameters . map ( p => { try { const path = pathUtil . posix . join ( '/' , ( this . controllerPath ? this . controllerPath : '' ) , this . path ) ; return new ParameterGenerator ( p , this . method , path , this . genericTypeMap ) . generate ( ) ; } catch ( e ) { const methodId = this . node . name as ts . Identifier ; const controllerId = ( this . node . parent as ts . ClassDeclaration ) . name as ts . Identifier ; const parameterId = p . name as ts . Identifier ; throw new Error ( `Error generate parameter method: '${ controllerId . text }.${ methodId . text }' argument: ${ parameterId . text } ${ e }` ) ; } } ) . filter ( p => ( p . in !== 'context' ) && ( p . in !== 'cookie' ) ) ; const bodyParameters = parameters . filter ( p => p . in === 'body' ) ; const formParameters = parameters . filter ( p => p . in === 'formData' ) ; if ( bodyParameters . length > 1 ) { throw new Error ( `Only one body parameter allowed in '${ this . getCurrentLocation ( ) }' method.` ) ; } if ( bodyParameters . length > 0 && formParameters . length > 0 ) { throw new Error ( `Choose either during @FormParam and @FileParam or body parameter  in '${ this . getCurrentLocation ( ) }' method.` ) ; } this . debugger ( 'Parameters list for method %s: %j.' , this . getCurrentLocation ( ) , parameters ) ; return parameters ; } private processMethodDecorators ( ) { const httpMethodDecorators = getDecorators ( this . node , decorator => this . supportsPathMethod ( decorator . text ) ) ; if ( ! httpMethodDecorators || ! httpMethodDecorators . length ) { return ; } if ( httpMethodDecorators . length > 1 ) { throw new Error ( `Only one HTTP Method decorator in '${ this . getCurrentLocation }' method is acceptable, Found: ${ httpMethodDecorators . map ( d => d . text ) . join ( ', ' ) }` ) ; } const methodDecorator = httpMethodDecorators [ 0 ] ; this . method = methodDecorator . text . toLowerCase ( ) ; this . debugger ( 'Processing method %s decorators.' , this . getCurrentLocation ( ) ) ; const pathDecorators = getDecorators ( this . node , decorator => decorator . text === 'Path' ) ; if ( pathDecorators && pathDecorators . length > 1 ) { throw new Error ( `Only one Path decorator in '${ this . getCurrentLocation }' method is acceptable, Found: ${ httpMethodDecorators . map ( d => d . text ) . join ( ', ' ) }` ) ; } if ( pathDecorators ) { const pathDecorator = pathDecorators [ 0 ] ; this . path = pathDecorator ? `/${ normalizePath ( pathDecorator . arguments [ 0 ] ) }` : '' ; } else { this . path = '' ; } this . debugger ( 'Mapping endpoint %s %s' , this . method , this . path ) ; } private getMethodSuccessResponse ( type ) { const responseData = this . getMethodSuccessResponseData ( type ) ; return { description : type . typeName === 'void' ? 'No content' : 'Ok' , examples : this . getMethodSuccessExamples ( ) , schema : responseData . type , status : responseData . status } ; } private getMethodSuccessResponseData ( type ) { switch ( type . typeName ) { case 'void' : return { status : '204' , type : type } ; case 'NewResource' : return { status : '201' , type : type . typeArgument || type } ; case 'RequestAccepted' : return { status : '202' , type : type . typeArgument || type } ; case 'MovedPermanently' : return { status : '301' , type : type . typeArgument || type } ; case 'MovedTemporarily' : return { status : '302' , type : type . typeArgument || type } ; case 'DownloadResource' : case 'DownloadBinaryData' : return { status : '200' , type : { typeName : 'buffer' } } ; default : return { status : '200' , type : type } ; } } private getMethodSuccessExamples ( ) { const exampleDecorators = getDecorators ( this . node , decorator => decorator . text === 'Example' ) ; if ( ! exampleDecorators || ! exampleDecorators . length ) { return undefined ; } if ( exampleDecorators . length > 1 ) { throw new Error ( `Only one Example decorator allowed in '${ this . getCurrentLocation }' method.` ) ; } const d = exampleDecorators [ 0 ] ; const argument = d . arguments [ 0 ] ; return this . getExamplesValue ( argument ) ; } private mergeResponses ( responses , defaultResponse ) { if ( ! responses || ! responses . length ) { return [ defaultResponse ] ; } const index = responses . findIndex ( ( resp ) => resp . status === defaultResponse . status ) ; if ( index >= 0 ) { if ( defaultResponse . examples && ! responses [ index ] . examples ) { responses [ index ] . examples = defaultResponse . examples ; } } else { responses . push ( defaultResponse ) ; } return responses ; } private supportsPathMethod ( method ) { return [ 'GET' , 'POST' , 'PATCH' , 'DELETE' , 'PUT' , 'OPTIONS' , 'HEAD' ] . some ( m => m === method ) ; } }