Skip to content

Commit 4e191cf

Browse files
committed
feat: factor response library out of io-ts-http
1 parent 94b374d commit 4e191cf

23 files changed

+76
-153
lines changed

package-lock.json

-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/express-wrapper/src/index.ts

+5-25
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,7 @@
66
import express from 'express';
77
import * as PathReporter from 'io-ts/lib/PathReporter';
88

9-
import {
10-
ApiSpec,
11-
HttpResponseCodes,
12-
HttpRoute,
13-
RequestType,
14-
ResponseType,
15-
} from '@api-ts/io-ts-http';
9+
import { ApiSpec, HttpRoute, RequestType, ResponseType } from '@api-ts/io-ts-http';
1610

1711
import { apiTsPathToExpress } from './path';
1812

@@ -33,9 +27,6 @@ const createNamedFunction = <F extends (...args: any) => void>(
3327
fn: F,
3428
): F => Object.defineProperty(fn, 'name', { value: name });
3529

36-
const isKnownStatusCode = (code: string): code is keyof typeof HttpResponseCodes =>
37-
HttpResponseCodes.hasOwnProperty(code);
38-
3930
const decodeRequestAndEncodeResponse = <Route extends HttpRoute>(
4031
apiName: string,
4132
httpRoute: Route,
@@ -65,22 +56,12 @@ const decodeRequestAndEncodeResponse = <Route extends HttpRoute>(
6556
return;
6657
}
6758

68-
// Take the first match -- the implication is that the ordering of declared response
69-
// codecs is significant!
70-
for (const [statusCode, responseCodec] of Object.entries(httpRoute.response)) {
71-
if (rawResponse.type !== statusCode) {
59+
for (const [statusCodeStr, responseCodec] of Object.entries(httpRoute.response)) {
60+
const statusCode = Number(statusCodeStr);
61+
if (!Number.isInteger(statusCode) || rawResponse.type !== statusCode) {
7262
continue;
7363
}
7464

75-
if (!isKnownStatusCode(statusCode)) {
76-
console.warn(
77-
`Got unrecognized status code ${statusCode} for ${apiName} ${httpRoute.method}`,
78-
);
79-
res.status(500);
80-
res.end();
81-
return;
82-
}
83-
8465
// We expect that some route implementations may "beat the type
8566
// system away with a stick" and return some unexpected values
8667
// that fail to encode, so we catch errors here just in case
@@ -96,8 +77,7 @@ const decodeRequestAndEncodeResponse = <Route extends HttpRoute>(
9677
res.end();
9778
return;
9879
}
99-
// DISCUSS: safer ways to handle this cast
100-
res.writeHead(HttpResponseCodes[statusCode], {
80+
res.writeHead(statusCode, {
10181
'Content-Type': 'application/json',
10282
});
10383
res.write(JSON.stringify(response));

packages/express-wrapper/test/server.test.ts

+27-18
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import express from 'express';
55
import supertest from 'supertest';
66

77
import { ApiSpec, apiSpec, httpRequest, httpRoute, optional } from '@api-ts/io-ts-http';
8-
import { Response } from '@api-ts/response';
98
import { buildApiClient, supertestRequestFactory } from '@api-ts/superagent-wrapper';
109

1110
import { createServer } from '../src';
@@ -24,17 +23,17 @@ const PutHello = httpRoute({
2423
}),
2524
response: {
2625
// TODO: create prettier names for these codecs at the io-ts-http level
27-
ok: t.type({
26+
200: t.type({
2827
message: t.string,
2928
appMiddlewareRan: t.boolean,
3029
routeMiddlewareRan: t.boolean,
3130
}),
32-
invalidRequest: t.type({
31+
400: t.type({
3332
errors: t.string,
3433
}),
35-
notFound: t.unknown,
34+
404: t.unknown,
3635
// DISCUSS: what if a response isn't listed here but shows up?
37-
internalError: t.unknown,
36+
500: t.unknown,
3837
},
3938
});
4039
type PutHello = typeof PutHello;
@@ -48,7 +47,7 @@ const GetHello = httpRoute({
4847
},
4948
}),
5049
response: {
51-
ok: t.type({
50+
200: t.type({
5251
id: t.string,
5352
}),
5453
},
@@ -78,21 +77,31 @@ const CreateHelloWorld = async (parameters: {
7877
routeMiddlewareRan?: boolean;
7978
}) => {
8079
if (parameters.secretCode === 0) {
81-
return Response.invalidRequest({
82-
errors: 'Please do not tell me zero! I will now explode',
83-
});
80+
return {
81+
type: 400,
82+
payload: {
83+
errors: 'Please do not tell me zero! I will now explode',
84+
},
85+
} as const;
8486
}
85-
return Response.ok({
86-
message:
87-
parameters.secretCode === 42
88-
? 'Everything you see from here is yours'
89-
: "Who's there?",
90-
appMiddlewareRan: parameters.appMiddlewareRan ?? false,
91-
routeMiddlewareRan: parameters.routeMiddlewareRan ?? false,
92-
});
87+
return {
88+
type: 200,
89+
payload: {
90+
message:
91+
parameters.secretCode === 42
92+
? 'Everything you see from here is yours'
93+
: "Who's there?",
94+
appMiddlewareRan: parameters.appMiddlewareRan ?? false,
95+
routeMiddlewareRan: parameters.routeMiddlewareRan ?? false,
96+
},
97+
} as const;
9398
};
9499

95-
const GetHelloWorld = async (params: { id: string }) => Response.ok(params);
100+
const GetHelloWorld = async (params: { id: string }) =>
101+
({
102+
type: 200,
103+
payload: params,
104+
} as const);
96105

97106
test('should offer a delightful developer experience', async (t) => {
98107
const app = createServer(ApiSpec, (app: express.Application) => {

packages/io-ts-http/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
"test": "nyc --reporter=lcov --reporter=text --reporter=json-summary mocha test/**/*.test.ts --require ts-node/register --exit"
1818
},
1919
"dependencies": {
20-
"@api-ts/response": "0.0.0-semantically-released",
2120
"fp-ts": "2.11.8",
2221
"io-ts": "2.1.3",
2322
"io-ts-types": "0.5.16",

packages/io-ts-http/src/httpResponse.ts

+1-36
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,9 @@
11
import * as t from 'io-ts';
22

3-
import { Status } from '@api-ts/response';
4-
53
export type HttpResponse = {
6-
[K in Status]?: t.Mixed;
4+
[K: number]: t.Mixed;
75
};
86

9-
export type KnownResponses<Response extends HttpResponse> = {
10-
[K in keyof Response]: K extends Status
11-
? undefined extends Response[K]
12-
? never
13-
: K
14-
: never;
15-
}[keyof Response];
16-
17-
export const HttpResponseCodes = {
18-
ok: 200,
19-
invalidRequest: 400,
20-
unauthenticated: 401,
21-
permissionDenied: 403,
22-
notFound: 404,
23-
rateLimitExceeded: 429,
24-
internalError: 500,
25-
serviceUnavailable: 503,
26-
} as const;
27-
28-
export type HttpResponseCodes = typeof HttpResponseCodes;
29-
30-
// Create a type-level assertion that the HttpResponseCodes map contains every key
31-
// in the Status union of string literals, and no unexpected keys. Violations of
32-
// this assertion will cause compile-time errors.
33-
//
34-
// Thanks to https://stackoverflow.com/a/67027737
35-
type ShapeOf<T> = Record<keyof T, any>;
36-
type AssertKeysEqual<X extends ShapeOf<Y>, Y extends ShapeOf<X>> = never;
37-
type _AssertHttpStatusCodeIsDefinedForAllResponses = AssertKeysEqual<
38-
{ [K in Status]: number },
39-
HttpResponseCodes
40-
>;
41-
427
export type ResponseTypeForStatus<
438
Response extends HttpResponse,
449
S extends keyof Response,

packages/io-ts-http/src/httpRoute.ts

+9-12
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import * as t from 'io-ts';
22

3-
import { HttpResponse, KnownResponses } from './httpResponse';
4-
import { httpRequest, HttpRequestCodec } from './httpRequest';
5-
import { Status } from '@api-ts/response';
3+
import { HttpResponse } from './httpResponse';
4+
import { HttpRequestCodec } from './httpRequest';
65

76
export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
87

@@ -13,17 +12,15 @@ export type HttpRoute = {
1312
readonly response: HttpResponse;
1413
};
1514

16-
type ResponseItem<Status, Codec extends t.Mixed | undefined> = Codec extends t.Mixed
17-
? {
18-
type: Status;
19-
payload: t.TypeOf<Codec>;
20-
}
21-
: never;
22-
2315
export type RequestType<T extends HttpRoute> = t.TypeOf<T['request']>;
2416
export type ResponseType<T extends HttpRoute> = {
25-
[K in KnownResponses<T['response']>]: ResponseItem<K, T['response'][K]>;
26-
}[KnownResponses<T['response']>];
17+
[K in keyof T['response']]: T['response'][K] extends t.Mixed
18+
? {
19+
type: K;
20+
payload: t.TypeOf<T['response'][K]>;
21+
}
22+
: never;
23+
}[keyof T['response']];
2724

2825
export type ApiSpec = {
2926
[Key: string]: {

packages/openapi-generator/corpus/test-array-property.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const MyRoute = h.httpRoute({
1010
method: 'GET',
1111
request: h.httpRequest({}),
1212
response: {
13-
ok: t.array(t.string),
13+
200: t.array(t.string),
1414
},
1515
});
1616

packages/openapi-generator/corpus/test-boolean-literal.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const MyRoute = h.httpRoute({
1010
method: 'GET',
1111
request: h.httpRequest({}),
1212
response: {
13-
ok: t.literal(false),
13+
200: t.literal(false),
1414
},
1515
});
1616

packages/openapi-generator/corpus/test-discriminated-union.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ const MyRoute = h.httpRoute({
1010
method: 'GET',
1111
request: h.httpRequest({}),
1212
response: {
13-
ok: t.union([t.type({ key: t.literal('foo') }), t.type({ key: t.literal('bar') })]),
13+
200: t.union([
14+
t.type({ key: t.literal('foo') }),
15+
t.type({ key: t.literal('bar') }),
16+
]),
1417
},
1518
} as const);
1619

packages/openapi-generator/corpus/test-intersection-flattening.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const MyRoute = h.httpRoute({
1010
method: 'GET',
1111
request: h.httpRequest({}),
1212
response: {
13-
ok: t.intersection([t.type({ foo: t.string }), t.type({ bar: t.string })]),
13+
200: t.intersection([t.type({ foo: t.string }), t.type({ bar: t.string })]),
1414
},
1515
});
1616

packages/openapi-generator/corpus/test-multi-route.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const FirstRoute = h.httpRoute({
2222
},
2323
}),
2424
response: {
25-
ok: t.string,
25+
200: t.string,
2626
},
2727
} as const);
2828

@@ -42,7 +42,7 @@ const SecondRoute = h.httpRoute({
4242
},
4343
}),
4444
response: {
45-
ok: t.string,
45+
200: t.string,
4646
},
4747
});
4848

packages/openapi-generator/corpus/test-multi-union.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const MyRoute = h.httpRoute({
1010
method: 'GET',
1111
request: h.httpRequest({}),
1212
response: {
13-
ok: t.union([t.literal('foo'), t.literal(42), t.type({ message: t.string })]),
13+
200: t.union([t.literal('foo'), t.literal(42), t.type({ message: t.string })]),
1414
},
1515
} as const);
1616

packages/openapi-generator/corpus/test-null-param.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const MyRoute = h.httpRoute({
1010
method: 'GET',
1111
request: h.httpRequest({}),
1212
response: {
13-
ok: t.null,
13+
200: t.null,
1414
},
1515
});
1616

packages/openapi-generator/corpus/test-optional-property.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const MyRoute = h.httpRoute({
1515
},
1616
}),
1717
response: {
18-
ok: t.string,
18+
200: t.string,
1919
},
2020
});
2121

packages/openapi-generator/corpus/test-record-type.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const MyRoute = h.httpRoute({
1010
method: 'GET',
1111
request: h.httpRequest({}),
1212
response: {
13-
ok: t.record(t.string, t.string),
13+
200: t.record(t.string, t.string),
1414
},
1515
});
1616

packages/openapi-generator/corpus/test-single-route-multi-method.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const FirstRoute = h.httpRoute({
1616
},
1717
}),
1818
response: {
19-
ok: t.string,
19+
200: t.string,
2020
},
2121
} as const);
2222

@@ -35,7 +35,7 @@ const SecondRoute = h.httpRoute({
3535
},
3636
}),
3737
response: {
38-
ok: t.string,
38+
200: t.string,
3939
},
4040
});
4141

packages/openapi-generator/corpus/test-single-route.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ const MyRoute = h.httpRoute({
3939
},
4040
}),
4141
response: {
42-
ok: t.number,
43-
invalidRequest: t.type({ foo: t.string, bar: t.number }),
42+
200: t.number,
43+
400: t.type({ foo: t.string, bar: t.number }),
4444
},
4545
});
4646

packages/openapi-generator/corpus/test-string-union.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const MyRoute = h.httpRoute({
1010
method: 'GET',
1111
request: h.httpRequest({}),
1212
response: {
13-
ok: t.keyof({ foo: 1, bar: 1, baz: 1 }),
13+
200: t.keyof({ foo: 1, bar: 1, baz: 1 }),
1414
},
1515
} as const);
1616

packages/openapi-generator/corpus/test-unknown-property.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const MyRoute = h.httpRoute({
1515
},
1616
}),
1717
response: {
18-
ok: t.type({
18+
200: t.type({
1919
foo: t.unknown,
2020
}),
2121
},

packages/openapi-generator/corpus/test-version-tag.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const MyRoute = h.httpRoute({
1010
method: 'GET',
1111
request: h.httpRequest({}),
1212
response: {
13-
ok: t.string,
13+
200: t.string,
1414
},
1515
});
1616

0 commit comments

Comments
 (0)