Skip to content

Commit

Permalink
Merge pull request #45 from lifeomic/better-inference
Browse files Browse the repository at this point in the history
feat!: use `infer` statements to better infer router state + context
  • Loading branch information
swain authored Jul 28, 2022
2 parents d62cb31 + e5c47ad commit ced0a50
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 14 deletions.
7 changes: 5 additions & 2 deletions src/integration.koa.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ const TEST_SPEC: OneSchemaDefinition = withAssumptions({
});

const executeTest = async (
overrides: Partial<ImplementationConfig<any, any, any>>,
overrides: Partial<ImplementationConfig<any, Router>>,
testFn: (client: AxiosInstance) => Promise<void>,
) => {
const ajv = new Ajv();
const config: ImplementationConfig<any, any, any> = {
const config: ImplementationConfig<any, Router> = {
on: new Router(),
parse: (ctx, { schema, data }) => {
if (ajv.validate(schema, data)) {
Expand Down Expand Up @@ -151,6 +151,9 @@ test('GET method', async () => {
{
implementation: {
'GET /posts': (ctx) => {
// TypeScript isn't smart enough to determine the correct ctx.request
// property here.
// @ts-ignore
return ctx.request.query;
},
},
Expand Down
30 changes: 29 additions & 1 deletion src/koa.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ test('using unsupported methods throws immediately', () => {
},
}),
{
// @ts-ignore
on: new Router(),
parse: () => null as any,
implementation: {
Expand Down Expand Up @@ -103,3 +102,32 @@ test('setting a 200-level response code overrides the response', async () => {

server.close();
});

/**
* This test doesn't perform expectations -- rather, it will just
* cause build errors if the TypeScript doesn't apss compilation
*/
test('router typing is inferred correctly', () => {
const router = new Router<
{ dummyStateProperty: string },
{ dummyContextProperty: string }
>();

implementSchema(withAssumptions({ Endpoints: {} }), {
introspection: undefined,
parse: () => null as any,
on: router,
implementation: {
'GET /dummy-route': (ctx) => {
// assert state is extended correctly
ctx.state.dummyStateProperty;

// assert context is extended correctly
ctx.dummyContextProperty;
},
},
});

// perform a dummy expectation just to satisfy jest
expect(true).toBe(true);
});
41 changes: 30 additions & 11 deletions src/koa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,18 @@ type ExtendableContextWithRequestFieldsRemoved =
request: Omit<ExtendableContext['request'], 'body' | 'query'>;
};

export type ImplementationOf<Schema extends OneSchema<any>, State, Context> = {
type StateOfRouter<RouterType> = RouterType extends Router<infer State, any>
? State
: never;

type ContextOfRouter<RouterType> = RouterType extends Router<any, infer Context>
? Context
: never;

export type ImplementationOf<
Schema extends OneSchema<any>,
RouterType extends Router<any, any>,
> = {
[Name in keyof EndpointsOf<Schema>]: (
// prettier-ignore
context:
Expand All @@ -52,8 +63,8 @@ export type ImplementationOf<Schema extends OneSchema<any>, State, Context> = {
// Why not just use ParameterizedContext: When we tried to use ParameterizedContext
// directly, it was incompatible with Omit (omitting a single property resulted in
// a fully empty object).
& { state: State; }
& Context,
& { state: StateOfRouter<RouterType>; }
& ContextOfRouter<RouterType>,
) =>
| EndpointsOf<Schema>[Name]['Response']
| Promise<EndpointsOf<Schema>[Name]['Response']>;
Expand All @@ -78,18 +89,17 @@ export type IntrospectionConfig = {
*/
export type ImplementationConfig<
Schema extends OneSchema<any>,
State,
Context,
RouterType extends Router<any, any>,
> = {
/**
* The implementation of the API.
*/
implementation: ImplementationOf<Schema, State, Context>;
implementation: ImplementationOf<Schema, RouterType>;

/**
* The router to use for implementing the API.
*/
on: Router<State, Context>;
on: RouterType;

/**
* A function for parsing the correct data from the provided `data`,
Expand All @@ -106,7 +116,10 @@ export type ImplementationConfig<
* @returns A validated payload.
*/
parse: <Endpoint extends keyof EndpointsOf<Schema>>(
ctx: ParameterizedContext<State, Context>,
ctx: ParameterizedContext<
StateOfRouter<RouterType>,
ContextOfRouter<RouterType>
>,
params: { endpoint: Endpoint; schema: JSONSchema4; data: unknown },
) => Schema['Endpoints'][Endpoint]['Request'];

Expand All @@ -120,14 +133,17 @@ export type ImplementationConfig<
* @param schema The API OneSchema object.
* @param config The implementation configuration.
*/
export const implementSchema = <State, Context, Schema extends OneSchema<any>>(
export const implementSchema = <
Schema extends OneSchema<any>,
RouterType extends Router<any, any>,
>(
schema: Schema,
{
implementation,
parse,
on: router,
introspection,
}: ImplementationConfig<Schema, State, Context>,
}: ImplementationConfig<Schema, RouterType>,
): void => {
if (introspection) {
router.get(introspection.route, (ctx, next) => {
Expand All @@ -149,7 +165,10 @@ export const implementSchema = <State, Context, Schema extends OneSchema<any>>(
const [method, path] = endpoint.split(' ');

/** A shared route handler. */
const handler: Router.Middleware<State, Context> = async (ctx, next) => {
const handler: Router.Middleware<
StateOfRouter<RouterType>,
ContextOfRouter<RouterType>
> = async (ctx, next) => {
// 1. Validate the input data.
const requestSchema = schema.Endpoints[endpoint].Request;
if (requestSchema) {
Expand Down

0 comments on commit ced0a50

Please sign in to comment.