@@ -3,7 +3,13 @@ import type { InstallerOptions } from '../language';
33import type Oas from 'oas' ;
44import type { Operation } from 'oas' ;
55import type { HttpMethods , SchemaObject } from 'oas/dist/rmoas.types' ;
6- import type { ClassDeclaration , JSDocStructure , OptionalKind , ParameterDeclarationStructure } from 'ts-morph' ;
6+ import type {
7+ ClassDeclaration ,
8+ JSDocStructure ,
9+ JSDocTagStructure ,
10+ OptionalKind ,
11+ ParameterDeclarationStructure ,
12+ } from 'ts-morph' ;
713import type { PackageJson } from 'type-fest' ;
814
915import fs from 'fs' ;
@@ -28,7 +34,13 @@ interface OperationTypeHousing {
2834 operation : Operation ;
2935 types : {
3036 params ?: false | Record < 'body' | 'formData' | 'metadata' , string > ;
31- responses ?: Record < string , string > ;
37+ responses ?: Record <
38+ string | number ,
39+ {
40+ description ?: string ;
41+ type : string ;
42+ }
43+ > ;
3244 } ;
3345}
3446
@@ -502,6 +514,20 @@ sdk.server('https://eu.api.example.com/v14');`)
502514 return sourceFile ;
503515 }
504516
517+ /**
518+ * Add a new JSDoc `@tag` to an existing docblock.
519+ *
520+ */
521+ static addTagToDocblock ( docblock : OptionalKind < JSDocStructure > , tag : OptionalKind < JSDocTagStructure > ) {
522+ const tags = docblock . tags ?? [ ] ;
523+ tags . push ( tag ) ;
524+
525+ return {
526+ ...docblock ,
527+ tags,
528+ } ;
529+ }
530+
505531 /**
506532 * Create operation accessors on the SDK.
507533 *
@@ -512,7 +538,7 @@ sdk.server('https://eu.api.example.com/v14');`)
512538 paramTypes ?: OperationTypeHousing [ 'types' ] [ 'params' ] ,
513539 responseTypes ?: OperationTypeHousing [ 'types' ] [ 'responses' ]
514540 ) {
515- const docblock : OptionalKind < JSDocStructure > = { } ;
541+ let docblock : OptionalKind < JSDocStructure > = { } ;
516542 const summary = operation . getSummary ( ) ;
517543 const description = operation . getDescription ( ) ;
518544 if ( summary || description ) {
@@ -531,7 +557,10 @@ sdk.server('https://eu.api.example.com/v14');`)
531557 } ;
532558
533559 if ( summary && description ) {
534- docblock . tags = [ { tagName : 'summary' , text : docblockEscape ( wordWrap ( summary ) ) } ] ;
560+ docblock = TSGenerator . addTagToDocblock ( docblock , {
561+ tagName : 'summary' ,
562+ text : docblockEscape ( wordWrap ( summary ) ) ,
563+ } ) ;
535564 }
536565 }
537566
@@ -568,8 +597,8 @@ sdk.server('https://eu.api.example.com/v14');`)
568597
569598 let returnType = 'Promise<FetchResponse<number, unknown>>' ;
570599 if ( responseTypes ) {
571- returnType = `Promise< ${ Object . entries ( responseTypes )
572- . map ( ( [ status , responseType ] ) => {
600+ const returnTypes = Object . entries ( responseTypes )
601+ . map ( ( [ status , { description : responseDescription , type : responseType } ] ) => {
573602 if ( status . toLowerCase ( ) === 'default' ) {
574603 return `FetchResponse<number, ${ responseType } >` ;
575604 } else if ( status . length === 3 && status . toUpperCase ( ) . endsWith ( 'XX' ) ) {
@@ -580,19 +609,54 @@ sdk.server('https://eu.api.example.com/v14');`)
580609 return `FetchResponse<number, ${ responseType } >` ;
581610 }
582611
612+ if ( Number ( statusPrefix ) >= 4 ) {
613+ docblock = TSGenerator . addTagToDocblock ( docblock , {
614+ tagName : 'throws' ,
615+ text : `FetchError<${ status } , ${ responseType } >${
616+ responseDescription ? docblockEscape ( wordWrap ( ` ${ responseDescription } ` ) ) : ''
617+ } `,
618+ } ) ;
619+
620+ return false ;
621+ }
622+
583623 this . usesHTTPMethodRangeInterface = true ;
584624 return `FetchResponse<HTTPMethodRange<${ statusPrefix } 00, ${ statusPrefix } 99>, ${ responseType } >` ;
585625 }
586626
627+ // 400 and 500 status code families are thrown as exceptions so adding them as a possible
628+ // return type isn't valid.
629+ if ( Number ( status ) >= 400 ) {
630+ docblock = TSGenerator . addTagToDocblock ( docblock , {
631+ tagName : 'throws' ,
632+ text : `FetchError<${ status } , ${ responseType } >${
633+ responseDescription ? docblockEscape ( wordWrap ( ` ${ responseDescription } ` ) ) : ''
634+ } `,
635+ } ) ;
636+
637+ return false ;
638+ }
639+
587640 return `FetchResponse<${ status } , ${ responseType } >` ;
588641 } )
589- . join ( ' | ' ) } >`;
642+ . filter ( Boolean )
643+ . join ( ' | ' ) ;
644+
645+ // If all of our documented responses are for error status codes then all we can document for
646+ // anything else that might happen is `unknown`.
647+ returnType = `Promise<${ returnTypes . length ? returnTypes : 'FetchResponse<number, unknown>' } >` ;
590648 }
591649
650+ const shouldAddAltTypedOverloads = Object . keys ( parameters ) . length === 2 && hasOptionalBody && ! hasOptionalMetadata ;
592651 const operationIdAccessor = this . sdk . addMethod ( {
593652 name : operationId ,
594653 returnType,
595- docs : Object . keys ( docblock ) . length ? [ docblock ] : null ,
654+
655+ // If we're going to be creating typed method overloads for optional body an metadata handling
656+ // we should only add a docblock to the first overload we create because IDE Intellisense will
657+ // always use that and adding a docblock to all three will bloat the SDK with unused and
658+ // unsurfaced method documentation.
659+ docs : shouldAddAltTypedOverloads ? null : Object . keys ( docblock ) . length ? [ docblock ] : null ,
596660 statements : writer => {
597661 /**
598662 * @example return this.core.fetch('/pet/findByStatus', 'get', body, metadata);
@@ -626,10 +690,6 @@ sdk.server('https://eu.api.example.com/v14');`)
626690 // If we have both body and metadata parameters but only body is optional we need to create
627691 // a couple function overloads as Typescript doesn't let us have an optional method parameter
628692 // come before one that's required.
629- //
630- // None of these accessor overloads will receive a docblock because the original will have
631- // that covered.
632- const shouldAddAltTypedOverloads = Object . keys ( parameters ) . length === 2 && hasOptionalBody && ! hasOptionalMetadata ;
633693 if ( shouldAddAltTypedOverloads ) {
634694 // Create an overload that has both `body` and `metadata` parameters as required.
635695 operationIdAccessor . addOverload ( {
@@ -645,7 +705,6 @@ sdk.server('https://eu.api.example.com/v14');`)
645705 operationIdAccessor . addOverload ( {
646706 parameters : [ { ...parameters . metadata } ] ,
647707 returnType,
648- docs : Object . keys ( docblock ) . length ? [ docblock ] : null ,
649708 } ) ;
650709
651710 // Create an overload that has both `body` and `metadata` parameters as optional. Even though
@@ -807,7 +866,7 @@ sdk.server('https://eu.api.example.com/v14');`)
807866 . reduce ( ( prev , next ) => Object . assign ( prev , next ) ) ;
808867
809868 const res = Object . entries ( schemas )
810- . map ( ( [ status , { schema } ] ) => {
869+ . map ( ( [ status , { description , schema } ] ) => {
811870 let typeName ;
812871
813872 if ( typeof schema === 'string' && schema . startsWith ( '::convert::' ) ) {
@@ -826,7 +885,10 @@ sdk.server('https://eu.api.example.com/v14');`)
826885 return {
827886 // Types are prefixed with `types.` because that's how we're importing them from
828887 // `types.d.ts`.
829- [ status ] : `types.${ typeName } ` ,
888+ [ status ] : {
889+ type : `types.${ typeName } ` ,
890+ description,
891+ } ,
830892 } ;
831893 } )
832894 . reduce ( ( prev , next ) => Object . assign ( prev , next ) , { } ) ;
0 commit comments