@@ -2,8 +2,10 @@ import * as prettier from "prettier";
2
2
import {
3
3
EmitContext ,
4
4
getNamespaceFullName ,
5
+ Model ,
5
6
ModelProperty ,
6
7
Operation ,
8
+ Type ,
7
9
} from "@typespec/compiler" ;
8
10
import {
9
11
getAllHttpServices ,
@@ -12,6 +14,7 @@ import {
12
14
isBodyRoot ,
13
15
isPathParam ,
14
16
isQueryParam ,
17
+ isStatusCode ,
15
18
listHttpOperationsIn ,
16
19
} from "@typespec/http" ;
17
20
import {
@@ -57,6 +60,12 @@ export class FetchClientEmitter extends TypescriptEmitter<EmitterOptions> {
57
60
) ;
58
61
}
59
62
63
+ operationHasAnyRequiredParams ( operation : Operation ) : boolean {
64
+ return Array . from ( operation . parameters . properties . values ( ) ) . some (
65
+ ( prop ) => ! prop . optional
66
+ ) ;
67
+ }
68
+
60
69
operationDeclaration (
61
70
operation : Operation ,
62
71
name : string
@@ -120,7 +129,7 @@ export class FetchClientEmitter extends TypescriptEmitter<EmitterOptions> {
120
129
}
121
130
122
131
cb . push (
123
- code `export type ${ name } ResponseBody = ${ this . emitter . emitTypeReference ( operation . returnType ) } ;`
132
+ code `export type ${ name } ResponseBody = ${ this . emitter . emitOperationReturnType ( operation ) } ;`
124
133
) ;
125
134
126
135
const argsTypeParts = [
@@ -137,6 +146,67 @@ export class FetchClientEmitter extends TypescriptEmitter<EmitterOptions> {
137
146
return this . emitter . result . declaration ( name , cb . reduce ( ) ) ;
138
147
}
139
148
149
+ operationReturnType (
150
+ operation : Operation ,
151
+ returnType : Type
152
+ ) : EmitterOutput < string > {
153
+ const program = this . emitter . getProgram ( ) ;
154
+ if ( returnType . kind === "Model" ) {
155
+ const builder = new StringBuilder ( ) ;
156
+
157
+ builder . push ( code `{data: ${ this . emitter . emitTypeReference ( returnType ) } ;` ) ;
158
+ const statusCodeProp = Array . from ( returnType . properties . values ( ) ) . find (
159
+ ( prop ) => isStatusCode ( program , prop )
160
+ ) ;
161
+ if ( statusCodeProp ) {
162
+ const propVal = this . emitter . emitModelProperty ( statusCodeProp ) ;
163
+ builder . push ( code `${ propVal } ;` ) ;
164
+ } else {
165
+ // If no status code property is found, assume 200
166
+ builder . push ( code `statusCode: 200;` ) ;
167
+ }
168
+ builder . push ( code `}` ) ;
169
+ return this . emitter . result . rawCode ( builder . reduce ( ) ) ;
170
+ } else if ( returnType . kind === "Union" ) {
171
+ const builder = new StringBuilder ( ) ;
172
+ for ( const { type } of returnType . variants . values ( ) ) {
173
+ if ( type . kind === "Model" ) {
174
+ builder . push ( code `| {data: ${ this . emitter . emitTypeReference ( type ) } ;` ) ;
175
+ const statusCodeProp = Array . from ( type . properties . values ( ) ) . find (
176
+ ( prop ) => isStatusCode ( program , prop )
177
+ ) ;
178
+ if ( statusCodeProp ) {
179
+ const propVal = this . emitter . emitModelProperty ( statusCodeProp ) ;
180
+ builder . push ( code `${ propVal } ;` ) ;
181
+ } else {
182
+ // If no status code property is found, assume 200
183
+ builder . push ( code `statusCode: 200;` ) ;
184
+ }
185
+ builder . push ( code `}` ) ;
186
+ }
187
+ }
188
+ return this . emitter . result . rawCode ( builder . reduce ( ) ) ;
189
+ }
190
+ return this . emitter . emitTypeReference ( returnType ) ;
191
+ }
192
+
193
+ modelProperties ( model : Model ) : EmitterOutput < string > {
194
+ const program = this . emitter . getProgram ( ) ;
195
+ const builder = new StringBuilder ( ) ;
196
+
197
+ for ( const prop of model . properties . values ( ) ) {
198
+ if ( isStatusCode ( program , prop ) ) {
199
+ // Remove status code from model properties
200
+ // This will be added to the response object
201
+ continue ;
202
+ }
203
+ const propVal = this . emitter . emitModelProperty ( prop ) ;
204
+ builder . push ( code `${ propVal } ;` ) ;
205
+ }
206
+
207
+ return this . emitter . result . rawCode ( builder . reduce ( ) ) ;
208
+ }
209
+
140
210
async sourceFile ( sourceFile : SourceFile < string > ) : Promise < EmittedSourceFile > {
141
211
const program = this . emitter . getProgram ( ) ;
142
212
const [ httpServices ] = getAllHttpServices ( program , { } ) ;
@@ -168,13 +238,23 @@ export class FetchClientEmitter extends TypescriptEmitter<EmitterOptions> {
168
238
new Map ( ) ;
169
239
170
240
for ( const httpService of httpServices ) {
171
- const [ operations ] = listHttpOperationsIn ( program , httpService . namespace ) ;
241
+ const [ httpOperations ] = listHttpOperationsIn (
242
+ program ,
243
+ httpService . namespace
244
+ ) ;
245
+
246
+ for ( const httpOperation of httpOperations ) {
247
+ const { operation, path, verb } = httpOperation ;
248
+
249
+ const operationArgsRequired =
250
+ this . operationHasAnyRequiredParams ( operation ) ;
251
+ const operationName = operation . name ;
252
+ const handlerType = `${ operationName } Client` ;
172
253
173
- for ( const operation of operations ) {
174
254
const namespace =
175
- operation . operation . namespace ?. name ?? httpService . namespace . name ;
176
- const namespaceChain = operation . operation . namespace
177
- ? getNamespaceFullName ( operation . operation . namespace ) . split ( "." )
255
+ operation . namespace ?. name ?? httpService . namespace . name ;
256
+ const namespaceChain = operation . namespace
257
+ ? getNamespaceFullName ( operation . namespace ) . split ( "." )
178
258
: [ ] ;
179
259
const declarations = declarationsByNamespace . get ( namespace ) ?? {
180
260
typedClientCallbackTypes : [ ] ,
@@ -183,20 +263,22 @@ export class FetchClientEmitter extends TypescriptEmitter<EmitterOptions> {
183
263
namespaceChain,
184
264
} ;
185
265
186
- const handlerType = `${ operation . operation . name } Client` ;
187
- const operationName = operation . operation . name ;
188
266
declarations . operationNames . push ( operationName ) ;
189
267
declarations . typedClientCallbackTypes . push (
190
- `${ operationName } : (args: ${ handlerType } Args, options?: RequestInit) => Promise<${ operationName } ResponseBody>;`
268
+ `${ operationName } : (args${ operationArgsRequired ? "" : "?" } : ${ handlerType } Args, options?: RequestInit) => Promise<${ operationName } ResponseBody>;`
191
269
) ;
192
270
declarations . routeHandlerFunctions . push (
193
271
`const ${ operationName } : ${ namespaceChain . join ( "." ) } .Client["${ operationName } "] = async (args, options) => {
194
- const queryString = ${ this . operationHasQuery ( operation . operation ) ? `queryParamsToString(args.query)` : '""' } ;
195
- const path = \`\${baseUrl}${ operation . path . replace ( / \{ ( \w + ) \} / , "${args.$1}" ) } \${queryString}\`;
272
+ ${
273
+ operationArgsRequired
274
+ ? `const queryString = ${ this . operationHasQuery ( operation ) ? `queryParamsToString(args.query)` : '""' } ;`
275
+ : `const queryString = ${ this . operationHasQuery ( operation ) ? `args ? queryParamsToString(args?.query) : ''` : '""' } ;`
276
+ }
277
+ const path = \`\${baseUrl}${ path . replace ( / \{ ( \w + ) \} / , "${args.$1}" ) } \${queryString}\`;
196
278
const opts: RequestInit = {
197
- method: '${ operation . verb . toUpperCase ( ) } ',
279
+ method: '${ verb . toUpperCase ( ) } ',
198
280
${
199
- this . operationHasBody ( operation . operation )
281
+ this . operationHasBody ( operation )
200
282
? `body: JSON.stringify(args.body),`
201
283
: ""
202
284
}
@@ -208,10 +290,10 @@ export class FetchClientEmitter extends TypescriptEmitter<EmitterOptions> {
208
290
};
209
291
210
292
const res = await fetch(path, opts);
211
- if (!res.ok) {
212
- throw new Error(\`Request failed with status \${ res.status}\` );
213
- }
214
- return res.json() ;
293
+
294
+ const data = await res.json( );
295
+ const statusCode = res.status;
296
+ return {data, statusCode} as ${ namespaceChain . join ( "." ) } . ${ operationName } ResponseBody ;
215
297
};`
216
298
) ;
217
299
0 commit comments