Skip to content

Commit

Permalink
feat: try out a new route declaration syntax, aimed at compatibility
Browse files Browse the repository at this point in the history
  • Loading branch information
swain committed Nov 1, 2023
1 parent b5f7dc5 commit ff2dc06
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 7 deletions.
89 changes: 89 additions & 0 deletions src/compat-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import Router from '@koa/router';
import { z } from 'zod';
import {
ContextOfEndpoint,
EndpointImplementation,
OneSchemaRouterMiddleware,
} from './koa-utils';
import {
NamedClient,
OneSchemaRouter,
OneSchemaRouterConfig,
RouterEndpointDefinition,
ZodSchema,
} from './router';
import { AxiosInstance } from 'axios';

type MethodMap = {
get: 'GET';
post: 'POST';
put: 'PUT';
patch: 'PATCH';
delete: 'DELETE';
};

export type OneSchemaCompatRouter<
Schema extends ZodSchema,
R extends Router,
> = {
[Method in keyof MethodMap]: <
Path extends string,
Name extends string,
Endpoint extends RouterEndpointDefinition<Name>,
>(
path: Path,
meta: Endpoint,
...middlewares: [
...OneSchemaRouterMiddleware<
ContextOfEndpoint<
`${MethodMap[Method]} ${Path}`,
z.output<Endpoint['request']>,
R
>
>[],
EndpointImplementation<
`${MethodMap[Method]} ${Path}`,
z.output<Endpoint['request']>,
z.infer<Endpoint['response']>,
R
>,
]
) => OneSchemaCompatRouter<
Schema & {
[Route in `${MethodMap[Method]} ${Path}`]: Endpoint;
},
R
>;
} & {
client: (instance: AxiosInstance) => NamedClient<Schema>;
middleware: () => Router.Middleware;
};

export const createCompatRouter = <R extends Router<any, any>>(
config: OneSchemaRouterConfig<R>,
): OneSchemaCompatRouter<{}, R> => {
const _router = OneSchemaRouter.create(config);

const compatRouter: any = {
middleware: () => _router.middleware(),
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
client: (instance: any) => _router.client(instance),
};
for (const method of ['get', 'post', 'put', 'patch', 'delete'] as const) {
compatRouter[method] = (
path: string,
endpoint: any,
...middlewares: any[]
) => {
const route = `${method.toUpperCase()} ${path}`;
_router
.declare({ ...endpoint, route })
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
.implement(route, ...middlewares);

return compatRouter;
};
}
return compatRouter;
};
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './koa';
export * from './types';
export * from './router';
export * from './compat-router';

// We intentionally avoid exposing codegen-related modules here,
// so that consumers don't unnecessarily bundle codegen logic
Expand Down
55 changes: 49 additions & 6 deletions src/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@ import { format } from 'prettier';
import Koa = require('koa');
import Router = require('@koa/router');
import bodyparser = require('koa-bodyparser');
import { NamedClientFor, OneSchemaRouter } from './router';
import { NamedClient, OneSchemaRouter, ZodSchema } from './router';
import { z } from 'zod';
import { generateAxiosClient } from './generate-axios-client';
import { OneSchemaCompatRouter, createCompatRouter } from './compat-router';

let server: Server | undefined = undefined;
afterEach(() => {
server?.close();
});

const setup = <T extends OneSchemaRouter<any, any>>(
expose: (router: OneSchemaRouter<{}, Router>) => T,
): { client: AxiosInstance; typed: NamedClientFor<T> } => {
const setup = <Schema extends ZodSchema>(
expose: (
router: OneSchemaRouter<{}, Router>,
) => OneSchemaRouter<Schema, any> | OneSchemaCompatRouter<Schema, any>,
): { client: AxiosInstance; typed: NamedClient<Schema> } => {
const router = expose(
OneSchemaRouter.create({ using: new Router(), introspection: undefined }),
);
Expand All @@ -25,13 +28,12 @@ const setup = <T extends OneSchemaRouter<any, any>>(

return {
client,
// @ts-expect-error TS is too dumb to know that this is right.
typed: router.client(client),
};
};

const serve = (
router: OneSchemaRouter<{}, Router>,
router: OneSchemaRouter<any, any> | OneSchemaCompatRouter<any, any>,
): { client: AxiosInstance } => {
server = new Koa().use(bodyparser()).use(router.middleware()).listen();

Expand Down Expand Up @@ -942,3 +944,44 @@ test('the client(...) helper', async () => {
expect(postResponse.status).toStrictEqual(200);
expect(postResponse.data).toStrictEqual({ id: 'some-id' });
});

describe('compat router', () => {
test('get + post', async () => {
const { typed: client } = setup(() =>
createCompatRouter({ introspection: undefined, using: new Router() })
.post(
'/items',
{
name: 'createItem',
request: z.object({ id: z.string() }),
response: z.object({ id: z.string() }),
},
(ctx) => ({ id: ctx.request.body.id }),
)
.get(
'/items/:id',
{
name: 'getItemById',
request: z.object({
filter: z.string(),
}),
response: z.object({ id: z.string() }),
},
(ctx) => ({
id: ctx.params.id + ':' + ctx.request.query.filter,
}),
),
);

const res1 = await client.createItem({ id: 'test-id' });
expect(res1.status).toStrictEqual(200);
expect(res1.data).toStrictEqual({ id: 'test-id' });

const res2 = await client.getItemById({
id: 'test-id',
filter: 'test-filter',
});
expect(res2.status).toStrictEqual(200);
expect(res2.data).toStrictEqual({ id: 'test-id:test-filter' });
});
});
2 changes: 1 addition & 1 deletion src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export type RouterEndpointDefinition<Name> = {
type Method = 'GET' | 'DELETE' | 'PUT' | 'POST' | 'PATCH';
type RoughRoute = `${Method} ${string}`;

type ZodSchema = {
export type ZodSchema = {
[route: string]: RouterEndpointDefinition<string>;
};

Expand Down

0 comments on commit ff2dc06

Please sign in to comment.