diff --git a/packages/express-wrapper/src/index.ts b/packages/express-wrapper/src/index.ts index 4fe1cf13..8a830bfc 100644 --- a/packages/express-wrapper/src/index.ts +++ b/packages/express-wrapper/src/index.ts @@ -4,122 +4,25 @@ */ import express from 'express'; -import * as PathReporter from 'io-ts/lib/PathReporter'; -import { - ApiSpec, - HttpResponseCodes, - HttpRoute, - RequestType, - ResponseType, -} from '@api-ts/io-ts-http'; +import { ApiSpec, HttpRoute } from '@api-ts/io-ts-http'; import { apiTsPathToExpress } from './path'; - -export type Function = ( - input: RequestType, -) => ResponseType | Promise>; -export type RouteStack = [ - ...express.RequestHandler[], - Function, -]; - -/** - * Dynamically assign a function name to avoid anonymous functions in stack traces - * https://stackoverflow.com/a/69465672 - */ -const createNamedFunction = void>( - name: string, - fn: F, -): F => Object.defineProperty(fn, 'name', { value: name }); - -const isKnownStatusCode = (code: string): code is keyof typeof HttpResponseCodes => - HttpResponseCodes.hasOwnProperty(code); - -const decodeRequestAndEncodeResponse = ( - apiName: string, - httpRoute: Route, - handler: Function, -): express.RequestHandler => { - return createNamedFunction( - 'decodeRequestAndEncodeResponse' + httpRoute.method + apiName, - async (req, res) => { - const maybeRequest = httpRoute.request.decode(req); - if (maybeRequest._tag === 'Left') { - console.log('Request failed to decode'); - const validationErrors = PathReporter.failure(maybeRequest.left); - const validationErrorMessage = validationErrors.join('\n'); - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.write(JSON.stringify({ error: validationErrorMessage })); - res.end(); - return; - } - - let rawResponse: ResponseType | undefined; - try { - rawResponse = await handler(maybeRequest.right); - } catch (err) { - console.warn('Error in route handler:', err); - res.statusCode = 500; - res.end(); - return; - } - - // Take the first match -- the implication is that the ordering of declared response - // codecs is significant! - for (const [statusCode, responseCodec] of Object.entries(httpRoute.response)) { - if (rawResponse.type !== statusCode) { - continue; - } - - if (!isKnownStatusCode(statusCode)) { - console.warn( - `Got unrecognized status code ${statusCode} for ${apiName} ${httpRoute.method}`, - ); - res.status(500); - res.end(); - return; - } - - // We expect that some route implementations may "beat the type - // system away with a stick" and return some unexpected values - // that fail to encode, so we catch errors here just in case - let response: unknown; - try { - response = responseCodec.encode(rawResponse.payload); - } catch (err) { - console.warn( - "Unable to encode route's return value, did you return the expected type?", - err, - ); - res.statusCode = 500; - res.end(); - return; - } - // DISCUSS: safer ways to handle this cast - res.writeHead(HttpResponseCodes[statusCode], { - 'Content-Type': 'application/json', - }); - res.write(JSON.stringify(response)); - res.end(); - return; - } - - // If we got here then we got an unexpected response - res.status(500); - res.end(); - }, - ); -}; +import { + decodeRequestAndEncodeResponse, + getMiddleware, + getServiceFunction, + RouteHandler, +} from './request'; const isHttpVerb = (verb: string): verb is 'get' | 'put' | 'post' | 'delete' => - ({ get: 1, put: 1, post: 1, delete: 1 }.hasOwnProperty(verb)); + verb === 'get' || verb === 'put' || verb === 'post' || verb === 'delete'; export function createServer( spec: Spec, configureExpressApplication: (app: express.Application) => { [ApiName in keyof Spec]: { - [Method in keyof Spec[ApiName]]: RouteStack; + [Method in keyof Spec[ApiName]]: RouteHandler; }; }, ) { @@ -134,14 +37,13 @@ export function createServer( continue; } const httpRoute: HttpRoute = resource[method]!; - const stack = routes[apiName]![method]!; - // Note: `stack` is guaranteed to be non-empty thanks to our function's type signature - const handler = decodeRequestAndEncodeResponse( + const routeHandler = routes[apiName]![method]!; + const expressRouteHandler = decodeRequestAndEncodeResponse( apiName, - httpRoute, - stack[stack.length - 1] as Function, + httpRoute as any, // TODO: wat + getServiceFunction(routeHandler), ); - const handlers = [...stack.slice(0, stack.length - 1), handler]; + const handlers = [...getMiddleware(routeHandler), expressRouteHandler]; const expressPath = apiTsPathToExpress(httpRoute.path); router[method](expressPath, handlers); diff --git a/packages/express-wrapper/src/request.ts b/packages/express-wrapper/src/request.ts new file mode 100644 index 00000000..f297020b --- /dev/null +++ b/packages/express-wrapper/src/request.ts @@ -0,0 +1,103 @@ +/** + * express-wrapper + * A simple, type-safe web server + */ + +import express from 'express'; +import * as t from 'io-ts'; +import * as PathReporter from 'io-ts/lib/PathReporter'; + +import { + HttpRoute, + HttpToKeyStatus, + KeyToHttpStatus, + RequestType, + ResponseType, +} from '@api-ts/io-ts-http'; + +type NumericOrKeyedResponseType = + | ResponseType + | { + [S in keyof R['response']]: S extends keyof HttpToKeyStatus + ? { + type: HttpToKeyStatus[S]; + payload: t.TypeOf; + } + : never; + }[keyof R['response']]; + +export type ServiceFunction = ( + input: RequestType, +) => NumericOrKeyedResponseType | Promise>; + +export type RouteHandler = + | ServiceFunction + | { middleware: express.RequestHandler[]; handler: ServiceFunction }; + +export const getServiceFunction = ( + routeHandler: RouteHandler, +): ServiceFunction => + 'handler' in routeHandler ? routeHandler.handler : routeHandler; + +export const getMiddleware = ( + routeHandler: RouteHandler, +): express.RequestHandler[] => + 'middleware' in routeHandler ? routeHandler.middleware : []; + +/** + * Dynamically assign a function name to avoid anonymous functions in stack traces + * https://stackoverflow.com/a/69465672 + */ +const createNamedFunction = void>( + name: string, + fn: F, +): F => Object.defineProperty(fn, 'name', { value: name }); + +export const decodeRequestAndEncodeResponse = ( + apiName: string, + httpRoute: Route, + handler: ServiceFunction, +): express.RequestHandler => { + return createNamedFunction( + 'decodeRequestAndEncodeResponse' + httpRoute.method + apiName, + async (req, res) => { + const maybeRequest = httpRoute.request.decode(req); + if (maybeRequest._tag === 'Left') { + console.log('Request failed to decode'); + const validationErrors = PathReporter.failure(maybeRequest.left); + const validationErrorMessage = validationErrors.join('\n'); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.write(JSON.stringify({ error: validationErrorMessage })); + res.end(); + return; + } + + let rawResponse: NumericOrKeyedResponseType | undefined; + try { + rawResponse = await handler(maybeRequest.right); + } catch (err) { + console.warn('Error in route handler:', err); + res.status(500).end(); + return; + } + + const { type, payload } = rawResponse; + const status = typeof type === 'number' ? type : (KeyToHttpStatus as any)[type]; + if (status === undefined) { + console.warn('Unknown status code returned'); + res.status(500).end(); + return; + } + const responseCodec = httpRoute.response[status]; + if (responseCodec === undefined || !responseCodec.is(payload)) { + console.warn( + "Unable to encode route's return value, did you return the expected type?", + ); + res.status(500).end(); + return; + } + + res.status(status).json(responseCodec.encode(payload)).end(); + }, + ); +}; diff --git a/packages/express-wrapper/test/server.test.ts b/packages/express-wrapper/test/server.test.ts index 4ac08d43..3e98c03c 100644 --- a/packages/express-wrapper/test/server.test.ts +++ b/packages/express-wrapper/test/server.test.ts @@ -5,7 +5,6 @@ import express from 'express'; import supertest from 'supertest'; import { ApiSpec, apiSpec, httpRequest, httpRoute, optional } from '@api-ts/io-ts-http'; -import { Response } from '@api-ts/response'; import { buildApiClient, supertestRequestFactory } from '@api-ts/superagent-wrapper'; import { createServer } from '../src'; @@ -24,17 +23,17 @@ const PutHello = httpRoute({ }), response: { // TODO: create prettier names for these codecs at the io-ts-http level - ok: t.type({ + 200: t.type({ message: t.string, appMiddlewareRan: t.boolean, routeMiddlewareRan: t.boolean, }), - invalidRequest: t.type({ + 400: t.type({ errors: t.string, }), - notFound: t.unknown, + 404: t.unknown, // DISCUSS: what if a response isn't listed here but shows up? - internalError: t.unknown, + 500: t.unknown, }, }); type PutHello = typeof PutHello; @@ -48,7 +47,7 @@ const GetHello = httpRoute({ }, }), response: { - ok: t.type({ + 200: t.type({ id: t.string, }), }, @@ -78,21 +77,31 @@ const CreateHelloWorld = async (parameters: { routeMiddlewareRan?: boolean; }) => { if (parameters.secretCode === 0) { - return Response.invalidRequest({ - errors: 'Please do not tell me zero! I will now explode', - }); + return { + type: 400, + payload: { + errors: 'Please do not tell me zero! I will now explode', + }, + } as const; } - return Response.ok({ - message: - parameters.secretCode === 42 - ? 'Everything you see from here is yours' - : "Who's there?", - appMiddlewareRan: parameters.appMiddlewareRan ?? false, - routeMiddlewareRan: parameters.routeMiddlewareRan ?? false, - }); + return { + type: 200, + payload: { + message: + parameters.secretCode === 42 + ? 'Everything you see from here is yours' + : "Who's there?", + appMiddlewareRan: parameters.appMiddlewareRan ?? false, + routeMiddlewareRan: parameters.routeMiddlewareRan ?? false, + }, + } as const; }; -const GetHelloWorld = async (params: { id: string }) => Response.ok(params); +const GetHelloWorld = async (params: { id: string }) => + ({ + type: 'ok', + payload: params, + } as const); test('should offer a delightful developer experience', async (t) => { const app = createServer(ApiSpec, (app: express.Application) => { @@ -101,8 +110,8 @@ test('should offer a delightful developer experience', async (t) => { app.use(appMiddleware); return { 'hello.world': { - put: [routeMiddleware, CreateHelloWorld], - get: [GetHelloWorld], + put: { middleware: [routeMiddleware], handler: CreateHelloWorld }, + get: GetHelloWorld, }, }; }); @@ -130,8 +139,8 @@ test('should handle io-ts-http formatted path parameters', async (t) => { app.use(appMiddleware); return { 'hello.world': { - put: [routeMiddleware, CreateHelloWorld], - get: [GetHelloWorld], + put: { middleware: [routeMiddleware], handler: CreateHelloWorld }, + get: GetHelloWorld, }, }; }); @@ -154,8 +163,8 @@ test('should invoke app-level middleware', async (t) => { app.use(appMiddleware); return { 'hello.world': { - put: [CreateHelloWorld], - get: [GetHelloWorld], + put: CreateHelloWorld, + get: GetHelloWorld, }, }; }); @@ -177,8 +186,8 @@ test('should invoke route-level middleware', async (t) => { app.use(express.json()); return { 'hello.world': { - put: [routeMiddleware, CreateHelloWorld], - get: [GetHelloWorld], + put: { middleware: [routeMiddleware], handler: CreateHelloWorld }, + get: GetHelloWorld, }, }; }); @@ -200,8 +209,8 @@ test('should infer status code from response type', async (t) => { app.use(express.json()); return { 'hello.world': { - put: [CreateHelloWorld], - get: [GetHelloWorld], + put: CreateHelloWorld, + get: GetHelloWorld, }, }; }); @@ -223,8 +232,8 @@ test('should return a 400 when request fails to decode', async (t) => { app.use(express.json()); return { 'hello.world': { - put: [CreateHelloWorld], - get: [GetHelloWorld], + put: CreateHelloWorld, + get: GetHelloWorld, }, }; }); diff --git a/packages/io-ts-http/docs/httpRoute.md b/packages/io-ts-http/docs/httpRoute.md index 8ea159f9..bbd7abf5 100644 --- a/packages/io-ts-http/docs/httpRoute.md +++ b/packages/io-ts-http/docs/httpRoute.md @@ -21,7 +21,7 @@ httpRoute({ }, }), response: { - ok: t.string, + 200: t.string, }, }); ``` @@ -38,7 +38,7 @@ httpRoute({ }, }), response: { - ok: t.string, + 200: t.string, }, }); ``` @@ -64,7 +64,7 @@ const Route = httpRoute({ }, }), response: { - ok: t.string, + 200: t.string, }, }); @@ -86,8 +86,8 @@ const response: string = await routeApiClient({ id: 1337 }); ### `response` Declares the potential responses that a route may return along with the codec associated -to each response. The possible response keys can be found in the `io-ts-response` -package. Incoming responses are assumed to be JSON. +to each response. Response keys correspond to HTTP status codes. Incoming responses are +assumed to be JSON. ```typescript const Route = httpRoute({ @@ -95,13 +95,13 @@ const Route = httpRoute({ method: 'GET', request: httpRequest({}), response: { - ok: t.type({ + 200: t.type({ foo: t.string, }), - notFound: t.type({ + 404: t.type({ message: t.string, }), - invalidRequest: t.type({ + 400: t.type({ message: t.string, }), }, @@ -154,7 +154,7 @@ const StringBodyRoute = httpRoute({ }), ]), response: { - ok: t.string, + 200: t.string, }, }); @@ -191,7 +191,7 @@ const UnionRoute = httpRoute({ }), ]), response: { - ok: string, + 200: string, }, }); diff --git a/packages/io-ts-http/src/httpResponse.ts b/packages/io-ts-http/src/httpResponse.ts index bafc380e..cbbb8996 100644 --- a/packages/io-ts-http/src/httpResponse.ts +++ b/packages/io-ts-http/src/httpResponse.ts @@ -1,44 +1,9 @@ import * as t from 'io-ts'; -import { Status } from '@api-ts/response'; - export type HttpResponse = { - [K in Status]?: t.Mixed; + [K: number]: t.Mixed; }; -export type KnownResponses = { - [K in keyof Response]: K extends Status - ? undefined extends Response[K] - ? never - : K - : never; -}[keyof Response]; - -export const HttpResponseCodes = { - ok: 200, - invalidRequest: 400, - unauthenticated: 401, - permissionDenied: 403, - notFound: 404, - rateLimitExceeded: 429, - internalError: 500, - serviceUnavailable: 503, -} as const; - -export type HttpResponseCodes = typeof HttpResponseCodes; - -// Create a type-level assertion that the HttpResponseCodes map contains every key -// in the Status union of string literals, and no unexpected keys. Violations of -// this assertion will cause compile-time errors. -// -// Thanks to https://stackoverflow.com/a/67027737 -type ShapeOf = Record; -type AssertKeysEqual, Y extends ShapeOf> = never; -type _AssertHttpStatusCodeIsDefinedForAllResponses = AssertKeysEqual< - { [K in Status]: number }, - HttpResponseCodes ->; - export type ResponseTypeForStatus< Response extends HttpResponse, S extends keyof Response, diff --git a/packages/io-ts-http/src/httpRoute.ts b/packages/io-ts-http/src/httpRoute.ts index 509a4251..5589c958 100644 --- a/packages/io-ts-http/src/httpRoute.ts +++ b/packages/io-ts-http/src/httpRoute.ts @@ -1,8 +1,7 @@ import * as t from 'io-ts'; -import { HttpResponse, KnownResponses } from './httpResponse'; -import { httpRequest, HttpRequestCodec } from './httpRequest'; -import { Status } from '@api-ts/response'; +import { HttpResponse } from './httpResponse'; +import { HttpRequestCodec } from './httpRequest'; export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE'; @@ -13,17 +12,15 @@ export type HttpRoute = { readonly response: HttpResponse; }; -type ResponseItem = Codec extends t.Mixed - ? { - type: Status; - payload: t.TypeOf; - } - : never; - export type RequestType = t.TypeOf; export type ResponseType = { - [K in KnownResponses]: ResponseItem; -}[KnownResponses]; + [K in keyof T['response']]: T['response'][K] extends t.Mixed + ? { + type: K; + payload: t.TypeOf; + } + : never; +}[keyof T['response']]; export type ApiSpec = { [Key: string]: { diff --git a/packages/io-ts-http/src/index.ts b/packages/io-ts-http/src/index.ts index 80165425..9ef6e249 100644 --- a/packages/io-ts-http/src/index.ts +++ b/packages/io-ts-http/src/index.ts @@ -8,3 +8,4 @@ export * from './httpResponse'; export * from './httpRequest'; export * from './httpRoute'; export * from './queryParams'; +export * from './statusCode'; diff --git a/packages/io-ts-http/src/statusCode.ts b/packages/io-ts-http/src/statusCode.ts new file mode 100644 index 00000000..a7185191 --- /dev/null +++ b/packages/io-ts-http/src/statusCode.ts @@ -0,0 +1,27 @@ +// TODO: Enforce consistency at the type level + +export const HttpToKeyStatus = { + 200: 'ok', + 400: 'invalidRequest', + 401: 'unauthenticated', + 403: 'permissionDenied', + 404: 'notFound', + 429: 'rateLimitExceeded', + 500: 'internalError', + 503: 'serviceUnavailable', +} as const; + +export type HttpToKeyStatus = typeof HttpToKeyStatus; + +export const KeyToHttpStatus = { + ok: 200, + invalidRequest: 400, + unauthenticated: 401, + permissionDenied: 403, + notFound: 404, + rateLimitExceeded: 429, + internalError: 500, + serviceUnavailable: 503, +} as const; + +export type KeyToHttpStatus = typeof KeyToHttpStatus; diff --git a/packages/openapi-generator/corpus/test-array-property.ts b/packages/openapi-generator/corpus/test-array-property.ts index a64ae84c..b8b0d1cc 100644 --- a/packages/openapi-generator/corpus/test-array-property.ts +++ b/packages/openapi-generator/corpus/test-array-property.ts @@ -10,7 +10,7 @@ const MyRoute = h.httpRoute({ method: 'GET', request: h.httpRequest({}), response: { - ok: t.array(t.string), + 200: t.array(t.string), }, }); diff --git a/packages/openapi-generator/corpus/test-boolean-literal.ts b/packages/openapi-generator/corpus/test-boolean-literal.ts index c9cacb2d..3b15fa20 100644 --- a/packages/openapi-generator/corpus/test-boolean-literal.ts +++ b/packages/openapi-generator/corpus/test-boolean-literal.ts @@ -10,7 +10,7 @@ const MyRoute = h.httpRoute({ method: 'GET', request: h.httpRequest({}), response: { - ok: t.literal(false), + 200: t.literal(false), }, }); diff --git a/packages/openapi-generator/corpus/test-discriminated-union.ts b/packages/openapi-generator/corpus/test-discriminated-union.ts index 9d50117a..73017102 100644 --- a/packages/openapi-generator/corpus/test-discriminated-union.ts +++ b/packages/openapi-generator/corpus/test-discriminated-union.ts @@ -10,7 +10,10 @@ const MyRoute = h.httpRoute({ method: 'GET', request: h.httpRequest({}), response: { - ok: t.union([t.type({ key: t.literal('foo') }), t.type({ key: t.literal('bar') })]), + 200: t.union([ + t.type({ key: t.literal('foo') }), + t.type({ key: t.literal('bar') }), + ]), }, } as const); diff --git a/packages/openapi-generator/corpus/test-intersection-flattening.ts b/packages/openapi-generator/corpus/test-intersection-flattening.ts index 2e6b1161..9759fd18 100644 --- a/packages/openapi-generator/corpus/test-intersection-flattening.ts +++ b/packages/openapi-generator/corpus/test-intersection-flattening.ts @@ -10,7 +10,7 @@ const MyRoute = h.httpRoute({ method: 'GET', request: h.httpRequest({}), response: { - ok: t.intersection([t.type({ foo: t.string }), t.type({ bar: t.string })]), + 200: t.intersection([t.type({ foo: t.string }), t.type({ bar: t.string })]), }, }); diff --git a/packages/openapi-generator/corpus/test-multi-route.ts b/packages/openapi-generator/corpus/test-multi-route.ts index b2210c19..b2fe2c0e 100644 --- a/packages/openapi-generator/corpus/test-multi-route.ts +++ b/packages/openapi-generator/corpus/test-multi-route.ts @@ -22,7 +22,7 @@ const FirstRoute = h.httpRoute({ }, }), response: { - ok: t.string, + 200: t.string, }, } as const); @@ -42,7 +42,7 @@ const SecondRoute = h.httpRoute({ }, }), response: { - ok: t.string, + 200: t.string, }, }); diff --git a/packages/openapi-generator/corpus/test-multi-union.ts b/packages/openapi-generator/corpus/test-multi-union.ts index 2ae04d7e..0b6259fe 100644 --- a/packages/openapi-generator/corpus/test-multi-union.ts +++ b/packages/openapi-generator/corpus/test-multi-union.ts @@ -10,7 +10,7 @@ const MyRoute = h.httpRoute({ method: 'GET', request: h.httpRequest({}), response: { - ok: t.union([t.literal('foo'), t.literal(42), t.type({ message: t.string })]), + 200: t.union([t.literal('foo'), t.literal(42), t.type({ message: t.string })]), }, } as const); diff --git a/packages/openapi-generator/corpus/test-null-param.ts b/packages/openapi-generator/corpus/test-null-param.ts index 08ba7285..8389ab98 100644 --- a/packages/openapi-generator/corpus/test-null-param.ts +++ b/packages/openapi-generator/corpus/test-null-param.ts @@ -10,7 +10,7 @@ const MyRoute = h.httpRoute({ method: 'GET', request: h.httpRequest({}), response: { - ok: t.null, + 200: t.null, }, }); diff --git a/packages/openapi-generator/corpus/test-optional-property.ts b/packages/openapi-generator/corpus/test-optional-property.ts index 03ab0c47..12f8a4c9 100644 --- a/packages/openapi-generator/corpus/test-optional-property.ts +++ b/packages/openapi-generator/corpus/test-optional-property.ts @@ -15,7 +15,7 @@ const MyRoute = h.httpRoute({ }, }), response: { - ok: t.string, + 200: t.string, }, }); diff --git a/packages/openapi-generator/corpus/test-record-type.ts b/packages/openapi-generator/corpus/test-record-type.ts index 3c2164b8..1553ab83 100644 --- a/packages/openapi-generator/corpus/test-record-type.ts +++ b/packages/openapi-generator/corpus/test-record-type.ts @@ -10,7 +10,7 @@ const MyRoute = h.httpRoute({ method: 'GET', request: h.httpRequest({}), response: { - ok: t.record(t.string, t.string), + 200: t.record(t.string, t.string), }, }); diff --git a/packages/openapi-generator/corpus/test-single-route-multi-method.ts b/packages/openapi-generator/corpus/test-single-route-multi-method.ts index 991001e9..8ace87b2 100644 --- a/packages/openapi-generator/corpus/test-single-route-multi-method.ts +++ b/packages/openapi-generator/corpus/test-single-route-multi-method.ts @@ -16,7 +16,7 @@ const FirstRoute = h.httpRoute({ }, }), response: { - ok: t.string, + 200: t.string, }, } as const); @@ -35,7 +35,7 @@ const SecondRoute = h.httpRoute({ }, }), response: { - ok: t.string, + 200: t.string, }, }); diff --git a/packages/openapi-generator/corpus/test-single-route.ts b/packages/openapi-generator/corpus/test-single-route.ts index 0d3d4439..f7312377 100644 --- a/packages/openapi-generator/corpus/test-single-route.ts +++ b/packages/openapi-generator/corpus/test-single-route.ts @@ -39,8 +39,8 @@ const MyRoute = h.httpRoute({ }, }), response: { - ok: t.number, - invalidRequest: t.type({ foo: t.string, bar: t.number }), + 200: t.number, + 400: t.type({ foo: t.string, bar: t.number }), }, }); diff --git a/packages/openapi-generator/corpus/test-string-union.ts b/packages/openapi-generator/corpus/test-string-union.ts index b13f295e..a86cc082 100644 --- a/packages/openapi-generator/corpus/test-string-union.ts +++ b/packages/openapi-generator/corpus/test-string-union.ts @@ -10,7 +10,7 @@ const MyRoute = h.httpRoute({ method: 'GET', request: h.httpRequest({}), response: { - ok: t.keyof({ foo: 1, bar: 1, baz: 1 }), + 200: t.keyof({ foo: 1, bar: 1, baz: 1 }), }, } as const); diff --git a/packages/openapi-generator/corpus/test-unknown-property.ts b/packages/openapi-generator/corpus/test-unknown-property.ts index 8a331d63..11c540e8 100644 --- a/packages/openapi-generator/corpus/test-unknown-property.ts +++ b/packages/openapi-generator/corpus/test-unknown-property.ts @@ -15,7 +15,7 @@ const MyRoute = h.httpRoute({ }, }), response: { - ok: t.type({ + 200: t.type({ foo: t.unknown, }), }, diff --git a/packages/openapi-generator/corpus/test-version-tag.ts b/packages/openapi-generator/corpus/test-version-tag.ts index 1e230f7b..be50af66 100644 --- a/packages/openapi-generator/corpus/test-version-tag.ts +++ b/packages/openapi-generator/corpus/test-version-tag.ts @@ -10,7 +10,7 @@ const MyRoute = h.httpRoute({ method: 'GET', request: h.httpRequest({}), response: { - ok: t.string, + 200: t.string, }, }); diff --git a/packages/openapi-generator/src/route.ts b/packages/openapi-generator/src/route.ts index fb4da4be..7bfe68ad 100644 --- a/packages/openapi-generator/src/route.ts +++ b/packages/openapi-generator/src/route.ts @@ -1,4 +1,3 @@ -import { HttpResponseCodes } from '@api-ts/io-ts-http'; import { parse as parseComment } from 'comment-parser'; import { flow, pipe } from 'fp-ts/function'; import * as E from 'fp-ts/Either'; @@ -247,11 +246,8 @@ export const schemaForRouteNode = (memo: any) => (node: Expression { const name = sym.getName(); - const statusCode = HttpResponseCodes.hasOwnProperty(name) - ? HttpResponseCodes[name as keyof typeof HttpResponseCodes] - : undefined; return RE.fromEither( - E.fromNullable(`unknown response type '${name}`)(statusCode), + E.fromNullable('undefined response code name')(name), ); }), ), diff --git a/packages/superagent-wrapper/src/request.ts b/packages/superagent-wrapper/src/request.ts index 337218b5..515e62f8 100644 --- a/packages/superagent-wrapper/src/request.ts +++ b/packages/superagent-wrapper/src/request.ts @@ -5,16 +5,15 @@ import type { Response, SuperAgent, SuperAgentRequest } from 'superagent'; import type { SuperTest } from 'supertest'; import { URL } from 'url'; import { pipe } from 'fp-ts/function'; -import { Status } from '@api-ts/response'; type SuccessfulResponses = { - [R in h.KnownResponses]: { - status: h.HttpResponseCodes[R]; + [R in keyof Route['response']]: { + status: R; error?: undefined; body: h.ResponseTypeForStatus; original: Response; }; -}[h.KnownResponses]; +}[keyof Route['response']]; type DecodedResponse = | SuccessfulResponses @@ -29,14 +28,12 @@ const decodedResponse = (res: DecodedResponse) type ExpectedDecodedResponse< Route extends h.HttpRoute, - StatusCode extends h.HttpResponseCodes[h.KnownResponses], + StatusCode extends keyof Route['response'], > = DecodedResponse & { status: StatusCode }; type PatchedRequest = Req & { decode: () => Promise>; - decodeExpecting: < - StatusCode extends h.HttpResponseCodes[h.KnownResponses], - >( + decodeExpecting: ( status: StatusCode, ) => Promise>; }; @@ -84,7 +81,7 @@ export const supertestRequestFactory = return supertest[method](path); }; -const hasCodecForStatus = ( +const hasCodecForStatus = ( responses: h.HttpResponse, status: S, ): responses is { [K in S]: t.Mixed } => { @@ -99,24 +96,7 @@ const patchRequest = ( patchedReq.decode = () => req.then((res) => { - const { body, status: statusCode } = res; - - let status: Status | undefined; - // DISCUSS: Should we have this as a preprocessed const in io-ts-http? - for (const [name, code] of Object.entries(h.HttpResponseCodes)) { - if (statusCode === code) { - status = name as Status; - break; - } - } - if (status === undefined) { - return decodedResponse({ - status: 'decodeError', - error: `Unknown status code ${statusCode}`, - body, - original: res, - }); - } + const { body, status } = res; if (!hasCodecForStatus(route.response, status)) { return decodedResponse({ @@ -131,9 +111,7 @@ const patchRequest = ( route.response[status].decode(res.body), E.map((body) => decodedResponse({ - status: statusCode as h.HttpResponseCodes[h.KnownResponses< - Route['response'] - >], + status, body, original: res, } as SuccessfulResponses), @@ -150,9 +128,7 @@ const patchRequest = ( ); }); - patchedReq.decodeExpecting = < - StatusCode extends h.HttpResponseCodes[h.KnownResponses], - >( + patchedReq.decodeExpecting = ( status: StatusCode, ) => patchedReq.decode().then((res) => { diff --git a/packages/superagent-wrapper/test/request.test.ts b/packages/superagent-wrapper/test/request.test.ts index 13b752b8..52d9cea0 100644 --- a/packages/superagent-wrapper/test/request.test.ts +++ b/packages/superagent-wrapper/test/request.test.ts @@ -25,7 +25,7 @@ const PostTestRoute = h.httpRoute({ }, }), response: { - ok: t.type({ + 200: t.type({ id: t.number, foo: t.string, bar: t.number, @@ -39,7 +39,7 @@ const HeaderGetTestRoute = h.httpRoute({ method: 'GET', request: h.httpRequest({}), response: { - ok: t.type({ + 200: t.type({ value: t.string, }), }, @@ -79,7 +79,7 @@ testApp.post('/test/:id', (req, res) => { error: 'bad request', }); } else { - const response = PostTestRoute.response['ok'].encode({ + const response = PostTestRoute.response[200].encode({ ...params, baz: true, }); @@ -89,7 +89,7 @@ testApp.post('/test/:id', (req, res) => { testApp.get(HeaderGetTestRoute.path, (req, res) => { res.send( - HeaderGetTestRoute.response['ok'].encode({ + HeaderGetTestRoute.response[200].encode({ value: String(req.headers['x-custom'] ?? ''), }), );