Skip to content

Commit 52a1fff

Browse files
committed
minor(emitter-express): emit namespace blocks
1 parent 2239271 commit 52a1fff

File tree

5 files changed

+90
-79
lines changed

5 files changed

+90
-79
lines changed

.changeset/strong-gifts-explode.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@typespec-tools/emitter-typescript": minor
3+
"@typespec-tools/emitter-express": minor
4+
---
5+
6+
emit namespace blocks

packages/emitter-express/src/emitter.ts

Lines changed: 57 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as prettier from "prettier";
22
import {
33
EmitContext,
4-
Model,
54
ModelProperty,
65
Namespace,
76
Operation,
@@ -18,6 +17,7 @@ import {
1817
code,
1918
Context,
2019
createAssetEmitter,
20+
Declaration,
2121
EmittedSourceFile,
2222
EmitterOutput,
2323
Scope,
@@ -28,6 +28,13 @@ import { TypescriptEmitter } from "@typespec-tools/emitter-typescript";
2828

2929
import { EmitterOptions } from "./lib.js";
3030

31+
type NamespaceDeclarations = {
32+
typedRouterCallbackTypes: string[];
33+
routeHandlerFunctions: string[];
34+
operationNames: string[];
35+
namespaceChain: string[];
36+
};
37+
3138
function emitNamespaces(scope: Scope<string>) {
3239
let res = "";
3340
for (const childScope of scope.childScopes) {
@@ -46,36 +53,17 @@ function emitNamespace(scope: Scope<string>) {
4653
return ns;
4754
}
4855

49-
export class ExpressEmitter extends TypescriptEmitter<EmitterOptions> {
50-
private nsByName: Map<string, Scope<string>> = new Map();
51-
52-
#DeclarationContext(decl: { namespace?: Namespace }): Context {
53-
const name = decl.namespace?.name;
54-
if (!name) return {};
55-
56-
let nsScope = this.nsByName.get(name);
57-
if (!nsScope) {
58-
nsScope = this.emitter.createScope(
59-
{},
60-
name,
61-
this.emitter.getContext().scope
62-
);
63-
this.nsByName.set(name, nsScope);
64-
}
65-
66-
return {
67-
scope: nsScope,
68-
};
69-
}
70-
71-
modelDeclarationContext(model: Model): Context {
72-
return this.#DeclarationContext(model);
73-
}
74-
75-
operationDeclarationContext(operation: Operation): Context {
76-
return this.#DeclarationContext(operation);
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;
7762
}
63+
return ns.filter((n): n is string => !!n).reverse();
64+
}
7865

66+
export class ExpressEmitter extends TypescriptEmitter<EmitterOptions> {
7967
operationDeclaration(
8068
operation: Operation,
8169
name: string
@@ -166,75 +154,86 @@ export class ExpressEmitter extends TypescriptEmitter<EmitterOptions> {
166154
emittedSourceFile.contents += decl.value + "\n";
167155
}
168156

169-
emittedSourceFile.contents += emitNamespaces(sourceFile.globalScope);
170-
171-
const declarationsByNamespace: Map<
172-
string,
173-
{
174-
typedRouterCallbackTypes: string[];
175-
routeHandlerFunctions: string[];
176-
operationNames: string[];
177-
}
178-
> = new Map();
157+
const declarationsByNamespace: Map<string, NamespaceDeclarations> =
158+
new Map();
179159

180160
for (const httpService of httpServices) {
181161
for (const operation of httpService.operations) {
182162
const namespace =
183163
operation.operation.namespace?.name ?? httpService.namespace.name;
164+
const namespaceChain = getNamespaceChain(operation.operation);
184165
const declarations = declarationsByNamespace.get(namespace) ?? {
185166
typedRouterCallbackTypes: [],
186167
routeHandlerFunctions: [],
187168
operationNames: [],
169+
namespaceChain,
188170
};
189171

190-
const namespaceName = operation.operation.namespace?.name ?? "";
191-
const handlerType = namespaceName
192-
? `${namespaceName}.${operation.operation.name}Handler`
193-
: `${operation.operation.name}Handler`;
172+
const handlerType = `${operation.operation.name}Handler`;
194173
const operationName = operation.operation.name;
195174
declarations.operationNames.push(operationName);
196175
declarations.typedRouterCallbackTypes.push(
197176
`${operationName}: (...handlers: Array<${handlerType}>) => void;`
198177
);
199178
declarations.routeHandlerFunctions.push(
200-
`const ${operationName}: ${namespaceName}Handlers["${operationName}"] = (...handlers) => { router.${operation.verb}('${operation.path.replace(/\{(\w+)\}/, ":$1")}', ...handlers); };`
179+
`const ${operationName}: ${namespaceChain.join(".")}.Handlers["${operationName}"] = (...handlers) => { router.${operation.verb}('${operation.path.replace(/\{(\w+)\}/, ":$1")}', ...handlers); };`
201180
);
202181

203182
declarationsByNamespace.set(namespace, declarations);
204183
}
205184
}
206185

186+
const handlerFunctions: string[] = [];
187+
207188
for (const [
208189
namespaceName,
209-
{ typedRouterCallbackTypes, routeHandlerFunctions, operationNames },
190+
{
191+
typedRouterCallbackTypes,
192+
routeHandlerFunctions,
193+
operationNames,
194+
namespaceChain,
195+
},
210196
] of declarationsByNamespace) {
211-
emittedSourceFile.contents += `\n
212-
export interface ${namespaceName}Handlers {
197+
const nsScope = this.nsByName.get(namespaceName);
198+
nsScope?.declarations.push(
199+
new Declaration(
200+
"",
201+
nsScope,
202+
`\n
203+
export interface Handlers {
213204
${typedRouterCallbackTypes.join("\n")}
214205
}
206+
`
207+
)
208+
);
215209

216-
export function create${namespaceName}Handlers(router: express.Router): ${namespaceName}Handlers {
217-
${routeHandlerFunctions.join("\n")}
218-
219-
return {
220-
${operationNames.join(",\n")}
221-
};
222-
}
223-
`;
210+
handlerFunctions.push(
211+
`export function create${namespaceChain.join("")}Handlers(router: express.Router): ${namespaceChain.join(".")}.Handlers {
212+
${routeHandlerFunctions.join("\n")}
213+
214+
return {
215+
${operationNames.join(",\n")}
216+
};
217+
}`
218+
);
224219
}
225220

226-
const namespaces = Array.from(declarationsByNamespace.keys());
221+
emittedSourceFile.contents += emitNamespaces(sourceFile.globalScope);
222+
223+
const namespaces = Array.from(declarationsByNamespace.values());
227224

228225
emittedSourceFile.contents += `\n
226+
${handlerFunctions.join("\n")}
227+
229228
export interface TypedRouter {
230229
router: express.Router;
231-
${namespaces.map((ns) => `${ns}: ${ns}Handlers;`).join("\n")}
230+
${namespaces.map(({ namespaceChain }) => `${namespaceChain.join("")}: ${namespaceChain.join(".")}.Handlers;`).join("\n")}
232231
}
233232
234233
export function createTypedRouter(router: express.Router): TypedRouter {
235234
return {
236235
router,
237-
${namespaces.map((ns) => `${ns}: create${ns}Handlers(router),`).join("\n")}
236+
${namespaces.map(({ namespaceChain }) => `${namespaceChain.join("")}: create${namespaceChain.join("")}Handlers(router),`).join("\n")}
238237
};
239238
}
240239
`;

packages/emitter-typescript/src/emitter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ function getNamespaceChain(decl: { namespace?: Namespace }): string[] {
8585
export class TypescriptEmitter<
8686
TEmitterOptions extends object = EmitterOptions,
8787
> extends CodeTypeEmitter<TEmitterOptions> {
88-
private nsByName: Map<string, Scope<string>> = new Map();
88+
protected nsByName: Map<string, Scope<string>> = new Map();
8989

9090
declarationContext(
9191
decl: TypeSpecDeclaration & { namespace?: Namespace }

packages/integration-tests/main.tsp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ namespace Pets {
6363
} | {
6464
@body error: NotFoundError;
6565
};
66+
67+
@route("/{petType}")
68+
namespace ByType {
69+
@get
70+
op listPets(@path petType: petType): {
71+
@body pets: Pet[];
72+
};
73+
}
6674
}
6775

6876
@error

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

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ import { beforeAll, describe, expect, it } from "vitest";
44
import {
55
createTypedRouter,
66
PetStore,
7-
petType,
87
TypedRouter,
9-
Pets,
108
} from "../tsp-output/@typespec-tools/emitter-express/output";
119
import bodyParser = require("body-parser");
1210

@@ -23,13 +21,13 @@ function startServer() {
2321
}
2422

2523
const pets: PetStore.Pet[] = [
26-
{ id: 1, name: "Fluffy", age: 3, kind: petType.dog },
27-
{ id: 2, name: "Rex", age: 8, kind: petType.cat },
28-
{ id: 3, name: "Charlie", age: 10, kind: petType.bird },
29-
{ id: 4, name: "Bella", age: 2, kind: petType.fish },
30-
{ id: 5, name: "Max", age: 4, kind: petType.dog },
31-
{ id: 6, name: "Lucy", age: 5, kind: petType.cat },
32-
{ id: 7, name: "Tucker", age: 1, kind: petType.reptile },
24+
{ id: 1, name: "Fluffy", age: 3, kind: PetStore.petType.dog },
25+
{ id: 2, name: "Rex", age: 8, kind: PetStore.petType.cat },
26+
{ id: 3, name: "Charlie", age: 10, kind: PetStore.petType.bird },
27+
{ id: 4, name: "Bella", age: 2, kind: PetStore.petType.fish },
28+
{ id: 5, name: "Max", age: 4, kind: PetStore.petType.dog },
29+
{ id: 6, name: "Lucy", age: 5, kind: PetStore.petType.cat },
30+
{ id: 7, name: "Tucker", age: 1, kind: PetStore.petType.reptile },
3331
];
3432

3533
describe("emitter-express", () => {
@@ -44,7 +42,7 @@ describe("emitter-express", () => {
4442

4543
describe('GET "/pets"', () => {
4644
beforeAll(() => {
47-
typedRouter.Pets.listPets((req, res) => {
45+
typedRouter.PetStorePets.listPets((req, res) => {
4846
const filteredPets = pets.filter((pet) => {
4947
return !req.query.type || pet.kind === req.query.type;
5048
});
@@ -66,7 +64,7 @@ describe("emitter-express", () => {
6664
it("should return a list of pets for specific type", async () => {
6765
const res = await fetch("http://localhost:3456/pets?type=dog");
6866
const data = (await res.json()) as Extract<
69-
Pets.listPetsResponseBody,
67+
PetStore.Pets.listPetsResponseBody,
7068
{ pets: any }
7169
>;
7270
expect(data.pets).toHaveLength(2);
@@ -75,7 +73,7 @@ describe("emitter-express", () => {
7573

7674
describe('GET "/pets/:id"', () => {
7775
beforeAll(() => {
78-
typedRouter.Pets.getPet((req, res) => {
76+
typedRouter.PetStorePets.getPet((req, res) => {
7977
const pet = pets.find((pet) => {
8078
return pet.id.toString() === req.params.petId;
8179
});
@@ -111,7 +109,7 @@ describe("emitter-express", () => {
111109

112110
describe('POST "/pets"', () => {
113111
beforeAll(() => {
114-
typedRouter.Pets.createPet((req, res) => {
112+
typedRouter.PetStorePets.createPet((req, res) => {
115113
const pet: PetStore.Pet = req.body;
116114
// pets.push(pet);
117115
res.json({ pet });
@@ -126,7 +124,7 @@ describe("emitter-express", () => {
126124
id: 8,
127125
name: "Daisy",
128126
age: 6,
129-
kind: petType.cat,
127+
kind: PetStore.petType.cat,
130128
}),
131129
});
132130
expect(res.status).toBe(200);
@@ -140,25 +138,25 @@ describe("emitter-express", () => {
140138
id: 8,
141139
name: "Daisy",
142140
age: 6,
143-
kind: petType.cat,
141+
kind: PetStore.petType.cat,
144142
}),
145143
});
146144
const data = (await res.json()) as Extract<
147-
Pets.createPetResponseBody,
145+
PetStore.Pets.createPetResponseBody,
148146
{ pet: any }
149147
>;
150148
expect(data.pet).toMatchObject({
151149
id: 8,
152150
name: "Daisy",
153151
age: 6,
154-
kind: petType.cat,
152+
kind: PetStore.petType.cat,
155153
});
156154
});
157155
});
158156

159157
describe('PUT "/pets/:id"', () => {
160158
beforeAll(() => {
161-
typedRouter.Pets.updatePet((req, res) => {
159+
typedRouter.PetStorePets.updatePet((req, res) => {
162160
const pet = pets.find((pet) => {
163161
return pet.id.toString() === req.params.petId;
164162
});
@@ -206,7 +204,7 @@ describe("emitter-express", () => {
206204
describe('DELETE "/pets/:id"', () => {
207205
beforeAll(() => {
208206
const tempPets = [...pets];
209-
typedRouter.Pets.deletePet((req, res) => {
207+
typedRouter.PetStorePets.deletePet((req, res) => {
210208
const index = tempPets.findIndex((pet) => {
211209
return pet.id.toString() === req.params.petId;
212210
});
@@ -232,7 +230,7 @@ describe("emitter-express", () => {
232230
it("should return the deleted pet", async () => {
233231
const res = await fetch("http://localhost:3456/pets/1");
234232
const data = (await res.json()) as Extract<
235-
Pets.deletePetResponseBody,
233+
PetStore.Pets.deletePetResponseBody,
236234
{ pet: any }
237235
>;
238236
expect(data.pet).toMatchObject(pets[0]!);

0 commit comments

Comments
 (0)