Skip to content

Commit a519e68

Browse files
committed
minor(emitter-express): add children routes to parent route handlers response
1 parent 52a1fff commit a519e68

File tree

4 files changed

+92
-41
lines changed

4 files changed

+92
-41
lines changed

.changeset/tidy-fans-tan.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@typespec-tools/emitter-express": minor
3+
---
4+
5+
Add children routes to parent route handlers response

packages/emitter-express/src/emitter.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import * as prettier from "prettier";
22
import {
33
EmitContext,
4+
getNamespaceFullName,
45
ModelProperty,
5-
Namespace,
66
Operation,
77
} from "@typespec/compiler";
88
import {
@@ -53,16 +53,6 @@ function emitNamespace(scope: Scope<string>) {
5353
return ns;
5454
}
5555

56-
function getNamespaceChain(decl: { namespace?: Namespace }): string[] {
57-
let ns = [decl.namespace?.name];
58-
let parent = decl.namespace?.namespace;
59-
while (parent) {
60-
ns.push(parent.name);
61-
parent = parent.namespace;
62-
}
63-
return ns.filter((n): n is string => !!n).reverse();
64-
}
65-
6656
export class ExpressEmitter extends TypescriptEmitter<EmitterOptions> {
6757
operationDeclaration(
6858
operation: Operation,
@@ -161,7 +151,9 @@ export class ExpressEmitter extends TypescriptEmitter<EmitterOptions> {
161151
for (const operation of httpService.operations) {
162152
const namespace =
163153
operation.operation.namespace?.name ?? httpService.namespace.name;
164-
const namespaceChain = getNamespaceChain(operation.operation);
154+
const namespaceChain = operation.operation.namespace
155+
? getNamespaceFullName(operation.operation.namespace).split(".")
156+
: [];
165157
const declarations = declarationsByNamespace.get(namespace) ?? {
166158
typedRouterCallbackTypes: [],
167159
routeHandlerFunctions: [],
@@ -195,13 +187,16 @@ export class ExpressEmitter extends TypescriptEmitter<EmitterOptions> {
195187
},
196188
] of declarationsByNamespace) {
197189
const nsScope = this.nsByName.get(namespaceName);
190+
const childrenOperations = nsScope?.childScopes;
191+
198192
nsScope?.declarations.push(
199193
new Declaration(
200194
"",
201195
nsScope,
202196
`\n
203197
export interface Handlers {
204198
${typedRouterCallbackTypes.join("\n")}
199+
${childrenOperations?.map((c) => `${c.name}: ${c.name}.Handlers;`).join("\n")}
205200
}
206201
`
207202
)
@@ -212,7 +207,14 @@ export class ExpressEmitter extends TypescriptEmitter<EmitterOptions> {
212207
${routeHandlerFunctions.join("\n")}
213208
214209
return {
215-
${operationNames.join(",\n")}
210+
${[
211+
...operationNames,
212+
childrenOperations?.map(
213+
(c) =>
214+
`${c.name}: create${namespaceChain.join("")}${c.name}Handlers(router)`
215+
),
216+
].join(",\n")}
217+
216218
};
217219
}`
218220
);

packages/emitter-express/test/emitter.test.ts

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ describe("emitter-express", () => {
136136
assert.match(contents, /export type listPetsBody = undefined;/);
137137
assert.match(
138138
contents,
139-
/export type listPetsResponseBody = { pets: PetStore.Pet\[\] };/
139+
/export type listPetsResponseBody = { pets: Pet\[\] };/
140140
);
141141
});
142142

@@ -150,14 +150,14 @@ describe("emitter-express", () => {
150150
it("emits the route callback type", () => {
151151
assert.match(
152152
contents,
153-
/export interface PetsHandlers \{(\n|.)*listPets: \(\.\.\.handlers: Array<Pets.listPetsHandler>\) => void;(\n|.)*\}/
153+
/export interface Handlers \{(\n|.)*listPets: \(\.\.\.handlers: Array<listPetsHandler>\) => void;(\n|.)*\}/
154154
);
155155
});
156156

157157
it("emits the route callback implementation", () => {
158158
assert.match(
159159
contents,
160-
/const listPets: PetsHandlers\["listPets"\] = \(\.\.\.handlers\) => \{[\n\s]*router.get\("\/pets", \.\.\.handlers\);(\n|.)*\};/
160+
/const listPets: PetStore.Pets.Handlers\["listPets"\] = \(\.\.\.handlers\) => \{[\n\s]*router.get\("\/pets", \.\.\.handlers\);(\n|.)*\};/
161161
);
162162
assert.match(contents, /return \{(\n|.)*listPets,(\n|.)*\};/);
163163
});
@@ -170,7 +170,7 @@ describe("emitter-express", () => {
170170
assert.match(contents, /export type getPetBody = undefined;/);
171171
assert.match(
172172
contents,
173-
/export type getPetResponseBody =(\n|.)*\| \{ pet: PetStore.Pet \}(\n|.)*\| \{ error: PetStore.NotFoundError \};/
173+
/export type getPetResponseBody = \{ pet: Pet \} \| \{ error: NotFoundError \};/
174174
);
175175
});
176176

@@ -184,14 +184,14 @@ describe("emitter-express", () => {
184184
it("emits the route callback type", () => {
185185
assert.match(
186186
contents,
187-
/export interface PetsHandlers \{(\n|.)*getPet: \(\.\.\.handlers: Array<Pets.getPetHandler>\) => void;(\n|.)*\}/
187+
/export interface Handlers \{(\n|.)*getPet: \(\.\.\.handlers: Array<getPetHandler>\) => void;(\n|.)*\}/
188188
);
189189
});
190190

191191
it("emits the route callback implementation", () => {
192192
assert.match(
193193
contents,
194-
/const getPet: PetsHandlers\["getPet"\] = \(\.\.\.handlers\) => \{[\n\s]*router.get\("\/pets\/:petId", \.\.\.handlers\);(\n|.)*\};/
194+
/const getPet: PetStore.Pets.Handlers\["getPet"\] = \(\.\.\.handlers\) => \{[\n\s]*router.get\("\/pets\/:petId", \.\.\.handlers\);(\n|.)*\};/
195195
);
196196
assert.match(contents, /return \{(\n|.)*getPet,(\n|.)*\};/);
197197
});
@@ -201,10 +201,10 @@ describe("emitter-express", () => {
201201
it("emits the function types", () => {
202202
assert.match(contents, /export type createPetParams = \{\};/);
203203
assert.match(contents, /export type createPetQuery = \{\};/);
204-
assert.match(contents, /export type createPetBody = PetStore.Pet;/);
204+
assert.match(contents, /export type createPetBody = Pet;/);
205205
assert.match(
206206
contents,
207-
/export type createPetResponseBody = \{ pet: PetStore.Pet \};/
207+
/export type createPetResponseBody = \{ pet: Pet \};/
208208
);
209209
});
210210

@@ -218,14 +218,14 @@ describe("emitter-express", () => {
218218
it("emits the route callback type", () => {
219219
assert.match(
220220
contents,
221-
/export interface PetsHandlers \{(\n|.)*createPet: \(\.\.\.handlers: Array<Pets.createPetHandler>\) => void;(\n|.)*\}/
221+
/export interface Handlers \{(\n|.)*createPet: \(\.\.\.handlers: Array<createPetHandler>\) => void;(\n|.)*\}/
222222
);
223223
});
224224

225225
it("emits the route callback implementation", () => {
226226
assert.match(
227227
contents,
228-
/const createPet: PetsHandlers\["createPet"\] = \(\.\.\.handlers\) => \{[\n\s]*router.post\("\/pets", \.\.\.handlers\);(\n|.)*\};/
228+
/const createPet: PetStore.Pets.Handlers\["createPet"\] = \(\.\.\.handlers\) => \{[\n\s]*router.post\("\/pets", \.\.\.handlers\);(\n|.)*\};/
229229
);
230230
assert.match(contents, /return \{(\n|.)*createPet,(\n|.)*\};/);
231231
});
@@ -244,7 +244,7 @@ describe("emitter-express", () => {
244244
);
245245
assert.match(
246246
contents,
247-
/export type updatePetResponseBody =(\n|.)*\| \{ pet: PetStore.Pet \}(\n|.)*\| \{ error: PetStore.NotFoundError \};/
247+
/export type updatePetResponseBody = \{ pet: Pet \} \| \{ error: NotFoundError \};/
248248
);
249249
});
250250

@@ -258,14 +258,14 @@ describe("emitter-express", () => {
258258
it("emits the route callback type", () => {
259259
assert.match(
260260
contents,
261-
/export interface PetsHandlers \{(\n|.)*updatePet: \(\.\.\.handlers: Array<Pets.updatePetHandler>\) => void;(\n|.)*\}/
261+
/export interface Handlers \{(\n|.)*updatePet: \(\.\.\.handlers: Array<updatePetHandler>\) => void;(\n|.)*\}/
262262
);
263263
});
264264

265265
it("emits the route callback implementation", () => {
266266
assert.match(
267267
contents,
268-
/const updatePet: PetsHandlers\["updatePet"\] = \(\.\.\.handlers\) => \{[\n\s]*router.put\("\/pets\/:petId", \.\.\.handlers\);(\n|.)*\};/
268+
/const updatePet: PetStore.Pets.Handlers\["updatePet"\] = \(\.\.\.handlers\) => \{[\n\s]*router.put\("\/pets\/:petId", \.\.\.handlers\);(\n|.)*\};/
269269
);
270270
assert.match(contents, /return \{(\n|.)*updatePet,(\n|.)*\};/);
271271
});
@@ -287,7 +287,7 @@ describe("emitter-express", () => {
287287
);
288288
assert.match(
289289
contents,
290-
/export namespace Animals \{(\n|.)*export type listPetsResponseBody = { pets: PetStore.Pet\[\] };(\n|.)*\}/
290+
/export namespace Animals \{(\n|.)*export type listPetsResponseBody = { pets: Pet\[\] };(\n|.)*\}/
291291
);
292292
});
293293

@@ -301,22 +301,22 @@ describe("emitter-express", () => {
301301
it("emits the route callback type", () => {
302302
assert.match(
303303
contents,
304-
/export interface AnimalsHandlers \{(\n|.)*listPets: \(\.\.\.handlers: Array<Animals.listPetsHandler>\) => void;(\n|.)*\}/
304+
/export interface Handlers \{(\n|.)*listPets: \(\.\.\.handlers: Array<listPetsHandler>\) => void;(\n|.)*\}/
305305
);
306306
});
307307

308308
it("emits the route callback implementation", () => {
309309
assert.match(
310310
contents,
311-
/const listPets: AnimalsHandlers\["listPets"\] = \(\.\.\.handlers\) => \{[\n\s]*router.get\("\/animals", \.\.\.handlers\);(\n|.)*\};/
311+
/const listPets: PetStore.Animals.Handlers\["listPets"\] = \(\.\.\.handlers\) => \{[\n\s]*router.get\("\/animals", \.\.\.handlers\);(\n|.)*\};/
312312
);
313313
assert.match(contents, /return \{(\n|.)*listPets,(\n|.)*\};/);
314314
});
315315

316316
it('emits the "Animals" namespace in the TypedRouter', () => {
317317
assert.match(
318318
contents,
319-
/export interface TypedRouter \{(\n|.)*Animals: AnimalsHandlers;(\n|.)*\}/
319+
/export interface TypedRouter \{(\n|.)*PetStoreAnimals: PetStore.Animals.Handlers;(\n|.)*\}/
320320
);
321321
});
322322
});
@@ -341,6 +341,57 @@ describe("emitter-express", () => {
341341
assert.match(contents, /export function createTypedRouter/);
342342
});
343343

344+
describe("namespaces", async () => {
345+
beforeAll(async () => {
346+
contents = await emitTypeSpecToTs(`
347+
import "@typespec/http";
348+
using TypeSpec.Http;
349+
350+
351+
@server("https://example.com", "Single server endpoint")
352+
namespace PetStore;
353+
354+
model Pet {
355+
id: int32;
356+
name: string;
357+
age: int32;
358+
kind: string;
359+
}
360+
361+
@route("/pets")
362+
namespace Pets {
363+
@get
364+
op listPets(@query type?: string): {
365+
@body pets: Pet[];
366+
};
367+
368+
@route("/{type}")
369+
namespace ByType {
370+
@get
371+
op listPets(@path type: string): {
372+
@body pets: Pet[];
373+
};
374+
375+
@route("/{age}")
376+
namespace ByAge {
377+
@get
378+
op listPets(@path type: string, @path age: int32): {
379+
@body pets: Pet[];
380+
};
381+
}
382+
}
383+
}
384+
`);
385+
});
386+
387+
it("emits the hierarchy of namespace types", () => {
388+
assert.match(
389+
contents,
390+
/export namespace PetStore \{(\n|.)*export namespace Pets \{(\n|.)*export namespace ByType \{(\n|.)*export namespace ByAge \{(\n|.)*\}(\n|.)*\}(\n|.)*\}(\n|.)*\}/
391+
);
392+
});
393+
});
394+
344395
// it("emits to multiple files", async () => {
345396
// const host = await getHostForTypeSpecFile(testCode);
346397

packages/emitter-typescript/src/emitter.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
Enum,
66
EnumMember,
77
getDoc,
8+
getNamespaceFullName,
89
Interface,
910
IntrinsicType,
1011
Model,
@@ -72,16 +73,6 @@ function emitNamespace(scope: Scope<string>) {
7273
return ns;
7374
}
7475

75-
function getNamespaceChain(decl: { namespace?: Namespace }): string[] {
76-
let ns = [decl.namespace?.name];
77-
let parent = decl.namespace?.namespace;
78-
while (parent) {
79-
ns.push(parent.name);
80-
parent = parent.namespace;
81-
}
82-
return ns.filter((n): n is string => !!n).reverse();
83-
}
84-
8576
export class TypescriptEmitter<
8677
TEmitterOptions extends object = EmitterOptions,
8778
> extends CodeTypeEmitter<TEmitterOptions> {
@@ -93,7 +84,9 @@ export class TypescriptEmitter<
9384
const name = decl.namespace?.name;
9485
if (!name) return {};
9586

96-
const namespaceChain = getNamespaceChain(decl);
87+
const namespaceChain = decl.namespace
88+
? getNamespaceFullName(decl.namespace).split(".")
89+
: [];
9790

9891
let nsScope = this.nsByName.get(name);
9992
if (!nsScope) {

0 commit comments

Comments
 (0)