Skip to content

Commit db56bb3

Browse files
committed
minor(emitter-fetch-client): wrap response data and set statusCode
1 parent 05a17fd commit db56bb3

File tree

7 files changed

+342
-41
lines changed

7 files changed

+342
-41
lines changed

.changeset/nasty-camels-check.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@typespec-tools/emitter-fetch-client": minor
3+
---
4+
5+
wrap response data and set statusCode

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/emitter-fetch-client/src/emitter.ts

Lines changed: 99 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import * as prettier from "prettier";
22
import {
33
EmitContext,
44
getNamespaceFullName,
5+
Model,
56
ModelProperty,
67
Operation,
8+
Type,
79
} from "@typespec/compiler";
810
import {
911
getAllHttpServices,
@@ -12,6 +14,7 @@ import {
1214
isBodyRoot,
1315
isPathParam,
1416
isQueryParam,
17+
isStatusCode,
1518
listHttpOperationsIn,
1619
} from "@typespec/http";
1720
import {
@@ -57,6 +60,12 @@ export class FetchClientEmitter extends TypescriptEmitter<EmitterOptions> {
5760
);
5861
}
5962

63+
operationHasAnyRequiredParams(operation: Operation): boolean {
64+
return Array.from(operation.parameters.properties.values()).some(
65+
(prop) => !prop.optional
66+
);
67+
}
68+
6069
operationDeclaration(
6170
operation: Operation,
6271
name: string
@@ -120,7 +129,7 @@ export class FetchClientEmitter extends TypescriptEmitter<EmitterOptions> {
120129
}
121130

122131
cb.push(
123-
code`export type ${name}ResponseBody = ${this.emitter.emitTypeReference(operation.returnType)};`
132+
code`export type ${name}ResponseBody = ${this.emitter.emitOperationReturnType(operation)};`
124133
);
125134

126135
const argsTypeParts = [
@@ -137,6 +146,67 @@ export class FetchClientEmitter extends TypescriptEmitter<EmitterOptions> {
137146
return this.emitter.result.declaration(name, cb.reduce());
138147
}
139148

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+
140210
async sourceFile(sourceFile: SourceFile<string>): Promise<EmittedSourceFile> {
141211
const program = this.emitter.getProgram();
142212
const [httpServices] = getAllHttpServices(program, {});
@@ -168,13 +238,23 @@ export class FetchClientEmitter extends TypescriptEmitter<EmitterOptions> {
168238
new Map();
169239

170240
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`;
172253

173-
for (const operation of operations) {
174254
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(".")
178258
: [];
179259
const declarations = declarationsByNamespace.get(namespace) ?? {
180260
typedClientCallbackTypes: [],
@@ -183,20 +263,22 @@ export class FetchClientEmitter extends TypescriptEmitter<EmitterOptions> {
183263
namespaceChain,
184264
};
185265

186-
const handlerType = `${operation.operation.name}Client`;
187-
const operationName = operation.operation.name;
188266
declarations.operationNames.push(operationName);
189267
declarations.typedClientCallbackTypes.push(
190-
`${operationName}: (args: ${handlerType}Args, options?: RequestInit) => Promise<${operationName}ResponseBody>;`
268+
`${operationName}: (args${operationArgsRequired ? "" : "?"}: ${handlerType}Args, options?: RequestInit) => Promise<${operationName}ResponseBody>;`
191269
);
192270
declarations.routeHandlerFunctions.push(
193271
`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}\`;
196278
const opts: RequestInit = {
197-
method: '${operation.verb.toUpperCase()}',
279+
method: '${verb.toUpperCase()}',
198280
${
199-
this.operationHasBody(operation.operation)
281+
this.operationHasBody(operation)
200282
? `body: JSON.stringify(args.body),`
201283
: ""
202284
}
@@ -208,10 +290,10 @@ export class FetchClientEmitter extends TypescriptEmitter<EmitterOptions> {
208290
};
209291
210292
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;
215297
};`
216298
);
217299

packages/integration-tests/main.tsp

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,45 +31,45 @@ enum petType {
3131
reptile: "reptile",
3232
}
3333

34+
model SinglePetResponse {
35+
...OkResponse;
36+
@body pet: Pet;
37+
}
38+
39+
model MultiplePetResponse {
40+
...OkResponse;
41+
@body pets: Pet[];
42+
}
43+
44+
model CommonNotFoundResponse {
45+
...NotFoundResponse;
46+
@body error: NotFoundError;
47+
}
48+
3449
@route("/pets")
3550
namespace Pets {
3651
@get
37-
op listPets(@query type?: petType): {
38-
@body pets: Pet[];
39-
};
52+
op listPets(@query type?: petType): MultiplePetResponse;
4053

4154
@get
42-
op getPet(@path petId: int32): {
43-
@body pet: Pet;
44-
} | {
45-
@body error: NotFoundError;
46-
};
55+
op getPet(@path petId: int32): SinglePetResponse | CommonNotFoundResponse;
4756

4857
@post
49-
op createPet(@body pet: Pet): {
50-
@body pet: Pet;
51-
};
58+
op createPet(@body pet: Pet): SinglePetResponse;
5259

5360
@put
54-
op updatePet(@path petId: int32, @body pet: Pet): {
55-
@body pet: Pet;
56-
} | {
57-
@body error: NotFoundError;
58-
};
61+
op updatePet(
62+
@path petId: int32,
63+
@body pet: OptionalProperties<Pet>,
64+
): SinglePetResponse | CommonNotFoundResponse;
5965

6066
@delete
61-
op deletePet(@path petId: int32): {
62-
@body pet: Pet;
63-
} | {
64-
@body error: NotFoundError;
65-
};
67+
op deletePet(@path petId: int32): SinglePetResponse | CommonNotFoundResponse;
6668

6769
@route("/type/{petType}")
6870
namespace ByType {
6971
@get
70-
op listPets(@path petType: petType): {
71-
@body pets: Pet[];
72-
};
72+
op listPets(@path petType: petType): MultiplePetResponse;
7373
}
7474
}
7575

packages/integration-tests/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
},
1111
"devDependencies": {
1212
"@typespec-tools/emitter-express": "*",
13+
"@typespec-tools/emitter-fetch-client": "*",
1314
"@typespec-tools/emitter-typescript": "*",
1415
"@typespec-tools/emitter-zod": "*",
1516
"@typespec/compiler": "0.60.0",

0 commit comments

Comments
 (0)