Skip to content

Commit

Permalink
minor(emitter-express): add children routes to parent route handlers …
Browse files Browse the repository at this point in the history
…response
  • Loading branch information
codingmatty committed Sep 17, 2024
1 parent 52a1fff commit a519e68
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 41 deletions.
5 changes: 5 additions & 0 deletions .changeset/tidy-fans-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@typespec-tools/emitter-express": minor
---

Add children routes to parent route handlers response
28 changes: 15 additions & 13 deletions packages/emitter-express/src/emitter.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as prettier from "prettier";
import {
EmitContext,
getNamespaceFullName,
ModelProperty,
Namespace,
Operation,
} from "@typespec/compiler";
import {
Expand Down Expand Up @@ -53,16 +53,6 @@ function emitNamespace(scope: Scope<string>) {
return ns;
}

function getNamespaceChain(decl: { namespace?: Namespace }): string[] {
let ns = [decl.namespace?.name];
let parent = decl.namespace?.namespace;
while (parent) {
ns.push(parent.name);
parent = parent.namespace;
}
return ns.filter((n): n is string => !!n).reverse();
}

export class ExpressEmitter extends TypescriptEmitter<EmitterOptions> {
operationDeclaration(
operation: Operation,
Expand Down Expand Up @@ -161,7 +151,9 @@ export class ExpressEmitter extends TypescriptEmitter<EmitterOptions> {
for (const operation of httpService.operations) {
const namespace =
operation.operation.namespace?.name ?? httpService.namespace.name;
const namespaceChain = getNamespaceChain(operation.operation);
const namespaceChain = operation.operation.namespace
? getNamespaceFullName(operation.operation.namespace).split(".")
: [];
const declarations = declarationsByNamespace.get(namespace) ?? {
typedRouterCallbackTypes: [],
routeHandlerFunctions: [],
Expand Down Expand Up @@ -195,13 +187,16 @@ export class ExpressEmitter extends TypescriptEmitter<EmitterOptions> {
},
] of declarationsByNamespace) {
const nsScope = this.nsByName.get(namespaceName);
const childrenOperations = nsScope?.childScopes;

nsScope?.declarations.push(
new Declaration(
"",
nsScope,
`\n
export interface Handlers {
${typedRouterCallbackTypes.join("\n")}
${childrenOperations?.map((c) => `${c.name}: ${c.name}.Handlers;`).join("\n")}
}
`
)
Expand All @@ -212,7 +207,14 @@ export class ExpressEmitter extends TypescriptEmitter<EmitterOptions> {
${routeHandlerFunctions.join("\n")}
return {
${operationNames.join(",\n")}
${[
...operationNames,
childrenOperations?.map(
(c) =>
`${c.name}: create${namespaceChain.join("")}${c.name}Handlers(router)`
),
].join(",\n")}
};
}`
);
Expand Down
85 changes: 68 additions & 17 deletions packages/emitter-express/test/emitter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ describe("emitter-express", () => {
assert.match(contents, /export type listPetsBody = undefined;/);
assert.match(
contents,
/export type listPetsResponseBody = { pets: PetStore.Pet\[\] };/
/export type listPetsResponseBody = { pets: Pet\[\] };/
);
});

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

it("emits the route callback implementation", () => {
assert.match(
contents,
/const listPets: PetsHandlers\["listPets"\] = \(\.\.\.handlers\) => \{[\n\s]*router.get\("\/pets", \.\.\.handlers\);(\n|.)*\};/
/const listPets: PetStore.Pets.Handlers\["listPets"\] = \(\.\.\.handlers\) => \{[\n\s]*router.get\("\/pets", \.\.\.handlers\);(\n|.)*\};/
);
assert.match(contents, /return \{(\n|.)*listPets,(\n|.)*\};/);
});
Expand All @@ -170,7 +170,7 @@ describe("emitter-express", () => {
assert.match(contents, /export type getPetBody = undefined;/);
assert.match(
contents,
/export type getPetResponseBody =(\n|.)*\| \{ pet: PetStore.Pet \}(\n|.)*\| \{ error: PetStore.NotFoundError \};/
/export type getPetResponseBody = \{ pet: Pet \} \| \{ error: NotFoundError \};/
);
});

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

it("emits the route callback implementation", () => {
assert.match(
contents,
/const getPet: PetsHandlers\["getPet"\] = \(\.\.\.handlers\) => \{[\n\s]*router.get\("\/pets\/:petId", \.\.\.handlers\);(\n|.)*\};/
/const getPet: PetStore.Pets.Handlers\["getPet"\] = \(\.\.\.handlers\) => \{[\n\s]*router.get\("\/pets\/:petId", \.\.\.handlers\);(\n|.)*\};/
);
assert.match(contents, /return \{(\n|.)*getPet,(\n|.)*\};/);
});
Expand All @@ -201,10 +201,10 @@ describe("emitter-express", () => {
it("emits the function types", () => {
assert.match(contents, /export type createPetParams = \{\};/);
assert.match(contents, /export type createPetQuery = \{\};/);
assert.match(contents, /export type createPetBody = PetStore.Pet;/);
assert.match(contents, /export type createPetBody = Pet;/);
assert.match(
contents,
/export type createPetResponseBody = \{ pet: PetStore.Pet \};/
/export type createPetResponseBody = \{ pet: Pet \};/
);
});

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

it("emits the route callback implementation", () => {
assert.match(
contents,
/const createPet: PetsHandlers\["createPet"\] = \(\.\.\.handlers\) => \{[\n\s]*router.post\("\/pets", \.\.\.handlers\);(\n|.)*\};/
/const createPet: PetStore.Pets.Handlers\["createPet"\] = \(\.\.\.handlers\) => \{[\n\s]*router.post\("\/pets", \.\.\.handlers\);(\n|.)*\};/
);
assert.match(contents, /return \{(\n|.)*createPet,(\n|.)*\};/);
});
Expand All @@ -244,7 +244,7 @@ describe("emitter-express", () => {
);
assert.match(
contents,
/export type updatePetResponseBody =(\n|.)*\| \{ pet: PetStore.Pet \}(\n|.)*\| \{ error: PetStore.NotFoundError \};/
/export type updatePetResponseBody = \{ pet: Pet \} \| \{ error: NotFoundError \};/
);
});

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

it("emits the route callback implementation", () => {
assert.match(
contents,
/const updatePet: PetsHandlers\["updatePet"\] = \(\.\.\.handlers\) => \{[\n\s]*router.put\("\/pets\/:petId", \.\.\.handlers\);(\n|.)*\};/
/const updatePet: PetStore.Pets.Handlers\["updatePet"\] = \(\.\.\.handlers\) => \{[\n\s]*router.put\("\/pets\/:petId", \.\.\.handlers\);(\n|.)*\};/
);
assert.match(contents, /return \{(\n|.)*updatePet,(\n|.)*\};/);
});
Expand All @@ -287,7 +287,7 @@ describe("emitter-express", () => {
);
assert.match(
contents,
/export namespace Animals \{(\n|.)*export type listPetsResponseBody = { pets: PetStore.Pet\[\] };(\n|.)*\}/
/export namespace Animals \{(\n|.)*export type listPetsResponseBody = { pets: Pet\[\] };(\n|.)*\}/
);
});

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

it("emits the route callback implementation", () => {
assert.match(
contents,
/const listPets: AnimalsHandlers\["listPets"\] = \(\.\.\.handlers\) => \{[\n\s]*router.get\("\/animals", \.\.\.handlers\);(\n|.)*\};/
/const listPets: PetStore.Animals.Handlers\["listPets"\] = \(\.\.\.handlers\) => \{[\n\s]*router.get\("\/animals", \.\.\.handlers\);(\n|.)*\};/
);
assert.match(contents, /return \{(\n|.)*listPets,(\n|.)*\};/);
});

it('emits the "Animals" namespace in the TypedRouter', () => {
assert.match(
contents,
/export interface TypedRouter \{(\n|.)*Animals: AnimalsHandlers;(\n|.)*\}/
/export interface TypedRouter \{(\n|.)*PetStoreAnimals: PetStore.Animals.Handlers;(\n|.)*\}/
);
});
});
Expand All @@ -341,6 +341,57 @@ describe("emitter-express", () => {
assert.match(contents, /export function createTypedRouter/);
});

describe("namespaces", async () => {
beforeAll(async () => {
contents = await emitTypeSpecToTs(`
import "@typespec/http";
using TypeSpec.Http;
@server("https://example.com", "Single server endpoint")
namespace PetStore;
model Pet {
id: int32;
name: string;
age: int32;
kind: string;
}
@route("/pets")
namespace Pets {
@get
op listPets(@query type?: string): {
@body pets: Pet[];
};
@route("/{type}")
namespace ByType {
@get
op listPets(@path type: string): {
@body pets: Pet[];
};
@route("/{age}")
namespace ByAge {
@get
op listPets(@path type: string, @path age: int32): {
@body pets: Pet[];
};
}
}
}
`);
});

it("emits the hierarchy of namespace types", () => {
assert.match(
contents,
/export namespace PetStore \{(\n|.)*export namespace Pets \{(\n|.)*export namespace ByType \{(\n|.)*export namespace ByAge \{(\n|.)*\}(\n|.)*\}(\n|.)*\}(\n|.)*\}/
);
});
});

// it("emits to multiple files", async () => {
// const host = await getHostForTypeSpecFile(testCode);

Expand Down
15 changes: 4 additions & 11 deletions packages/emitter-typescript/src/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Enum,
EnumMember,
getDoc,
getNamespaceFullName,
Interface,
IntrinsicType,
Model,
Expand Down Expand Up @@ -72,16 +73,6 @@ function emitNamespace(scope: Scope<string>) {
return ns;
}

function getNamespaceChain(decl: { namespace?: Namespace }): string[] {
let ns = [decl.namespace?.name];
let parent = decl.namespace?.namespace;
while (parent) {
ns.push(parent.name);
parent = parent.namespace;
}
return ns.filter((n): n is string => !!n).reverse();
}

export class TypescriptEmitter<
TEmitterOptions extends object = EmitterOptions,
> extends CodeTypeEmitter<TEmitterOptions> {
Expand All @@ -93,7 +84,9 @@ export class TypescriptEmitter<
const name = decl.namespace?.name;
if (!name) return {};

const namespaceChain = getNamespaceChain(decl);
const namespaceChain = decl.namespace
? getNamespaceFullName(decl.namespace).split(".")
: [];

let nsScope = this.nsByName.get(name);
if (!nsScope) {
Expand Down

0 comments on commit a519e68

Please sign in to comment.