Skip to content

Commit 499a183

Browse files
authored
allow Handler to handle multiple endpoints (#641)
1 parent d942cb3 commit 499a183

File tree

9 files changed

+201
-28
lines changed

9 files changed

+201
-28
lines changed

.changeset/friendly-months-type.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"effect-http-node": patch
3+
"effect-http": patch
4+
---
5+
6+
Change internal structure of `Handler` to give it an ability to handler multiple endpoints.

packages/effect-http-node/src/internal/node-testing.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"
55
import type * as HttpApp from "@effect/platform/HttpApp"
66
import * as HttpClient from "@effect/platform/HttpClient"
77
import * as HttpClientRequest from "@effect/platform/HttpClientRequest"
8-
import * as HttpRouter from "@effect/platform/HttpRouter"
98
import * as HttpServer from "@effect/platform/HttpServer"
109
import * as Deferred from "effect/Deferred"
1110
import * as Effect from "effect/Effect"
@@ -50,7 +49,7 @@ export const makeRaw = <R, E>(
5049
export const handler = <A extends ApiEndpoint.ApiEndpoint.Any, E, R>(
5150
handler: Handler.Handler<A, E, R>
5251
) =>
53-
startTestServer(HttpRouter.fromIterable([Handler.getRoute(handler)])).pipe(
52+
startTestServer(Handler.getRouter(handler)).pipe(
5453
Effect.map((url) => makeHttpClient(HttpClient.fetch, url)),
5554
Effect.provide(NodeContext.layer)
5655
)

packages/effect-http-node/test/handler.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,4 +346,45 @@ describe("error reporting", () => {
346346
expect(response.status).toEqual(302)
347347
expect(response.headers).toMatchObject({ "set-cookie": "session=123; Path=/; HttpOnly" })
348348
}))
349+
350+
it.scoped("concat", () =>
351+
Effect.gen(function*() {
352+
const getValueHandler = Handler.make(Api.getEndpoint(exampleApiGet, "getValue"), () => Effect.succeed(12))
353+
const testHandler = Handler.make(Api.getEndpoint(exampleApiPostNullableField, "test"), () =>
354+
Effect.succeed({ value: Option.some("test") }))
355+
356+
const handler = Handler.concat(getValueHandler, testHandler)
357+
358+
const client = yield* NodeTesting.handler(handler)
359+
const [response1, response2] = yield* Effect.zip(
360+
client(HttpClientRequest.get("/get-value")),
361+
client(HttpClientRequest.post("/test"))
362+
)
363+
364+
expect(yield* response1.json).toEqual(12)
365+
expect(yield* response2.json).toEqual({ value: "test" })
366+
}))
367+
368+
it.scoped("concatAll", () =>
369+
Effect.gen(function*() {
370+
const helloHandler = Api.getEndpoint(exampleApiParams, "hello").pipe(
371+
Handler.make(({ path }) => Effect.succeed(path.value))
372+
)
373+
const getValueHandler = Handler.make(Api.getEndpoint(exampleApiGet, "getValue"), () => Effect.succeed(12))
374+
const testHandler = Handler.make(Api.getEndpoint(exampleApiPostNullableField, "test"), () =>
375+
Effect.succeed({ value: Option.some("test") }))
376+
377+
const handler = Handler.concatAll(getValueHandler, testHandler, helloHandler)
378+
379+
const client = yield* NodeTesting.handler(handler)
380+
const [response1, response2, response3] = yield* Effect.all([
381+
client(HttpClientRequest.get("/get-value")),
382+
client(HttpClientRequest.post("/test")),
383+
client(HttpClientRequest.post("/hello/a"))
384+
])
385+
386+
expect(yield* response1.json).toEqual(12)
387+
expect(yield* response2.json).toEqual({ value: "test" })
388+
expect(yield* response3.json).toEqual("a")
389+
}))
349390
})
Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,46 @@
11
import { Effect } from "effect"
22
import { Api, Handler } from "effect-http"
33

4-
const endpoint = Api.get("getArticle", "/article")
4+
const endpoint1 = Api.get("endpoint1", "/endpoint1")
5+
const endpoint2 = Api.get("endpoint2", "/endpoint2")
6+
const endpoint3 = Api.get("endpoint3", "/endpoint3")
57

6-
// $ExpectType Handler<Default<"getArticle">, never, never>
7-
Handler.make(endpoint, () => Effect.void)
8+
declare const eff1: Effect.Effect<void, "E1", "R1">
9+
declare const eff2: Effect.Effect<void, "E2", "R2">
10+
declare const eff3: Effect.Effect<void, "E3", "R3">
811

9-
// $ExpectType Handler<Default<"getArticle">, never, never>
10-
endpoint.pipe(Handler.make(() => Effect.void))
12+
// $ExpectType Handler<Default<"endpoint1">, never, never>
13+
Handler.make(endpoint1, () => Effect.void)
14+
15+
// $ExpectType Handler<Default<"endpoint1">, never, never>
16+
endpoint1.pipe(Handler.make(() => Effect.void))
17+
18+
// $ExpectType Handler<Default<"endpoint1"> | Default<"endpoint2">, never, never>
19+
Handler.concat(
20+
Handler.make(endpoint1, () => Effect.void),
21+
Handler.make(endpoint2, () => Effect.void)
22+
)
23+
24+
// $ExpectType Handler<Default<"endpoint1"> | Default<"endpoint2">, never, never>
25+
Handler.make(endpoint1, () => Effect.void).pipe(Handler.concat(
26+
Handler.make(endpoint2, () => Effect.void)
27+
))
28+
29+
// $ExpectType Handler<Default<"endpoint1"> | Default<"endpoint2">, "E1", "R1">
30+
Handler.concat(
31+
Handler.make(endpoint1, () => eff1),
32+
Handler.make(endpoint2, () => Effect.void)
33+
)
34+
35+
// $ExpectType Handler<Default<"endpoint1"> | Default<"endpoint2">, "E1" | "E2", "R1" | "R2">
36+
Handler.concat(
37+
Handler.make(endpoint1, () => eff1),
38+
Handler.make(endpoint2, () => eff2)
39+
)
40+
41+
// $ExpectType Handler<Default<"endpoint1"> | Default<"endpoint2"> | Default<"endpoint3">, "E1" | "E2" | "E3", "R1" | "R2" | "R3">
42+
Handler.concatAll(
43+
Handler.make(endpoint1, () => eff1),
44+
Handler.make(endpoint2, () => eff2),
45+
Handler.make(endpoint3, () => eff3)
46+
)

packages/effect-http/src/ApiEndpoint.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export declare namespace ApiEndpoint {
9999
Id,
100100
ApiRequest.ApiRequest.Default,
101101
ApiResponse.ApiResponse.Default,
102-
Security.Security<void>
102+
Security.Security.Default
103103
>
104104

105105
/**

packages/effect-http/src/Handler.ts

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,32 @@ export declare namespace Handler {
124124
* @since 1.0.0
125125
*/
126126
export type ToSecurity<Security extends Security.Security.Any> = Security.Security.Success<Security>
127+
128+
/**
129+
* @category models
130+
* @since 1.0.0
131+
*/
132+
export type Endpoint<H> = H extends Handler<infer A, any, any> ? A : never
133+
134+
/**
135+
* @category models
136+
* @since 1.0.0
137+
*/
138+
export type Error<H> = H extends Handler<any, infer E, any> ? E : never
139+
140+
/**
141+
* @category models
142+
* @since 1.0.0
143+
*/
144+
export type Context<H> = H extends Handler<any, any, infer R> ? R : never
127145
}
128146

147+
/**
148+
* @category constructors
149+
* @since 1.0.0
150+
*/
151+
export const empty: Handler<never, never, never> = internal.empty
152+
129153
/**
130154
* @category constructors
131155
* @since 1.0.0
@@ -157,17 +181,40 @@ export const makeRaw: {
157181
} = internal.makeRaw
158182

159183
/**
160-
* @category getters
184+
* @category combinators
185+
* @since 1.0.0
186+
*/
187+
export const concat: {
188+
<A extends ApiEndpoint.ApiEndpoint.Any, B extends ApiEndpoint.ApiEndpoint.Any, E1, E2, R1, R2>(
189+
self: Handler<A, E1, R1>,
190+
handler: Handler<B, E2, R2>
191+
): Handler<A | B, E1 | E2, R1 | R2>
192+
193+
<B extends ApiEndpoint.ApiEndpoint.Any, E2, R2>(
194+
handler: Handler<B, E2, R2>
195+
): <A extends ApiEndpoint.ApiEndpoint.Any, E1, R1>(self: Handler<A, E1, R1>) => Handler<A | B, E1 | E2, R1 | R2>
196+
} = internal.concat
197+
198+
/**
199+
* @category combinators
200+
* @since 1.0.0
201+
*/
202+
export const concatAll: <Handlers extends ReadonlyArray<Handler.Any>>(
203+
...handlers: Handlers
204+
) => Handler<Handler.Endpoint<Handlers[number]>, Handler.Error<Handlers[number]>, Handler.Context<Handlers[number]>> =
205+
internal.concatAll
206+
/**
207+
* @category destructors
161208
* @since 1.0.0
162209
*/
163-
export const getRoute: <A extends ApiEndpoint.ApiEndpoint.Any, E, R>(
210+
export const getRouter: <A extends ApiEndpoint.ApiEndpoint.Any, E, R>(
164211
handler: Handler<A, E, R>
165-
) => HttpRouter.Route<E, R> = internal.getRoute
212+
) => HttpRouter.HttpRouter<E, R> = internal.getRouter
166213

167214
/**
168-
* @category getters
215+
* @category destructors
169216
* @since 1.0.0
170217
*/
171-
export const getEndpoint: <A extends ApiEndpoint.ApiEndpoint.Any, E, R>(
218+
export const getEndpoints: <A extends ApiEndpoint.ApiEndpoint.Any, E, R>(
172219
handler: Handler<A, E, R>
173-
) => A = internal.getEndpoint
220+
) => ReadonlyArray<A> = internal.getEndpoints

packages/effect-http/src/Security.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ export declare namespace Security {
5454
*/
5555
export type Any = Security<any, any, any>
5656

57+
/**
58+
* @category models
59+
* @since 1.0.0
60+
*/
61+
export type Default = Security<void, never, never>
62+
5763
/**
5864
* @category models
5965
* @since 1.0.0

packages/effect-http/src/internal/handler.ts

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as HttpRouter from "@effect/platform/HttpRouter"
2+
import * as Array from "effect/Array"
23
import * as Effect from "effect/Effect"
3-
import { pipe } from "effect/Function"
4+
import { dual, pipe } from "effect/Function"
45
import * as Pipeable from "effect/Pipeable"
56

67
import * as ApiEndpoint from "../ApiEndpoint.js"
@@ -19,23 +20,26 @@ export const variance = {
1920
/* c8 ignore next */
2021
_A: (_: any) => _,
2122
/* c8 ignore next */
22-
_E: (_: any) => _,
23+
_E: (_: never) => _,
2324
/* c8 ignore next */
24-
_R: (_: any) => _
25+
_R: (_: never) => _
2526
}
2627

2728
/** @internal */
2829
class HandlerImpl<A extends ApiEndpoint.ApiEndpoint.Any, E, R> implements Handler.Handler<A, E, R> {
2930
readonly [TypeId] = variance
3031

31-
constructor(readonly endpoint: A, readonly route: HttpRouter.Route<E, R>) {}
32+
constructor(readonly endpoints: ReadonlyArray<A>, readonly router: HttpRouter.HttpRouter<E, R>) {}
3233

3334
pipe() {
3435
// eslint-disable-next-line prefer-rest-params
3536
return Pipeable.pipeArguments(this, arguments)
3637
}
3738
}
3839

40+
/** @internal */
41+
export const empty = new HandlerImpl([], HttpRouter.empty) as unknown as Handler.Handler<never, never, never> // TODO check variance
42+
3943
/** @internal */
4044
export const make: {
4145
<A extends ApiEndpoint.ApiEndpoint.Any, E, R>(
@@ -77,8 +81,8 @@ const _makeRaw = <A extends ApiEndpoint.ApiEndpoint.Any, E, R>(
7781
endpoint: A,
7882
handler: HttpRouter.Route.Handler<E, R>
7983
): Handler.Handler<A, E, R> => {
80-
const router = HttpRouter.makeRoute(ApiEndpoint.getMethod(endpoint), ApiEndpoint.getPath(endpoint), handler)
81-
return new HandlerImpl(endpoint, router)
84+
const route = HttpRouter.makeRoute(ApiEndpoint.getMethod(endpoint), ApiEndpoint.getPath(endpoint), handler)
85+
return new HandlerImpl([endpoint], HttpRouter.fromIterable([route]))
8286
}
8387

8488
/** @internal */
@@ -110,11 +114,45 @@ const _make = <A extends ApiEndpoint.ApiEndpoint.Any, E, R>(
110114
}
111115

112116
/** @internal */
113-
export const getRoute = <A extends ApiEndpoint.ApiEndpoint.Any, E, R>(
117+
export const getRouter = <A extends ApiEndpoint.ApiEndpoint.Any, E, R>(
114118
handler: Handler.Handler<A, E, R>
115-
): HttpRouter.Route<E, R> => (handler as HandlerImpl<A, E, R>).route
119+
): HttpRouter.HttpRouter<E, R> => (handler as HandlerImpl<A, E, R>).router
116120

117121
/** @internal */
118-
export const getEndpoint = <A extends ApiEndpoint.ApiEndpoint.Any, E, R>(
122+
export const getEndpoints = <A extends ApiEndpoint.ApiEndpoint.Any, E, R>(
119123
handler: Handler.Handler<A, E, R>
120-
): A => (handler as HandlerImpl<A, E, R>).endpoint
124+
): ReadonlyArray<A> => (handler as HandlerImpl<A, E, R>).endpoints
125+
126+
/** @internal */
127+
export const concat: {
128+
<A extends ApiEndpoint.ApiEndpoint.Any, B extends ApiEndpoint.ApiEndpoint.Any, E1, E2, R1, R2>(
129+
self: Handler.Handler<A, E1, R1>,
130+
handler: Handler.Handler<B, E2, R2>
131+
): Handler.Handler<A | B, E1 | E2, R1 | R2>
132+
133+
<B extends ApiEndpoint.ApiEndpoint.Any, E2, R2>(
134+
handler: Handler.Handler<B, E2, R2>
135+
): <A extends ApiEndpoint.ApiEndpoint.Any, E1, R1>(
136+
self: Handler.Handler<A, E1, R1>
137+
) => Handler.Handler<A | B, E1 | E2, R1 | R2>
138+
} = dual(2, <A extends ApiEndpoint.ApiEndpoint.Any, B extends ApiEndpoint.ApiEndpoint.Any, E1, E2, R1, R2>(
139+
self: Handler.Handler<A, E1, R1>,
140+
handler: Handler.Handler<B, E2, R2>
141+
): Handler.Handler<A | B, E1 | E2, R1 | R2> =>
142+
new HandlerImpl(
143+
[...getEndpoints(self), ...getEndpoints(handler)],
144+
HttpRouter.concat(getRouter(self), getRouter(handler))
145+
))
146+
147+
/** @internal */
148+
export const concatAll = <Handlers extends ReadonlyArray<Handler.Handler.Any>>(
149+
...handlers: Handlers
150+
): Handler.Handler<
151+
Handler.Handler.Endpoint<Handlers[number]>,
152+
Handler.Handler.Error<Handlers[number]>,
153+
Handler.Handler.Context<Handlers[number]>
154+
> =>
155+
new HandlerImpl(
156+
Array.flatten(handlers.map(getEndpoints)),
157+
handlers.map(getRouter).reduce(HttpRouter.concat, HttpRouter.empty)
158+
)

packages/effect-http/src/internal/router-builder.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,11 @@ export const handle = (...args: Array<any>) => (builder: RouterBuilder.RouterBui
120120
}
121121

122122
const handler = args[0] as Handler.Handler.Any
123-
const remainingEndpoints = removeRemainingEndpoint(
124-
builder,
125-
ApiEndpoint.getId(Handler.getEndpoint(handler))
123+
const handlerEndpointIds = Handler.getEndpoints(handler).map((e) => ApiEndpoint.getId(e))
124+
const remainingEndpoints = builder.remainingEndpoints.filter((e1) =>
125+
!handlerEndpointIds.includes(ApiEndpoint.getId(e1))
126126
)
127-
const router = addRoute(builder.router, Handler.getRoute(handler))
127+
const router = HttpRouter.concat(builder.router, Handler.getRouter(handler))
128128

129129
return new RouterBuilderImpl(remainingEndpoints, builder.api, router, builder.options)
130130
}

0 commit comments

Comments
 (0)