1/**
2 * Tests `HttpApi` implementations through an in-memory generated client.
3 *
4 * The helpers use the same request encoding, routing, response encoding, and
5 * client decoding as the normal `HttpApiBuilder` and `HttpApiClient` pipeline,
6 * but they do not start an HTTP server. This is useful for focused handler
7 * tests, schema round trips, middleware behavior, and typed client calls.
8 *
9 * @stability unstable
10 * @since 4.0.0
11 */
12import * as Context from "../Context.ts"
13import * as Effect from "../Effect.ts"
14import type { FileSystem } from "../FileSystem.ts"
15import type { Generator } from "../http/Etag.ts"
16import * as HttpClient from "../http/HttpClient.ts"
17import type { HttpPlatform } from "../http/HttpPlatform.ts"
18import * as HttpRouter from "../http/HttpRouter.ts"
19import * as HttpServerRequest from "../http/HttpServerRequest.ts"
20import * as HttpServerResponse from "../http/HttpServerResponse.ts"
21import * as preResponseHandler from "../http/internal/preResponseHandler.ts"
22import * as Layer from "../Layer.ts"
23import type { Path } from "../Path.ts"
24import type { Scope } from "../Scope.ts"
25import type * as HttpApi from "./HttpApi.ts"
26import type { HandlerRuntime } from "./HttpApiBuilder.ts"
27import * as HttpApiBuilder from "./HttpApiBuilder.ts"
28import * as HttpApiClient from "./HttpApiClient.ts"
29import type * as HttpApiEndpoint from "./HttpApiEndpoint.ts"
30import type * as HttpApiGroup from "./HttpApiGroup.ts"
31
32/**
33 * Creates an in-memory client for testing selected groups of an `HttpApi`.
34 *
35 * **Details**
36 *
37 * Handlers for the selected groups are taken from the environment; unselected
38 * groups are wired with placeholder handlers that fail if called.
39 *
40 * @stability unstable
41 * @category testing
42 * @since 4.0.0
43 */
44export const groups = Effect.fnUntraced(function*<
45 ApiId extends string,
46 Groups extends HttpApiGroup.Constraint,
47 const Identifiers extends ReadonlyArray<HttpApiGroup.Identifier<Groups>>,
48 SelectedGroups extends HttpApiGroup.Constraint = HttpApiGroup.WithIdentifier<Groups, Identifiers[number]>
49>(
50 api: HttpApi.HttpApi<ApiId, Groups>,
51 groupIdentifiers: Identifiers,
52 options?: {
53 readonly baseUrl?: string | URL | undefined
54 }
55): Effect.fn.Return<
56 HttpApiClient.Client<Groups>,
57 never,
58 | HttpApiGroup.ToService<ApiId, SelectedGroups>
59 | HttpApiGroup.MiddlewareClient<Groups>
60 | HttpApiEndpoint.Middleware<HttpApiGroup.Endpoints<Groups>>
61 | FileSystem
62 | Generator
63 | HttpPlatform
64 | Path
65 | Scope
66> {
67 let context = yield* Effect.context<HttpApiGroup.ToService<ApiId, SelectedGroups>>()
68
69 const groups = api.groups as unknown as Record<string, HttpApiGroup.Top>
70 for (const identifier in groups) {
71 const group = groups[identifier]
72 if (groupIdentifiers.includes(identifier as any)) {
73 continue
74 }
75 const handlers = new Map<string, HandlerRuntime>()
76 const routes: Array<HttpRouter.Route<any, any>> = []
77 for (const endpointIdentifier in group.endpoints) {
78 const endpoint = group.endpoints[endpointIdentifier]
79 const handler: HandlerRuntime = {
80 endpoint: endpoint as any,
81 handler: () => Effect.die(new Error(`Unhandled endpoint: ${endpointIdentifier}`)),
82 isRaw: false,
83 uninterruptible: false
84 }
85 handlers.set(endpointIdentifier, handler)
86 routes.push(HttpApiBuilder.handlerToRoute(api, group as any, handler, context))
87 }
88 context = Context.add(context, group as any, { handlers, routes })
89 }
90
91 const layer: Layer.Layer<
92 never,
93 never,
94 | FileSystem
95 | Generator
96 | HttpPlatform
97 | HttpRouter.HttpRouter
98 | Path
99 > = HttpApiBuilder.layer(api).pipe(
100 Layer.provide(Layer.succeedContext(context))
101 ) as any
102 const handler = yield* HttpRouter.toHttpEffect(layer)
103 const httpClient = HttpClient.make(Effect.fnUntraced(function*(request) {
104 const serverRequest = HttpServerRequest.fromClientRequest(request)
105 const response = yield* handler.pipe(
106 Effect.flatMap((response) => {
107 const preResponse = preResponseHandler.requestPreResponseHandlers.get(serverRequest.source)
108 return preResponse === undefined ? Effect.succeed(response) : preResponse(serverRequest, response)
109 }),
110 Effect.provideService(HttpServerRequest.HttpServerRequest, serverRequest),
111 Effect.orDie
112 )
113 return HttpServerResponse.toClientResponse(response, { request })
114 }, Effect.scoped))
115
116 return yield* HttpApiClient.makeWith(api, {
117 httpClient,
118 baseUrl: options?.baseUrl ?? "http://localhost:3000"
119 })
120})