From 90ceeab3a04051b740af18c8af8bd73ee8ec6363 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 9 Oct 2024 09:45:47 +1300 Subject: [PATCH] HttpClient refactor & simplification (#3746) --- .changeset/early-otters-confess.md | 7 + .changeset/stale-shirts-notice.md | 7 + .changeset/tricky-ears-protect.md | 7 + .../platform-browser/src/BrowserHttpClient.ts | 2 +- .../src/internal/httpClient.ts | 2 +- .../test/BrowserHttpClient.test.ts | 10 +- packages/platform-bun/src/BunHttpServer.ts | 2 +- packages/platform-node/src/NodeHttpClient.ts | 12 +- packages/platform-node/src/NodeHttpServer.ts | 6 +- .../platform-node/src/internal/httpClient.ts | 4 +- .../src/internal/httpClientUndici.ts | 4 +- packages/platform-node/test/HttpApi.test.ts | 8 +- .../platform-node/test/HttpClient.test.ts | 31 +- .../platform-node/test/HttpServer.test.ts | 40 +- packages/platform/README.md | 94 ++-- packages/platform/src/FetchHttpClient.ts | 2 +- packages/platform/src/HttpApiClient.ts | 4 +- packages/platform/src/HttpClient.ts | 481 +++++++++-------- packages/platform/src/HttpClientRequest.ts | 9 +- packages/platform/src/HttpServer.ts | 2 +- .../platform/src/internal/fetchHttpClient.ts | 2 +- packages/platform/src/internal/httpClient.ts | 494 +++++++----------- .../src/internal/httpClientRequest.ts | 14 +- packages/platform/test/HttpClient.test.ts | 50 +- packages/rpc-http/src/HttpRpcResolver.ts | 2 +- .../rpc-http/src/HttpRpcResolverNoStream.ts | 2 +- 26 files changed, 613 insertions(+), 685 deletions(-) create mode 100644 .changeset/early-otters-confess.md create mode 100644 .changeset/stale-shirts-notice.md create mode 100644 .changeset/tricky-ears-protect.md diff --git a/.changeset/early-otters-confess.md b/.changeset/early-otters-confess.md new file mode 100644 index 0000000000..5b9368a20d --- /dev/null +++ b/.changeset/early-otters-confess.md @@ -0,0 +1,7 @@ +--- +"@effect/platform-browser": minor +"@effect/platform-node": minor +"@effect/platform": minor +--- + +remove HttpClient.Service type diff --git a/.changeset/stale-shirts-notice.md b/.changeset/stale-shirts-notice.md new file mode 100644 index 0000000000..41bb231b42 --- /dev/null +++ b/.changeset/stale-shirts-notice.md @@ -0,0 +1,7 @@ +--- +"@effect/platform-browser": minor +"@effect/platform-node": minor +"@effect/platform": minor +--- + +constrain HttpClient success type to HttpClientResponse diff --git a/.changeset/tricky-ears-protect.md b/.changeset/tricky-ears-protect.md new file mode 100644 index 0000000000..a30f03ae16 --- /dev/null +++ b/.changeset/tricky-ears-protect.md @@ -0,0 +1,7 @@ +--- +"@effect/platform-browser": minor +"@effect/platform-node": minor +"@effect/platform": minor +--- + +add HttpClient accessor apis diff --git a/packages/platform-browser/src/BrowserHttpClient.ts b/packages/platform-browser/src/BrowserHttpClient.ts index 303f59a89c..24dde80bb1 100644 --- a/packages/platform-browser/src/BrowserHttpClient.ts +++ b/packages/platform-browser/src/BrowserHttpClient.ts @@ -13,7 +13,7 @@ import * as internal from "./internal/httpClient.js" * @since 1.0.0 * @category layers */ -export const layerXMLHttpRequest: Layer.Layer = internal.layerXMLHttpRequest +export const layerXMLHttpRequest: Layer.Layer = internal.layerXMLHttpRequest /** * @since 1.0.0 diff --git a/packages/platform-browser/src/internal/httpClient.ts b/packages/platform-browser/src/internal/httpClient.ts index b8b4b96ca9..d6f20e12e0 100644 --- a/packages/platform-browser/src/internal/httpClient.ts +++ b/packages/platform-browser/src/internal/httpClient.ts @@ -37,7 +37,7 @@ export const withXHRArrayBuffer = (effect: Effect.Effect): Eff const makeXhr = () => new XMLHttpRequest() -const makeXMLHttpRequest = Client.makeService((request, url, signal, fiber) => +const makeXMLHttpRequest = Client.make((request, url, signal, fiber) => Effect.suspend(() => { const xhr = Context.getOrElse( fiber.getFiberRef(FiberRef.currentContext), diff --git a/packages/platform-browser/test/BrowserHttpClient.test.ts b/packages/platform-browser/test/BrowserHttpClient.test.ts index 7dab7b9c5f..24b3d29086 100644 --- a/packages/platform-browser/test/BrowserHttpClient.test.ts +++ b/packages/platform-browser/test/BrowserHttpClient.test.ts @@ -1,4 +1,4 @@ -import { Cookies, HttpClientRequest } from "@effect/platform" +import { Cookies, HttpClient } from "@effect/platform" import { BrowserHttpClient } from "@effect/platform-browser" import { assert, describe, it } from "@effect/vitest" import { Chunk, Effect, Layer, Stream } from "effect" @@ -15,7 +15,7 @@ const layer = (...args: Parameters) => describe("BrowserHttpClient", () => { it.effect("json", () => Effect.gen(function*() { - const body = yield* HttpClientRequest.get("http://localhost:8080/my/url").pipe( + const body = yield* HttpClient.get("http://localhost:8080/my/url").pipe( Effect.flatMap((_) => _.json), Effect.scoped ) @@ -29,7 +29,7 @@ describe("BrowserHttpClient", () => { it.effect("stream", () => Effect.gen(function*() { - const body = yield* HttpClientRequest.get("http://localhost:8080/my/url").pipe( + const body = yield* HttpClient.get("http://localhost:8080/my/url").pipe( Effect.map((_) => _.stream.pipe( Stream.decodeText(), @@ -49,7 +49,7 @@ describe("BrowserHttpClient", () => { it.effect("cookies", () => Effect.gen(function*() { - const cookies = yield* HttpClientRequest.get("http://localhost:8080/my/url").pipe( + const cookies = yield* HttpClient.get("http://localhost:8080/my/url").pipe( Effect.map((res) => res.cookies), Effect.scoped ) @@ -67,7 +67,7 @@ describe("BrowserHttpClient", () => { it.effect("arrayBuffer", () => Effect.gen(function*() { - const body = yield* HttpClientRequest.get("http://localhost:8080/my/url").pipe( + const body = yield* HttpClient.get("http://localhost:8080/my/url").pipe( Effect.flatMap((_) => _.arrayBuffer), Effect.scoped, BrowserHttpClient.withXHRArrayBuffer diff --git a/packages/platform-bun/src/BunHttpServer.ts b/packages/platform-bun/src/BunHttpServer.ts index 913dec2972..5f55e9db6d 100644 --- a/packages/platform-bun/src/BunHttpServer.ts +++ b/packages/platform-bun/src/BunHttpServer.ts @@ -46,7 +46,7 @@ export const layer: ( * @category layers */ export const layerTest: Layer.Layer< - | HttpClient.HttpClient.Service + | HttpClient.HttpClient | Server.HttpServer | Platform.HttpPlatform | Etag.Generator diff --git a/packages/platform-node/src/NodeHttpClient.ts b/packages/platform-node/src/NodeHttpClient.ts index d1d2657577..9260de4b68 100644 --- a/packages/platform-node/src/NodeHttpClient.ts +++ b/packages/platform-node/src/NodeHttpClient.ts @@ -63,19 +63,19 @@ export const makeAgentLayer: (options?: Https.AgentOptions) => Layer.Layer = internal.make +export const make: Effect.Effect = internal.make /** * @since 1.0.0 * @category layers */ -export const layer: Layer.Layer = internal.layer +export const layer: Layer.Layer = internal.layer /** * @since 1.0.0 * @category layers */ -export const layerWithoutAgent: Layer.Layer = internal.layerWithoutAgent +export const layerWithoutAgent: Layer.Layer = internal.layerWithoutAgent /** * @since 1.0.0 @@ -122,17 +122,17 @@ export class UndiciRequestOptions extends Context.Tag(internalUndici.undiciOptio * @since 1.0.0 * @category constructors */ -export const makeUndici: (dispatcher: Undici.Dispatcher) => Client.HttpClient.Service = internalUndici.make +export const makeUndici: (dispatcher: Undici.Dispatcher) => Client.HttpClient = internalUndici.make /** * @since 1.0.0 * @category layers */ -export const layerUndici: Layer.Layer = internalUndici.layer +export const layerUndici: Layer.Layer = internalUndici.layer /** * @since 1.0.0 * @category layers */ -export const layerUndiciWithoutDispatcher: Layer.Layer = +export const layerUndiciWithoutDispatcher: Layer.Layer = internalUndici.layerWithoutDispatcher diff --git a/packages/platform-node/src/NodeHttpServer.ts b/packages/platform-node/src/NodeHttpServer.ts index 244ecc0ee5..fbd8885385 100644 --- a/packages/platform-node/src/NodeHttpServer.ts +++ b/packages/platform-node/src/NodeHttpServer.ts @@ -91,13 +91,13 @@ export const layerConfig: ( * with prepended url of the running http server. * * @example - * import { HttpClientRequest, HttpRouter, HttpServer } from "@effect/platform" + * import { HttpClient, HttpRouter, HttpServer } from "@effect/platform" * import { NodeHttpServer } from "@effect/platform-node" * import { Effect } from "effect" * * Effect.gen(function*() { * yield* HttpServer.serveEffect(HttpRouter.empty) - * const response = yield* HttpClientRequest.get("/") + * const response = yield* HttpClient.get("/") * assert.strictEqual(response.status, 404) * }).pipe(Effect.provide(NodeHttpServer.layerTest)) * @@ -105,7 +105,7 @@ export const layerConfig: ( * @category layers */ export const layerTest: Layer.Layer< - | HttpClient.HttpClient.Service + | HttpClient.HttpClient | Server.HttpServer | Platform.HttpPlatform | Etag.Generator diff --git a/packages/platform-node/src/internal/httpClient.ts b/packages/platform-node/src/internal/httpClient.ts index 3b7104fc34..b9da0d7b87 100644 --- a/packages/platform-node/src/internal/httpClient.ts +++ b/packages/platform-node/src/internal/httpClient.ts @@ -54,8 +54,8 @@ export const makeAgentLayer = (options?: Https.AgentOptions): Layer.Layer - Client.makeService((request, url, signal) => { +const fromAgent = (agent: NodeClient.HttpAgent): Client.HttpClient => + Client.make((request, url, signal) => { const nodeRequest = url.protocol === "https:" ? Https.request(url, { agent: agent.https, diff --git a/packages/platform-node/src/internal/httpClientUndici.ts b/packages/platform-node/src/internal/httpClientUndici.ts index 7574f3f7d3..2a86be633a 100644 --- a/packages/platform-node/src/internal/httpClientUndici.ts +++ b/packages/platform-node/src/internal/httpClientUndici.ts @@ -40,8 +40,8 @@ export const dispatcherLayerGlobal = Layer.sync(Dispatcher, () => Undici.getGlob export const undiciOptionsTagKey = "@effect/platform-node/NodeHttpClient/undiciOptions" /** @internal */ -export const make = (dispatcher: Undici.Dispatcher): Client.HttpClient.Service => - Client.makeService((request, url, signal, fiber) => { +export const make = (dispatcher: Undici.Dispatcher): Client.HttpClient => + Client.make((request, url, signal, fiber) => { const context = fiber.getFiberRef(FiberRef.currentContext) const options: Undici.Dispatcher.RequestOptions = context.unsafeMap.get(undiciOptionsTagKey) ?? {} return convertBody(request.body).pipe( diff --git a/packages/platform-node/test/HttpApi.test.ts b/packages/platform-node/test/HttpApi.test.ts index 847470279f..a9a513ccea 100644 --- a/packages/platform-node/test/HttpApi.test.ts +++ b/packages/platform-node/test/HttpApi.test.ts @@ -73,7 +73,7 @@ describe("HttpApi", () => { describe("errors", () => { it.scoped("empty errors have no body", () => Effect.gen(function*() { - const response = yield* HttpClientRequest.get("/groups/0") + const response = yield* HttpClient.get("/groups/0") assert.strictEqual(response.status, 418) const text = yield* response.text assert.strictEqual(text, "") @@ -91,7 +91,8 @@ describe("HttpApi", () => { it.scoped("default to 500 status code", () => Effect.gen(function*() { const response = yield* HttpClientRequest.get("/users").pipe( - HttpClientRequest.setHeaders({ page: "0" }) + HttpClientRequest.setHeaders({ page: "0" }), + HttpClient.execute ) assert.strictEqual(response.status, 500) const body = yield* response.json @@ -103,7 +104,8 @@ describe("HttpApi", () => { it.scoped("class level annotations", () => Effect.gen(function*() { const response = yield* HttpClientRequest.post("/users").pipe( - HttpClientRequest.bodyUnsafeJson({ name: "boom" }) + HttpClientRequest.bodyUnsafeJson({ name: "boom" }), + HttpClient.execute ) assert.strictEqual(response.status, 400) }).pipe(Effect.provide(HttpLive))) diff --git a/packages/platform-node/test/HttpClient.test.ts b/packages/platform-node/test/HttpClient.test.ts index 574d4a279d..80e116a31f 100644 --- a/packages/platform-node/test/HttpClient.test.ts +++ b/packages/platform-node/test/HttpClient.test.ts @@ -2,6 +2,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platf import * as NodeClient from "@effect/platform-node/NodeHttpClient" import * as Schema from "@effect/schema/Schema" import { describe, expect, it } from "@effect/vitest" +import { Struct } from "effect" import * as Context from "effect/Context" import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" @@ -13,23 +14,24 @@ const Todo = Schema.Struct({ title: Schema.String, completed: Schema.Boolean }) +const TodoWithoutId = Schema.Struct({ + ...Struct.omit(Todo.fields, "id") +}) const makeJsonPlaceholder = Effect.gen(function*(_) { const defaultClient = yield* _(HttpClient.HttpClient) const client = defaultClient.pipe( HttpClient.mapRequest(HttpClientRequest.prependUrl("https://jsonplaceholder.typicode.com")) ) - const todoClient = client.pipe( - HttpClient.mapEffect(HttpClientResponse.schemaBodyJson(Todo)), - HttpClient.scoped - ) - const createTodo = HttpClient.schemaFunction( - todoClient, - Todo.pipe(Schema.omit("id")) - )(HttpClientRequest.post("/todos")) + const createTodo = (todo: typeof TodoWithoutId.Type) => + HttpClientRequest.post("/todos").pipe( + HttpClientRequest.schemaBodyJson(TodoWithoutId)(todo), + Effect.flatMap(client.execute), + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)), + Effect.scoped + ) return { client, - todoClient, createTodo } as const }) @@ -50,7 +52,7 @@ const JsonPlaceholderLive = Layer.effect(JsonPlaceholder, makeJsonPlaceholder) it.effect("google", () => Effect.gen(function*(_) { const response = yield* _( - HttpClientRequest.get("https://www.google.com/"), + HttpClient.get("https://www.google.com/"), Effect.flatMap((_) => _.text), Effect.scoped ) @@ -83,13 +85,16 @@ const JsonPlaceholderLive = Layer.effect(JsonPlaceholder, makeJsonPlaceholder) it.effect("jsonplaceholder", () => Effect.gen(function*() { const jp = yield* JsonPlaceholder - const response = yield* jp.todoClient.get("/todos/1") + const response = yield* jp.client.get("/todos/1").pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)), + Effect.scoped + ) expect(response.id).toBe(1) }).pipe(Effect.provide(JsonPlaceholderLive.pipe( Layer.provide(layer) )))) - it.effect("jsonplaceholder schemaFunction", () => + it.effect("jsonplaceholder schemaBodyJson", () => Effect.gen(function*() { const jp = yield* JsonPlaceholder const response = yield* jp.createTodo({ @@ -130,7 +135,7 @@ const JsonPlaceholderLive = Layer.effect(JsonPlaceholder, makeJsonPlaceholder) it.effect("close early", () => Effect.gen(function*(_) { const response = yield* _( - HttpClientRequest.get("https://www.google.com/"), + HttpClient.get("https://www.google.com/"), Effect.scoped ) expect(response.status).toBe(200) diff --git a/packages/platform-node/test/HttpServer.test.ts b/packages/platform-node/test/HttpServer.test.ts index 0afd0556c3..ebbd9877f4 100644 --- a/packages/platform-node/test/HttpServer.test.ts +++ b/packages/platform-node/test/HttpServer.test.ts @@ -18,7 +18,7 @@ import { import { NodeHttpServer } from "@effect/platform-node" import * as Schema from "@effect/schema/Schema" import { assert, describe, expect, it } from "@effect/vitest" -import { Deferred, Duration, Fiber, flow, Stream } from "effect" +import { Deferred, Duration, Fiber, Stream } from "effect" import * as Effect from "effect/Effect" import * as Option from "effect/Option" import * as Tracer from "effect/Tracer" @@ -33,14 +33,6 @@ const IdParams = Schema.Struct({ }) const todoResponse = HttpServerResponse.schemaJson(Todo) -const makeTodoClient = Effect.map( - HttpClient.HttpClient, - flow( - HttpClient.mapEffect(HttpClientResponse.schemaBodyJson(Todo)), - HttpClient.scoped - ) -) - describe("HttpServer", () => { it.scoped("schema", () => Effect.gen(function*() { @@ -54,8 +46,10 @@ describe("HttpServer", () => { ), HttpServer.serveEffect() ) - const client = yield* makeTodoClient - const todo = yield* client.get("/todos/1") + const todo = yield* HttpClient.get("/todos/1").pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)), + Effect.scoped + ) expect(todo).toEqual({ id: 1, title: "test" }) }).pipe(Effect.provide(NodeHttpServer.layerTest))) @@ -306,10 +300,10 @@ describe("HttpServer", () => { ), HttpServer.serveEffect() ) - const client = yield* makeTodoClient const todo = yield* HttpClientRequest.post("/todos").pipe( HttpClientRequest.bodyUrlParams({ id: "1", title: "test" }), - client.execute, + HttpClient.execute, + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)), Effect.scoped ) expect(todo).toEqual({ id: 1, title: "test" }) @@ -674,10 +668,10 @@ describe("HttpServer", () => { Effect.gen(function*() { yield* HttpRouter.concat(routerA, routerB).pipe(HttpServer.serveEffect()) const [responseA, responseMountA, responseB, responseMountB] = yield* Effect.all([ - HttpClientRequest.get("/a"), - HttpClientRequest.get("/ma"), - HttpClientRequest.get("/b"), - HttpClientRequest.get("/mb") + HttpClient.get("/a"), + HttpClient.get("/ma"), + HttpClient.get("/b"), + HttpClient.get("/mb") ]) expect(yield* responseA.text).toEqual("a") expect(yield* responseMountA.text).toEqual("ma") @@ -689,10 +683,10 @@ describe("HttpServer", () => { Effect.gen(function*() { yield* HttpRouter.concatAll(routerA, routerB).pipe(HttpServer.serveEffect()) const [responseA, responseMountA, responseB, responseMountB] = yield* Effect.all([ - HttpClientRequest.get("/a"), - HttpClientRequest.get("/ma"), - HttpClientRequest.get("/b"), - HttpClientRequest.get("/mb") + HttpClient.get("/a"), + HttpClient.get("/ma"), + HttpClient.get("/b"), + HttpClient.get("/mb") ]) expect(yield* responseA.text).toEqual("a") expect(yield* responseMountA.text).toEqual("ma") @@ -706,9 +700,9 @@ describe("HttpServer", () => { HttpRouter.get("/:param", HttpServerResponse.empty()), HttpServer.serveEffect() ) - let res = yield* HttpClientRequest.get("/123456").pipe(Effect.scoped) + let res = yield* HttpClient.get("/123456").pipe(Effect.scoped) assert.strictEqual(res.status, 404) - res = yield* HttpClientRequest.get("/12345").pipe(Effect.scoped) + res = yield* HttpClient.get("/12345").pipe(Effect.scoped) assert.strictEqual(res.status, 204) }).pipe( Effect.provide(NodeHttpServer.layerTest), diff --git a/packages/platform/README.md b/packages/platform/README.md index 47b1e8d9d5..7c960f3362 100644 --- a/packages/platform/README.md +++ b/packages/platform/README.md @@ -574,8 +574,7 @@ The `HttpClient` interface has a set of methods for sending requests: executing it in one step To access the `HttpClient`, you can use the `HttpClient.HttpClient` [tag](https://effect.website/docs/guides/context-management/services). -This will give you access to a `HttpClient.Service` instance, which is the default -instance of the `HttpClient` interface. +This will give you access to a `HttpClient` instance. **Example: Retrieving JSON Data (GET)** @@ -617,6 +616,42 @@ Output: */ ``` +**Example: Retrieving JSON Data with accessor apis (GET)** + +The `HttpClient` module also provides a set of accessor apis that allow you to +easily send requests without first accessing the `HttpClient` service. + +Below is an example of using the `get` accessor api to send a GET request: + +(The following examples will continue to use the `HttpClient` service approach). + +```ts +import { FetchHttpClient, HttpClient } from "@effect/platform" +import { Effect } from "effect" + +const program = HttpClient.get( + "https://jsonplaceholder.typicode.com/posts/1" +).pipe( + Effect.andThen((response) => response.json), + Effect.scoped, + Effect.provide(FetchHttpClient.layer) +) + +Effect.runPromise(program) +/* +Output: +{ + userId: 1, + id: 1, + title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', + body: 'quia et suscipit\n' + + 'suscipit recusandae consequuntur expedita et cum\n' + + 'reprehenderit molestiae ut ut quas totam\n' + + 'nostrum rerum est autem sunt rem eveniet architecto' +} +*/ +``` + **Example: Creating and Executing a Custom Request** Using [HttpClientRequest](#httpclientrequest), you can create and then execute a request. This is useful for customizing the request further. @@ -745,20 +780,17 @@ Output: | Operation | Description | | ------------------------ | --------------------------------------------------------------------------------------- | +| `get`,`post`,`put`... | Send a request without first accessing the `HttpClient` service. | | `filterOrElse` | Filters the result of a response, or runs an alternative effect if the predicate fails. | | `filterOrFail` | Filters the result of a response, or throws an error if the predicate fails. | | `filterStatus` | Filters responses by HTTP status code. | | `filterStatusOk` | Filters responses that return a 2xx status code. | | `followRedirects` | Follows HTTP redirects up to a specified number of times. | -| `map` | Transforms the result of a request. | -| `mapEffect` | Transforms the result of a request using an effectful function. | | `mapRequest` | Appends a transformation of the request object before sending it. | | `mapRequestEffect` | Appends an effectful transformation of the request object before sending it. | | `mapRequestInput` | Prepends a transformation of the request object before sending it. | | `mapRequestInputEffect` | Prepends an effectful transformation of the request object before sending it. | | `retry` | Retries the request based on a provided schedule or policy. | -| `schemaFunction` | Creates a function that validates request data against a schema before sending it. | -| `scoped` | Ensures resources are properly scoped and released after execution. | | `tap` | Performs an additional effect after a successful request. | | `tapRequest` | Performs an additional effect on the request before sending it. | | `withCookiesRef` | Associates a `Ref` of cookies with the client for handling cookies across requests. | @@ -819,48 +851,6 @@ Output: */ ``` -### Integration with Schema - -The `HttpClient.schemaFunction` allows you to integrate schemas into your HTTP client requests. This function ensures that the data you send conforms to a specified schema, enhancing type safety and validation. - -```ts -import { - FetchHttpClient, - HttpClient, - HttpClientRequest -} from "@effect/platform" -import { Schema } from "@effect/schema" -import { Effect } from "effect" - -const program = Effect.gen(function* () { - const client = yield* HttpClient.HttpClient - const addTodo = HttpClient.schemaFunction( - client, - Schema.Struct({ - title: Schema.String, - body: Schema.String, - userId: Schema.Number - }) - )(HttpClientRequest.post("https://jsonplaceholder.typicode.com/posts")) - - const response = yield* addTodo({ - title: "foo", - body: "bar", - userId: 1 - }) - - const json = yield* response.json - - console.log(json) -}).pipe(Effect.scoped, Effect.provide(FetchHttpClient.layer)) - -Effect.runPromise(program) -/* -Output: -{ title: 'foo', body: 'bar', userId: 1, id: 101 } -*/ -``` - ### Persisting Cookies You can manage cookies across requests using the `HttpClient.withCookiesRef` function, which associates a reference to a `Cookies` object with the client. @@ -891,9 +881,9 @@ Effect.runPromise(program) ## RequestInit Options -You can customize the `HttpClient` by passing `RequestInit` options to configure aspects of the HTTP requests, such as credentials, headers, and more. +You can customize the `FetchHttpClient` by passing `RequestInit` options to configure aspects of the HTTP requests, such as credentials, headers, and more. -In this example, we customize the `HttpClient` to include credentials with every request: +In this example, we customize the `FetchHttpClient` to include credentials with every request: ```ts import { FetchHttpClient, HttpClient } from "@effect/platform" @@ -919,13 +909,13 @@ const program = Effect.gen(function* () { ## Create a Custom HttpClient -You can create a custom `HttpClient.Service` using the `HttpClient.makeService` function. This allows you to simulate or mock server responses within your application. +You can create a custom `HttpClient` using the `HttpClient.make` function. This allows you to simulate or mock server responses within your application. ```ts import { HttpClient, HttpClientResponse } from "@effect/platform" import { Effect, Layer } from "effect" -const myClient = HttpClient.makeService((req) => +const myClient = HttpClient.make((req) => Effect.succeed( HttpClientResponse.fromWeb( req, diff --git a/packages/platform/src/FetchHttpClient.ts b/packages/platform/src/FetchHttpClient.ts index 94ad39bcf8..0f1a54650f 100644 --- a/packages/platform/src/FetchHttpClient.ts +++ b/packages/platform/src/FetchHttpClient.ts @@ -22,4 +22,4 @@ export class RequestInit extends Context.Tag(internal.requestInitTagKey) = internal.layer +export const layer: Layer.Layer = internal.layer diff --git a/packages/platform/src/HttpApiClient.ts b/packages/platform/src/HttpApiClient.ts index a31c9cb896..ee98f97d48 100644 --- a/packages/platform/src/HttpApiClient.ts +++ b/packages/platform/src/HttpApiClient.ts @@ -56,10 +56,10 @@ export type Client = [A] extends export const make = ( api: A, options?: { - readonly transformClient?: ((client: HttpClient.HttpClient.Service) => HttpClient.HttpClient.Service) | undefined + readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient) | undefined readonly baseUrl?: string | undefined } -): Effect.Effect>, never, HttpApi.HttpApi.Context | HttpClient.HttpClient.Service> => +): Effect.Effect>, never, HttpApi.HttpApi.Context | HttpClient.HttpClient> => Effect.gen(function*() { const context = yield* Effect.context() const httpClient = (yield* HttpClient.HttpClient).pipe( diff --git a/packages/platform/src/HttpClient.ts b/packages/platform/src/HttpClient.ts index 421d979f80..ae941aef14 100644 --- a/packages/platform/src/HttpClient.ts +++ b/packages/platform/src/HttpClient.ts @@ -1,9 +1,6 @@ /** * @since 1.0.0 */ -import type { ParseOptions } from "@effect/schema/AST" -import type * as ParseResult from "@effect/schema/ParseResult" -import type * as Schema from "@effect/schema/Schema" import type * as Context from "effect/Context" import type * as Effect from "effect/Effect" import type { RuntimeFiber } from "effect/Fiber" @@ -37,17 +34,38 @@ export type TypeId = typeof TypeId * @since 1.0.0 * @category models */ -export interface HttpClient extends Pipeable, Inspectable { +export interface HttpClient extends Pipeable, Inspectable { readonly [TypeId]: TypeId - readonly execute: (request: ClientRequest.HttpClientRequest) => Effect.Effect - - readonly get: (url: string | URL, options?: ClientRequest.Options.NoBody) => Effect.Effect - readonly head: (url: string | URL, options?: ClientRequest.Options.NoBody) => Effect.Effect - readonly post: (url: string | URL, options?: ClientRequest.Options.NoUrl) => Effect.Effect - readonly patch: (url: string | URL, options?: ClientRequest.Options.NoUrl) => Effect.Effect - readonly put: (url: string | URL, options?: ClientRequest.Options.NoUrl) => Effect.Effect - readonly del: (url: string | URL, options?: ClientRequest.Options.NoUrl) => Effect.Effect - readonly options: (url: string | URL, options?: ClientRequest.Options.NoUrl) => Effect.Effect + readonly execute: (request: ClientRequest.HttpClientRequest) => Effect.Effect + + readonly get: ( + url: string | URL, + options?: ClientRequest.Options.NoBody + ) => Effect.Effect + readonly head: ( + url: string | URL, + options?: ClientRequest.Options.NoBody + ) => Effect.Effect + readonly post: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl + ) => Effect.Effect + readonly patch: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl + ) => Effect.Effect + readonly put: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl + ) => Effect.Effect + readonly del: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl + ) => Effect.Effect + readonly options: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl + ) => Effect.Effect } /** @@ -66,41 +84,129 @@ export declare namespace HttpClient { * @since 1.0.0 * @category models */ - export type Postprocess = ( + export type Postprocess = ( request: Effect.Effect - ) => Effect.Effect - - /** - * @since 1.0.0 - * @category models - */ - export type WithResponse = HttpClient - - /** - * @since 1.0.0 - * @category models - */ - export type Service = WithResponse + ) => Effect.Effect } /** * @since 1.0.0 * @category tags */ -export const HttpClient: Context.Tag = internal.tag +export const HttpClient: Context.Tag = internal.tag + +/** + * @since 1.0.0 + * @category accessors + */ +export const execute: ( + request: ClientRequest.HttpClientRequest +) => Effect.Effect = + internal.execute + +/** + * @since 1.0.0 + * @category accessors + */ +export const get: ( + url: string | URL, + options?: ClientRequest.Options.NoBody | undefined +) => Effect.Effect< + ClientResponse.HttpClientResponse, + Error.HttpClientError, + Scope.Scope | HttpClient +> = internal.get + +/** + * @since 1.0.0 + * @category accessors + */ +export const head: ( + url: string | URL, + options?: ClientRequest.Options.NoBody | undefined +) => Effect.Effect< + ClientResponse.HttpClientResponse, + Error.HttpClientError, + Scope.Scope | HttpClient +> = internal.head + +/** + * @since 1.0.0 + * @category accessors + */ +export const post: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl | undefined +) => Effect.Effect< + ClientResponse.HttpClientResponse, + Error.HttpClientError, + Scope.Scope | HttpClient +> = internal.post + +/** + * @since 1.0.0 + * @category accessors + */ +export const patch: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl | undefined +) => Effect.Effect< + ClientResponse.HttpClientResponse, + Error.HttpClientError, + Scope.Scope | HttpClient +> = internal.patch + +/** + * @since 1.0.0 + * @category accessors + */ +export const put: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl | undefined +) => Effect.Effect< + ClientResponse.HttpClientResponse, + Error.HttpClientError, + Scope.Scope | HttpClient +> = internal.put + +/** + * @since 1.0.0 + * @category accessors + */ +export const del: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl | undefined +) => Effect.Effect< + ClientResponse.HttpClientResponse, + Error.HttpClientError, + Scope.Scope | HttpClient +> = internal.del + +/** + * @since 1.0.0 + * @category accessors + */ +export const options: ( + url: string | URL, + options?: ClientRequest.Options.NoUrl | undefined +) => Effect.Effect< + ClientResponse.HttpClientResponse, + Error.HttpClientError, + Scope.Scope | HttpClient +> = internal.options /** * @since 1.0.0 * @category error handling */ export const catchAll: { - ( - f: (e: E) => Effect.Effect - ): (self: HttpClient) => HttpClient - ( - self: HttpClient, + ( + f: (e: E) => Effect.Effect + ): (self: HttpClient) => HttpClient + ( + self: HttpClient, f: (e: E) => Effect.Effect - ): HttpClient + ): HttpClient } = internal.catchAll /** @@ -108,15 +214,15 @@ export const catchAll: { * @category error handling */ export const catchTag: { - ( + ( tag: K, - f: (e: Extract) => Effect.Effect - ): (self: HttpClient) => HttpClient, R1 | R> - ( - self: HttpClient, + f: (e: Extract) => Effect.Effect + ): (self: HttpClient) => HttpClient, R1 | R> + ( + self: HttpClient, tag: K, - f: (e: Extract) => Effect.Effect - ): HttpClient, R | R1> + f: (e: Extract) => Effect.Effect + ): HttpClient, R1 | R> } = internal.catchTag /** @@ -125,17 +231,19 @@ export const catchTag: { */ export const catchTags: { < - E extends { _tag: string }, - Cases extends { [K in E["_tag"]]+?: ((error: Extract) => Effect.Effect) | undefined } + E, + Cases extends + & { + [K in Extract["_tag"]]+?: ( + error: Extract + ) => Effect.Effect + } + & (unknown extends E ? {} : { [K in Exclude["_tag"]>]: never }) >( cases: Cases - ): ( - self: HttpClient + ): ( + self: HttpClient ) => HttpClient< - | A - | { - [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect.Effect ? A : never - }[keyof Cases], | Exclude | { [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect.Effect ? E : never @@ -146,18 +254,19 @@ export const catchTags: { }[keyof Cases] > < - A, E extends { _tag: string }, R, - Cases extends { [K in E["_tag"]]+?: ((error: Extract) => Effect.Effect) | undefined } + Cases extends + & { + [K in Extract["_tag"]]+?: ( + error: Extract + ) => Effect.Effect + } + & (unknown extends E ? {} : { [K in Exclude["_tag"]>]: never }) >( - self: HttpClient, + self: HttpClient, cases: Cases ): HttpClient< - | A - | { - [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect.Effect ? A : never - }[keyof Cases], | Exclude | { [K in keyof Cases]: Cases[K] extends (...args: Array) => Effect.Effect ? E : never @@ -176,26 +285,15 @@ export const catchTags: { * @category filters */ export const filterOrElse: { - ( - refinement: Predicate.Refinement, B>, - orElse: (a: NoInfer) => Effect.Effect - ): (self: HttpClient) => HttpClient - ( - predicate: Predicate.Predicate>, - orElse: (a: NoInfer) => Effect.Effect - ): ( - self: HttpClient - ) => HttpClient - ( - self: HttpClient, - refinement: Predicate.Refinement, - orElse: (a: A) => Effect.Effect - ): HttpClient - ( - self: HttpClient, - predicate: Predicate.Predicate, - orElse: (a: A) => Effect.Effect - ): HttpClient + ( + predicate: Predicate.Predicate, + orElse: (response: ClientResponse.HttpClientResponse) => Effect.Effect + ): (self: HttpClient) => HttpClient + ( + self: HttpClient, + predicate: Predicate.Predicate, + orElse: (response: ClientResponse.HttpClientResponse) => Effect.Effect + ): HttpClient } = internal.filterOrElse /** @@ -205,24 +303,15 @@ export const filterOrElse: { * @category filters */ export const filterOrFail: { - ( - refinement: Predicate.Refinement, B>, - orFailWith: (a: NoInfer) => E2 - ): (self: HttpClient) => HttpClient - ( - predicate: Predicate.Predicate>, - orFailWith: (a: NoInfer) => E2 - ): (self: HttpClient) => HttpClient - ( - self: HttpClient, - refinement: Predicate.Refinement, - orFailWith: (a: A) => E2 - ): HttpClient - ( - self: HttpClient, - predicate: Predicate.Predicate, - orFailWith: (a: A) => E2 - ): HttpClient + ( + predicate: Predicate.Predicate, + orFailWith: (response: ClientResponse.HttpClientResponse) => E2 + ): (self: HttpClient) => HttpClient + ( + self: HttpClient, + predicate: Predicate.Predicate, + orFailWith: (response: ClientResponse.HttpClientResponse) => E2 + ): HttpClient } = internal.filterOrFail /** @@ -232,13 +321,8 @@ export const filterOrFail: { * @category filters */ export const filterStatus: { - ( - f: (status: number) => boolean - ): (self: HttpClient.WithResponse) => HttpClient.WithResponse - ( - self: HttpClient.WithResponse, - f: (status: number) => boolean - ): HttpClient.WithResponse + (f: (status: number) => boolean): (self: HttpClient) => HttpClient + (self: HttpClient, f: (status: number) => boolean): HttpClient } = internal.filterStatus /** @@ -247,44 +331,51 @@ export const filterStatus: { * @since 1.0.0 * @category filters */ -export const filterStatusOk: ( - self: HttpClient.WithResponse -) => HttpClient.WithResponse = internal.filterStatusOk +export const filterStatusOk: (self: HttpClient) => HttpClient = + internal.filterStatusOk /** * @since 1.0.0 * @category constructors */ -export const make: ( - execute: (request: Effect.Effect) => Effect.Effect, +export const makeWith: ( + postprocess: ( + request: Effect.Effect + ) => Effect.Effect, preprocess: HttpClient.Preprocess -) => HttpClient = internal.make +) => HttpClient = internal.makeWith /** * @since 1.0.0 * @category constructors */ -export const makeService: ( +export const make: ( f: ( request: ClientRequest.HttpClientRequest, url: URL, signal: AbortSignal, fiber: RuntimeFiber ) => Effect.Effect -) => HttpClient.Service = internal.makeService +) => HttpClient = internal.make /** * @since 1.0.0 * @category mapping & sequencing */ export const transform: { - ( - f: (effect: Effect.Effect, request: ClientRequest.HttpClientRequest) => Effect.Effect - ): (self: HttpClient) => HttpClient - ( - self: HttpClient, - f: (effect: Effect.Effect, request: ClientRequest.HttpClientRequest) => Effect.Effect - ): HttpClient + ( + f: ( + effect: Effect.Effect, + request: ClientRequest.HttpClientRequest + ) => Effect.Effect + ): (self: HttpClient) => HttpClient + ( + self: HttpClient, + f: ( + effect: Effect.Effect, + request: ClientRequest.HttpClientRequest + ) => Effect.Effect + ): HttpClient } = internal.transform /** @@ -292,39 +383,19 @@ export const transform: { * @category mapping & sequencing */ export const transformResponse: { - ( - f: (effect: Effect.Effect) => Effect.Effect - ): (self: HttpClient) => HttpClient - ( - self: HttpClient, - f: (effect: Effect.Effect) => Effect.Effect - ): HttpClient + ( + f: ( + effect: Effect.Effect + ) => Effect.Effect + ): (self: HttpClient) => HttpClient + ( + self: HttpClient, + f: ( + effect: Effect.Effect + ) => Effect.Effect + ): HttpClient } = internal.transformResponse -/** - * Transforms the result of a request. - * - * @since 1.0.0 - * @category mapping & sequencing - */ -export const map: { - (f: (a: A) => B): (self: HttpClient) => HttpClient - (self: HttpClient, f: (a: A) => B): HttpClient -} = internal.map - -/** - * Transforms the result of a request using an effectful function. - * - * @since 1.0.0 - * @category mapping & sequencing - */ -export const mapEffect: { - ( - f: (a: A) => Effect.Effect - ): (self: HttpClient) => HttpClient - (self: HttpClient, f: (a: A) => Effect.Effect): HttpClient -} = internal.mapEffect - /** * Appends a transformation of the request object before sending it. * @@ -334,11 +405,11 @@ export const mapEffect: { export const mapRequest: { ( f: (a: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest - ): (self: HttpClient) => HttpClient - ( - self: HttpClient, + ): (self: HttpClient) => HttpClient + ( + self: HttpClient, f: (a: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest - ): HttpClient + ): HttpClient } = internal.mapRequest /** @@ -350,11 +421,11 @@ export const mapRequest: { export const mapRequestEffect: { ( f: (a: ClientRequest.HttpClientRequest) => Effect.Effect - ): (self: HttpClient) => HttpClient - ( - self: HttpClient, + ): (self: HttpClient) => HttpClient + ( + self: HttpClient, f: (a: ClientRequest.HttpClientRequest) => Effect.Effect - ): HttpClient + ): HttpClient } = internal.mapRequestEffect /** @@ -366,11 +437,11 @@ export const mapRequestEffect: { export const mapRequestInput: { ( f: (a: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest - ): (self: HttpClient) => HttpClient - ( - self: HttpClient, + ): (self: HttpClient) => HttpClient + ( + self: HttpClient, f: (a: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest - ): HttpClient + ): HttpClient } = internal.mapRequestInput /** @@ -382,11 +453,11 @@ export const mapRequestInput: { export const mapRequestInputEffect: { ( f: (a: ClientRequest.HttpClientRequest) => Effect.Effect - ): (self: HttpClient) => HttpClient - ( - self: HttpClient, + ): (self: HttpClient) => HttpClient + ( + self: HttpClient, f: (a: ClientRequest.HttpClientRequest) => Effect.Effect - ): HttpClient + ): HttpClient } = internal.mapRequestInputEffect /** @@ -398,8 +469,7 @@ export declare namespace Retry { * @since 1.0.0 * @category error handling */ - export type Return> = HttpClient< - A, + export type Return> = HttpClient< | (O extends { schedule: Schedule.Schedule } ? E : O extends { until: Predicate.Refinement } ? E2 : E) @@ -419,12 +489,10 @@ export declare namespace Retry { * @category error handling */ export const retry: { - >(options: O): (self: HttpClient) => Retry.Return - ( - policy: Schedule.Schedule, R1> - ): (self: HttpClient) => HttpClient - >(self: HttpClient, options: O): Retry.Return - (self: HttpClient, policy: Schedule.Schedule): HttpClient + >(options: O): (self: HttpClient) => Retry.Return + (policy: Schedule.Schedule, R1>): (self: HttpClient) => HttpClient + >(self: HttpClient, options: O): Retry.Return + (self: HttpClient, policy: Schedule.Schedule): HttpClient } = internal.retry /** @@ -438,47 +506,15 @@ export const retryTransient: { options: | { readonly schedule?: Schedule.Schedule, R1>; readonly times?: number } | Schedule.Schedule, R1> - ): (self: HttpClient) => HttpClient - ( - self: HttpClient, + ): (self: HttpClient) => HttpClient + ( + self: HttpClient, options: | { readonly schedule?: Schedule.Schedule, R1>; readonly times?: number } | Schedule.Schedule, R1> - ): HttpClient + ): HttpClient } = internal.retryTransient -/** - * Ensures resources are properly scoped and released after execution. - * - * @since 1.0.0 - * @category resources & finalizers - */ -export const scoped: (self: HttpClient) => HttpClient> = internal.scoped - -/** - * Creates a function that validates request data against a schema before sending it. - * - * @since 1.0.0 - * @category schema - */ -export const schemaFunction: { - ( - schema: Schema.Schema, - options?: ParseOptions | undefined - ): ( - self: HttpClient - ) => ( - request: ClientRequest.HttpClientRequest - ) => (a: SA) => Effect.Effect - ( - self: HttpClient, - schema: Schema.Schema, - options?: ParseOptions | undefined - ): ( - request: ClientRequest.HttpClientRequest - ) => (a: SA) => Effect.Effect -} = internal.schemaFunction - /** * Performs an additional effect after a successful request. * @@ -486,10 +522,13 @@ export const schemaFunction: { * @category mapping & sequencing */ export const tap: { - ( - f: (a: A) => Effect.Effect<_, E2, R2> - ): (self: HttpClient) => HttpClient - (self: HttpClient, f: (a: A) => Effect.Effect<_, E2, R2>): HttpClient + <_, E2, R2>( + f: (response: ClientResponse.HttpClientResponse) => Effect.Effect<_, E2, R2> + ): (self: HttpClient) => HttpClient + ( + self: HttpClient, + f: (response: ClientResponse.HttpClientResponse) => Effect.Effect<_, E2, R2> + ): HttpClient } = internal.tap /** @@ -501,11 +540,11 @@ export const tap: { export const tapRequest: { <_, E2, R2>( f: (a: ClientRequest.HttpClientRequest) => Effect.Effect<_, E2, R2> - ): (self: HttpClient) => HttpClient - ( - self: HttpClient, + ): (self: HttpClient) => HttpClient + ( + self: HttpClient, f: (a: ClientRequest.HttpClientRequest) => Effect.Effect<_, E2, R2> - ): HttpClient + ): HttpClient } = internal.tapRequest /** @@ -515,8 +554,8 @@ export const tapRequest: { * @category cookies */ export const withCookiesRef: { - (ref: Ref): (self: HttpClient.WithResponse) => HttpClient.WithResponse - (self: HttpClient.WithResponse, ref: Ref): HttpClient.WithResponse + (ref: Ref): (self: HttpClient) => HttpClient + (self: HttpClient, ref: Ref): HttpClient } = internal.withCookiesRef /** @@ -526,8 +565,8 @@ export const withCookiesRef: { * @category redirects */ export const followRedirects: { - (maxRedirects?: number | undefined): (self: HttpClient.WithResponse) => HttpClient.WithResponse - (self: HttpClient.WithResponse, maxRedirects?: number | undefined): HttpClient.WithResponse + (maxRedirects?: number | undefined): (self: HttpClient) => HttpClient + (self: HttpClient, maxRedirects?: number | undefined): HttpClient } = internal.followRedirects /** @@ -574,5 +613,5 @@ export const withTracerPropagation: { * @since 1.0.0 */ export const layerMergedContext: ( - effect: Effect.Effect -) => Layer = internal.layerMergedContext + effect: Effect.Effect +) => Layer = internal.layerMergedContext diff --git a/packages/platform/src/HttpClientRequest.ts b/packages/platform/src/HttpClientRequest.ts index c3022a7d69..68d511f51a 100644 --- a/packages/platform/src/HttpClientRequest.ts +++ b/packages/platform/src/HttpClientRequest.ts @@ -6,16 +6,13 @@ import type * as Schema from "@effect/schema/Schema" import type * as Effect from "effect/Effect" import type { Inspectable } from "effect/Inspectable" import type * as Option from "effect/Option" +import type { Pipeable } from "effect/Pipeable" import type { Redacted } from "effect/Redacted" -import type { Scope } from "effect/Scope" import type * as Stream from "effect/Stream" import type * as PlatformError from "./Error.js" import type * as FileSystem from "./FileSystem.js" import type * as Headers from "./Headers.js" import type * as Body from "./HttpBody.js" -import type { HttpClient } from "./HttpClient.js" -import type { HttpClientError } from "./HttpClientError.js" -import type { HttpClientResponse } from "./HttpClientResponse.js" import type { HttpMethod } from "./HttpMethod.js" import * as internal from "./internal/httpClientRequest.js" import type * as UrlParams from "./UrlParams.js" @@ -36,9 +33,7 @@ export type TypeId = typeof TypeId * @since 1.0.0 * @category models */ -export interface HttpClientRequest - extends Effect.Effect, Inspectable -{ +export interface HttpClientRequest extends Inspectable, Pipeable { readonly [TypeId]: TypeId readonly method: HttpMethod readonly url: string diff --git a/packages/platform/src/HttpServer.ts b/packages/platform/src/HttpServer.ts index c972e2a5d8..695ce3e98b 100644 --- a/packages/platform/src/HttpServer.ts +++ b/packages/platform/src/HttpServer.ts @@ -201,5 +201,5 @@ export const withLogAddress: (layer: Layer.Layer) => Layer.Lay * @since 1.0.0 * @category layers */ -export const layerTestClient: Layer.Layer = +export const layerTestClient: Layer.Layer = internal.layerTestClient diff --git a/packages/platform/src/internal/fetchHttpClient.ts b/packages/platform/src/internal/fetchHttpClient.ts index 2662d13964..1438c3f51a 100644 --- a/packages/platform/src/internal/fetchHttpClient.ts +++ b/packages/platform/src/internal/fetchHttpClient.ts @@ -11,7 +11,7 @@ export const fetchTagKey = "@effect/platform/FetchHttpClient/Fetch" /** @internal */ export const requestInitTagKey = "@effect/platform/FetchHttpClient/FetchOptions" -const fetch: Client.HttpClient.Service = client.makeService((request, url, signal, fiber) => { +const fetch: Client.HttpClient = client.make((request, url, signal, fiber) => { const context = fiber.getFiberRef(FiberRef.currentContext) const fetch: typeof globalThis.fetch = context.unsafeMap.get(fetchTagKey) ?? globalThis.fetch const options: RequestInit = context.unsafeMap.get(requestInitTagKey) ?? {} diff --git a/packages/platform/src/internal/httpClient.ts b/packages/platform/src/internal/httpClient.ts index e165a60354..b171986c19 100644 --- a/packages/platform/src/internal/httpClient.ts +++ b/packages/platform/src/internal/httpClient.ts @@ -1,6 +1,3 @@ -import type { ParseOptions } from "@effect/schema/AST" -import type * as ParseResult from "@effect/schema/ParseResult" -import * as Schema from "@effect/schema/Schema" import * as Context from "effect/Context" import * as Effect from "effect/Effect" import type * as Fiber from "effect/Fiber" @@ -22,7 +19,6 @@ import type * as ClientRequest from "../HttpClientRequest.js" import type * as ClientResponse from "../HttpClientResponse.js" import * as TraceContext from "../HttpTraceContext.js" import * as UrlParams from "../UrlParams.js" -import * as internalBody from "./httpBody.js" import * as internalRequest from "./httpClientRequest.js" import * as internalResponse from "./httpClientResponse.js" @@ -32,7 +28,7 @@ export const TypeId: Client.TypeId = Symbol.for( ) as Client.TypeId /** @internal */ -export const tag = Context.GenericTag("@effect/platform/HttpClient") +export const tag = Context.GenericTag("@effect/platform/HttpClient") /** @internal */ export const currentTracerDisabledWhen = globalValue( @@ -79,43 +75,43 @@ const ClientProto = { _id: "@effect/platform/HttpClient" } }, - get(this: Client.HttpClient.Service, url: string | URL, options?: ClientRequest.Options.NoBody) { + get(this: Client.HttpClient, url: string | URL, options?: ClientRequest.Options.NoBody) { return this.execute(internalRequest.get(url, options)) }, - head(this: Client.HttpClient.Service, url: string | URL, options?: ClientRequest.Options.NoBody) { + head(this: Client.HttpClient, url: string | URL, options?: ClientRequest.Options.NoBody) { return this.execute(internalRequest.head(url, options)) }, - post(this: Client.HttpClient.Service, url: string | URL, options: ClientRequest.Options.NoUrl) { + post(this: Client.HttpClient, url: string | URL, options: ClientRequest.Options.NoUrl) { return this.execute(internalRequest.post(url, options)) }, - put(this: Client.HttpClient.Service, url: string | URL, options: ClientRequest.Options.NoUrl) { + put(this: Client.HttpClient, url: string | URL, options: ClientRequest.Options.NoUrl) { return this.execute(internalRequest.put(url, options)) }, - patch(this: Client.HttpClient.Service, url: string | URL, options: ClientRequest.Options.NoUrl) { + patch(this: Client.HttpClient, url: string | URL, options: ClientRequest.Options.NoUrl) { return this.execute(internalRequest.patch(url, options)) }, - del(this: Client.HttpClient.Service, url: string | URL, options?: ClientRequest.Options.NoUrl) { + del(this: Client.HttpClient, url: string | URL, options?: ClientRequest.Options.NoUrl) { return this.execute(internalRequest.del(url, options)) }, - options(this: Client.HttpClient.Service, url: string | URL, options?: ClientRequest.Options.NoBody) { + options(this: Client.HttpClient, url: string | URL, options?: ClientRequest.Options.NoBody) { return this.execute(internalRequest.options(url, options)) } } -const isClient = (u: unknown): u is Client.HttpClient => Predicate.hasProperty(u, TypeId) +const isClient = (u: unknown): u is Client.HttpClient => Predicate.hasProperty(u, TypeId) -interface HttpClientImpl extends Client.HttpClient { +interface HttpClientImpl extends Client.HttpClient { readonly preprocess: Client.HttpClient.Preprocess - readonly postprocess: Client.HttpClient.Postprocess + readonly postprocess: Client.HttpClient.Postprocess } /** @internal */ -export const make = ( +export const makeWith = ( postprocess: ( request: Effect.Effect - ) => Effect.Effect, + ) => Effect.Effect, preprocess: Client.HttpClient.Preprocess -): Client.HttpClient => { +): Client.HttpClient => { const self = Object.create(ClientProto) self.preprocess = preprocess self.postprocess = postprocess @@ -126,15 +122,15 @@ export const make = ( } /** @internal */ -export const makeService = ( +export const make = ( f: ( request: ClientRequest.HttpClientRequest, url: URL, signal: AbortSignal, fiber: Fiber.RuntimeFiber ) => Effect.Effect -): Client.HttpClient.Service => - make((effect) => +): Client.HttpClient => + makeWith((effect) => Effect.flatMap(effect, (request) => Effect.withFiberRuntime((fiber) => { const scope = Context.unsafeGet(fiber.getFiberRef(FiberRef.currentContext), Scope.Scope) @@ -202,24 +198,43 @@ export const makeService = ( ) })), Effect.succeed as Client.HttpClient.Preprocess) +export const { + /** @internal */ + del, + /** @internal */ + execute, + /** @internal */ + get, + /** @internal */ + head, + /** @internal */ + options, + /** @internal */ + patch, + /** @internal */ + post, + /** @internal */ + put +} = Effect.serviceFunctions(tag) + /** @internal */ export const transform = dual< - ( + ( f: ( - effect: Effect.Effect, + effect: Effect.Effect, request: ClientRequest.HttpClientRequest - ) => Effect.Effect - ) => (self: Client.HttpClient) => Client.HttpClient, - ( - self: Client.HttpClient, + ) => Effect.Effect + ) => (self: Client.HttpClient) => Client.HttpClient, + ( + self: Client.HttpClient, f: ( - effect: Effect.Effect, + effect: Effect.Effect, request: ClientRequest.HttpClientRequest - ) => Effect.Effect - ) => Client.HttpClient + ) => Effect.Effect + ) => Client.HttpClient >(2, (self, f) => { - const client = self as HttpClientImpl - return make( + const client = self as HttpClientImpl + return makeWith( Effect.flatMap((request) => f(client.postprocess(Effect.succeed(request)), request)), client.preprocess ) @@ -230,70 +245,68 @@ export const filterStatus = dual< ( f: (status: number) => boolean ) => ( - self: Client.HttpClient.WithResponse - ) => Client.HttpClient.WithResponse, + self: Client.HttpClient + ) => Client.HttpClient, ( - self: Client.HttpClient.WithResponse, + self: Client.HttpClient, f: (status: number) => boolean - ) => Client.HttpClient.WithResponse + ) => Client.HttpClient >(2, (self, f) => transformResponse(self, Effect.flatMap(internalResponse.filterStatus(f)))) /** @internal */ export const filterStatusOk = ( - self: Client.HttpClient.WithResponse -): Client.HttpClient.WithResponse => + self: Client.HttpClient +): Client.HttpClient => transformResponse(self, Effect.flatMap(internalResponse.filterStatusOk)) /** @internal */ export const transformResponse = dual< - ( - f: (effect: Effect.Effect) => Effect.Effect - ) => (self: Client.HttpClient) => Client.HttpClient, - ( - self: Client.HttpClient, - f: (effect: Effect.Effect) => Effect.Effect - ) => Client.HttpClient + ( + f: ( + effect: Effect.Effect + ) => Effect.Effect + ) => (self: Client.HttpClient) => Client.HttpClient, + ( + self: Client.HttpClient, + f: ( + effect: Effect.Effect + ) => Effect.Effect + ) => Client.HttpClient >(2, (self, f) => { - const client = self as HttpClientImpl - return make((request) => f(client.postprocess(request)), client.preprocess) + const client = self as HttpClientImpl + return makeWith((request) => f(client.postprocess(request)), client.preprocess) }) /** @internal */ export const catchTag: { - ( + ( tag: K, - f: (e: Extract) => Effect.Effect - ): ( - self: Client.HttpClient - ) => Client.HttpClient, R1 | R> + f: (e: Extract) => Effect.Effect + ): (self: Client.HttpClient) => Client.HttpClient, R1 | R> < R, E, - A, K extends E extends { _tag: string } ? E["_tag"] : never, - A1, R1, E1 >( - self: Client.HttpClient, + self: Client.HttpClient, tag: K, - f: (e: Extract) => Effect.Effect - ): Client.HttpClient, R1 | R> + f: (e: Extract) => Effect.Effect + ): Client.HttpClient, R1 | R> } = dual( 3, < R, E, - A, K extends E extends { _tag: string } ? E["_tag"] : never, R1, - E1, - A1 + E1 >( - self: Client.HttpClient, + self: Client.HttpClient, tag: K, - f: (e: Extract) => Effect.Effect - ): Client.HttpClient, R1 | R> => transformResponse(self, Effect.catchTag(tag, f)) + f: (e: Extract) => Effect.Effect + ): Client.HttpClient, R1 | R> => transformResponse(self, Effect.catchTag(tag, f)) ) /** @internal */ @@ -304,7 +317,7 @@ export const catchTags: { & { [K in Extract["_tag"]]+?: ( error: Extract - ) => Effect.Effect + ) => Effect.Effect } & (unknown extends E ? {} : { @@ -317,16 +330,7 @@ export const catchTags: { }) >( cases: Cases - ): ( - self: Client.HttpClient - ) => Client.HttpClient< - | A - | { - [K in keyof Cases]: Cases[K] extends ( - ...args: Array - ) => Effect.Effect ? A - : never - }[keyof Cases], + ): (self: Client.HttpClient) => Client.HttpClient< | Exclude | { [K in keyof Cases]: Cases[K] extends ( @@ -343,14 +347,13 @@ export const catchTags: { }[keyof Cases] > < - A, E extends { _tag: string }, R, Cases extends & { [K in Extract["_tag"]]+?: ( error: Extract - ) => Effect.Effect + ) => Effect.Effect } & (unknown extends E ? {} : { @@ -362,16 +365,9 @@ export const catchTags: { ]: never }) >( - self: Client.HttpClient, + self: Client.HttpClient, cases: Cases ): Client.HttpClient< - | A - | { - [K in keyof Cases]: Cases[K] extends ( - ...args: Array - ) => Effect.Effect ? A - : never - }[keyof Cases], | Exclude | { [K in keyof Cases]: Cases[K] extends ( @@ -390,14 +386,13 @@ export const catchTags: { } = dual( 2, < - A, E extends { _tag: string }, R, Cases extends & { [K in Extract["_tag"]]+?: ( error: Extract - ) => Effect.Effect + ) => Effect.Effect } & (unknown extends E ? {} : { @@ -409,16 +404,9 @@ export const catchTags: { ]: never }) >( - self: Client.HttpClient, + self: Client.HttpClient, cases: Cases ): Client.HttpClient< - | A - | { - [K in keyof Cases]: Cases[K] extends ( - ...args: Array - ) => Effect.Effect ? A - : never - }[keyof Cases], | Exclude | { [K in keyof Cases]: Cases[K] extends ( @@ -433,122 +421,66 @@ export const catchTags: { ) => Effect.Effect ? R : never }[keyof Cases] - > => transformResponse(self, Effect.catchTags(cases)) + > => transformResponse(self, Effect.catchTags(cases) as any) ) /** @internal */ export const catchAll: { - ( - f: (e: E) => Effect.Effect - ): (self: Client.HttpClient) => Client.HttpClient - ( - self: Client.HttpClient, - f: (e: E) => Effect.Effect - ): Client.HttpClient + ( + f: (e: E) => Effect.Effect + ): (self: Client.HttpClient) => Client.HttpClient + ( + self: Client.HttpClient, + f: (e: E) => Effect.Effect + ): Client.HttpClient } = dual( 2, - ( - self: Client.HttpClient, - f: (e: E) => Effect.Effect - ): Client.HttpClient => transformResponse(self, Effect.catchAll(f)) + ( + self: Client.HttpClient, + f: (e: E) => Effect.Effect + ): Client.HttpClient => transformResponse(self, Effect.catchAll(f)) ) /** @internal */ export const filterOrElse: { - ( - refinement: Predicate.Refinement, B>, - orElse: (a: NoInfer) => Effect.Effect - ): (self: Client.HttpClient) => Client.HttpClient - ( - predicate: Predicate.Predicate>, - orElse: (a: NoInfer) => Effect.Effect + ( + predicate: Predicate.Predicate, + orElse: (response: ClientResponse.HttpClientResponse) => Effect.Effect ): ( - self: Client.HttpClient - ) => Client.HttpClient - ( - self: Client.HttpClient, - refinement: Predicate.Refinement, - orElse: (a: A) => Effect.Effect - ): Client.HttpClient - ( - self: Client.HttpClient, - predicate: Predicate.Predicate, - orElse: (a: A) => Effect.Effect - ): Client.HttpClient + self: Client.HttpClient + ) => Client.HttpClient + ( + self: Client.HttpClient, + predicate: Predicate.Predicate, + orElse: (response: ClientResponse.HttpClientResponse) => Effect.Effect + ): Client.HttpClient } = dual(3, (self, f, orElse) => transformResponse(self, Effect.filterOrElse(f, orElse))) /** @internal */ export const filterOrFail: { - ( - refinement: Predicate.Refinement, B>, - orFailWith: (a: NoInfer) => E2 - ): (self: Client.HttpClient) => Client.HttpClient - ( - predicate: Predicate.Predicate>, - orFailWith: (a: NoInfer) => E2 - ): (self: Client.HttpClient) => Client.HttpClient - ( - self: Client.HttpClient, - refinement: Predicate.Refinement, - orFailWith: (a: A) => E2 - ): Client.HttpClient - ( - self: Client.HttpClient, - predicate: Predicate.Predicate, - orFailWith: (a: A) => E2 - ): Client.HttpClient + ( + predicate: Predicate.Predicate, + orFailWith: (response: ClientResponse.HttpClientResponse) => E2 + ): (self: Client.HttpClient) => Client.HttpClient + ( + self: Client.HttpClient, + predicate: Predicate.Predicate, + orFailWith: (response: ClientResponse.HttpClientResponse) => E2 + ): Client.HttpClient } = dual(3, (self, f, orFailWith) => transformResponse(self, Effect.filterOrFail(f, orFailWith))) -/** @internal */ -export const map = dual< - ( - f: (a: A) => B - ) => (self: Client.HttpClient) => Client.HttpClient, - ( - self: Client.HttpClient, - f: (a: A) => B - ) => Client.HttpClient ->(2, (self, f) => transformResponse(self, Effect.map(f))) - -/** @internal */ -export const mapEffect = dual< - ( - f: (a: A) => Effect.Effect - ) => (self: Client.HttpClient) => Client.HttpClient, - ( - self: Client.HttpClient, - f: (a: A) => Effect.Effect - ) => Client.HttpClient ->(2, (self, f) => transformResponse(self, Effect.flatMap(f))) - -/** @internal */ -export const scoped = ( - self: Client.HttpClient -): Client.HttpClient> => transformResponse(self, Effect.scoped) - -/** @internal */ -export const mapEffectScoped = dual< - ( - f: (a: A) => Effect.Effect - ) => (self: Client.HttpClient) => Client.HttpClient>, - ( - self: Client.HttpClient, - f: (a: A) => Effect.Effect - ) => Client.HttpClient> ->(2, (self, f) => scoped(mapEffect(self, f))) - /** @internal */ export const mapRequest = dual< ( f: (a: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest - ) => (self: Client.HttpClient) => Client.HttpClient, - ( - self: Client.HttpClient, + ) => (self: Client.HttpClient) => Client.HttpClient, + ( + self: Client.HttpClient, f: (a: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest - ) => Client.HttpClient + ) => Client.HttpClient >(2, (self, f) => { - const client = self as HttpClientImpl - return make(client.postprocess, (request) => Effect.map(client.preprocess(request), f)) + const client = self as HttpClientImpl + return makeWith(client.postprocess, (request) => Effect.map(client.preprocess(request), f)) }) /** @internal */ @@ -557,32 +489,32 @@ export const mapRequestEffect = dual< f: ( a: ClientRequest.HttpClientRequest ) => Effect.Effect - ) => ( - self: Client.HttpClient - ) => Client.HttpClient, - ( - self: Client.HttpClient, + ) => ( + self: Client.HttpClient + ) => Client.HttpClient, + ( + self: Client.HttpClient, f: ( a: ClientRequest.HttpClientRequest ) => Effect.Effect - ) => Client.HttpClient + ) => Client.HttpClient >(2, (self, f) => { - const client = self as HttpClientImpl - return make(client.postprocess as any, (request) => Effect.flatMap(client.preprocess(request), f)) + const client = self as HttpClientImpl + return makeWith(client.postprocess as any, (request) => Effect.flatMap(client.preprocess(request), f)) }) /** @internal */ export const mapRequestInput = dual< ( f: (a: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest - ) => (self: Client.HttpClient) => Client.HttpClient, - ( - self: Client.HttpClient, + ) => (self: Client.HttpClient) => Client.HttpClient, + ( + self: Client.HttpClient, f: (a: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest - ) => Client.HttpClient + ) => Client.HttpClient >(2, (self, f) => { - const client = self as HttpClientImpl - return make(client.postprocess, (request) => client.preprocess(f(request))) + const client = self as HttpClientImpl + return makeWith(client.postprocess, (request) => client.preprocess(f(request))) }) /** @internal */ @@ -591,44 +523,42 @@ export const mapRequestInputEffect = dual< f: ( a: ClientRequest.HttpClientRequest ) => Effect.Effect - ) => ( - self: Client.HttpClient - ) => Client.HttpClient, - ( - self: Client.HttpClient, + ) => ( + self: Client.HttpClient + ) => Client.HttpClient, + ( + self: Client.HttpClient, f: ( a: ClientRequest.HttpClientRequest ) => Effect.Effect - ) => Client.HttpClient + ) => Client.HttpClient >(2, (self, f) => { - const client = self as HttpClientImpl - return make(client.postprocess as any, (request) => Effect.flatMap(f(request), client.preprocess)) + const client = self as HttpClientImpl + return makeWith(client.postprocess as any, (request) => Effect.flatMap(f(request), client.preprocess)) }) /** @internal */ export const retry: { >( options: O - ): ( - self: Client.HttpClient - ) => Client.Retry.Return + ): (self: Client.HttpClient) => Client.Retry.Return ( policy: Schedule.Schedule, R1> - ): (self: Client.HttpClient) => Client.HttpClient - >( - self: Client.HttpClient, + ): (self: Client.HttpClient) => Client.HttpClient + >( + self: Client.HttpClient, options: O - ): Client.Retry.Return - ( - self: Client.HttpClient, + ): Client.Retry.Return + ( + self: Client.HttpClient, policy: Schedule.Schedule - ): Client.HttpClient + ): Client.HttpClient } = dual( 2, - ( - self: Client.HttpClient, + ( + self: Client.HttpClient, policy: Schedule.Schedule - ): Client.HttpClient => transformResponse(self, Effect.retry(policy)) + ): Client.HttpClient => transformResponse(self, Effect.retry(policy)) ) /** @internal */ @@ -638,23 +568,23 @@ export const retryTransient: { readonly schedule?: Schedule.Schedule, R1> readonly times?: number } | Schedule.Schedule, R1> - ): (self: Client.HttpClient) => Client.HttpClient - ( - self: Client.HttpClient, + ): (self: Client.HttpClient) => Client.HttpClient + ( + self: Client.HttpClient, options: { readonly schedule?: Schedule.Schedule, R1> readonly times?: number } | Schedule.Schedule, R1> - ): Client.HttpClient + ): Client.HttpClient } = dual( 2, - ( - self: Client.HttpClient, + ( + self: Client.HttpClient, options: { readonly schedule?: Schedule.Schedule, R1> readonly times?: number } | Schedule.Schedule, R1> - ): Client.HttpClient => + ): Client.HttpClient => transformResponse( self, Effect.retry({ @@ -668,94 +598,50 @@ export const retryTransient: { ) ) -/** @internal */ -export const schemaFunction = dual< - ( - schema: Schema.Schema, - options?: ParseOptions | undefined - ) => ( - self: Client.HttpClient - ) => ( - request: ClientRequest.HttpClientRequest - ) => ( - a: SA - ) => Effect.Effect, - ( - self: Client.HttpClient, - schema: Schema.Schema, - options?: ParseOptions | undefined - ) => ( - request: ClientRequest.HttpClientRequest - ) => ( - a: SA - ) => Effect.Effect ->((args) => isClient(args[0]), (self, schema, options) => { - const encode = Schema.encode(schema, options) - return (request) => (a) => - Effect.flatMap( - Effect.tryMap(encode(a), { - try: (body) => new TextEncoder().encode(JSON.stringify(body)), - catch: (cause) => - new Error.RequestError({ - request, - reason: "Encode", - cause - }) - }), - (body) => - self.execute( - internalRequest.setBody( - request, - internalBody.uint8Array(body, "application/json") - ) - ) - ) -}) - /** @internal */ export const tap = dual< - ( - f: (a: A) => Effect.Effect<_, E2, R2> - ) => (self: Client.HttpClient) => Client.HttpClient, - ( - self: Client.HttpClient, - f: (a: A) => Effect.Effect<_, E2, R2> - ) => Client.HttpClient + <_, E2, R2>( + f: (response: ClientResponse.HttpClientResponse) => Effect.Effect<_, E2, R2> + ) => (self: Client.HttpClient) => Client.HttpClient, + ( + self: Client.HttpClient, + f: (response: ClientResponse.HttpClientResponse) => Effect.Effect<_, E2, R2> + ) => Client.HttpClient >(2, (self, f) => transformResponse(self, Effect.tap(f))) /** @internal */ export const tapRequest = dual< <_, E2, R2>( f: (a: ClientRequest.HttpClientRequest) => Effect.Effect<_, E2, R2> - ) => ( - self: Client.HttpClient - ) => Client.HttpClient, - ( - self: Client.HttpClient, + ) => ( + self: Client.HttpClient + ) => Client.HttpClient, + ( + self: Client.HttpClient, f: (a: ClientRequest.HttpClientRequest) => Effect.Effect<_, E2, R2> - ) => Client.HttpClient + ) => Client.HttpClient >(2, (self, f) => { - const client = self as HttpClientImpl - return make(client.postprocess as any, (request) => Effect.tap(client.preprocess(request), f)) + const client = self as HttpClientImpl + return makeWith(client.postprocess as any, (request) => Effect.tap(client.preprocess(request), f)) }) /** @internal */ export const withCookiesRef = dual< ( ref: Ref.Ref - ) => (self: Client.HttpClient.WithResponse) => Client.HttpClient.WithResponse, + ) => (self: Client.HttpClient) => Client.HttpClient, ( - self: Client.HttpClient.WithResponse, + self: Client.HttpClient, ref: Ref.Ref - ) => Client.HttpClient.WithResponse + ) => Client.HttpClient >( 2, ( - self: Client.HttpClient.WithResponse, + self: Client.HttpClient, ref: Ref.Ref - ): Client.HttpClient.WithResponse => { - const client = self as HttpClientImpl - return make( + ): Client.HttpClient => { + const client = self as HttpClientImpl + return makeWith( (request: Effect.Effect) => Effect.tap( client.postprocess(request), @@ -778,17 +664,17 @@ export const withCookiesRef = dual< export const followRedirects = dual< ( maxRedirects?: number | undefined - ) => (self: Client.HttpClient.WithResponse) => Client.HttpClient.WithResponse, + ) => (self: Client.HttpClient) => Client.HttpClient, ( - self: Client.HttpClient.WithResponse, + self: Client.HttpClient, maxRedirects?: number | undefined - ) => Client.HttpClient.WithResponse + ) => Client.HttpClient >((args) => isClient(args[0]), ( - self: Client.HttpClient.WithResponse, + self: Client.HttpClient, maxRedirects?: number | undefined -): Client.HttpClient.WithResponse => { - const client = self as HttpClientImpl - return make( +): Client.HttpClient => { + const client = self as HttpClientImpl + return makeWith( (request) => { const loop = ( request: ClientRequest.HttpClientRequest, @@ -815,7 +701,9 @@ export const followRedirects = dual< }) /** @internal */ -export const layerMergedContext = (effect: Effect.Effect) => +export const layerMergedContext = ( + effect: Effect.Effect +): Layer.Layer => Layer.effect( tag, Effect.flatMap(Effect.context(), (context) => diff --git a/packages/platform/src/internal/httpClientRequest.ts b/packages/platform/src/internal/httpClientRequest.ts index c1ee92eb00..7f5aa36565 100644 --- a/packages/platform/src/internal/httpClientRequest.ts +++ b/packages/platform/src/internal/httpClientRequest.ts @@ -1,18 +1,16 @@ import type { ParseOptions } from "@effect/schema/AST" import type * as Schema from "@effect/schema/Schema" -import * as Context from "effect/Context" import * as Effect from "effect/Effect" -import * as Effectable from "effect/Effectable" import { dual } from "effect/Function" import * as Inspectable from "effect/Inspectable" import * as Option from "effect/Option" +import { pipeArguments } from "effect/Pipeable" import * as Redacted from "effect/Redacted" import type * as Stream from "effect/Stream" import type * as PlatformError from "../Error.js" import type * as FileSystem from "../FileSystem.js" import * as Headers from "../Headers.js" import type * as Body from "../HttpBody.js" -import type { HttpClient } from "../HttpClient.js" import type * as ClientRequest from "../HttpClientRequest.js" import type { HttpMethod } from "../HttpMethod.js" import * as UrlParams from "../UrlParams.js" @@ -21,16 +19,9 @@ import * as internalBody from "./httpBody.js" /** @internal */ export const TypeId: ClientRequest.TypeId = Symbol.for("@effect/platform/HttpClientRequest") as ClientRequest.TypeId -/** @internal */ -export const clientTag = Context.GenericTag("@effect/platform/HttpClient") - const Proto = { [TypeId]: TypeId, - ...Effectable.CommitPrototype, ...Inspectable.BaseProto, - commit(this: ClientRequest.HttpClientRequest) { - return Effect.flatMap(clientTag, (client) => client.execute(this)) - }, toJSON(this: ClientRequest.HttpClientRequest): unknown { return { _id: "@effect/platform/HttpClientRequest", @@ -41,6 +32,9 @@ const Proto = { headers: this.headers, body: this.body.toJSON() } + }, + pipe() { + return pipeArguments(this, arguments) } } diff --git a/packages/platform/test/HttpClient.test.ts b/packages/platform/test/HttpClient.test.ts index 59b0647f1a..a27fa7afa4 100644 --- a/packages/platform/test/HttpClient.test.ts +++ b/packages/platform/test/HttpClient.test.ts @@ -8,7 +8,7 @@ import { } from "@effect/platform" import * as Schema from "@effect/schema/Schema" import { assert, describe, expect, it } from "@effect/vitest" -import { Either, Ref } from "effect" +import { Either, Ref, Struct } from "effect" import * as Context from "effect/Context" import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" @@ -21,6 +21,9 @@ const Todo = Schema.Struct({ title: Schema.String, completed: Schema.Boolean }) +const TodoWithoutId = Schema.Struct({ + ...Struct.omit(Todo.fields, "id") +}) const OkTodo = Schema.Struct({ status: Schema.Literal(200), body: Todo @@ -31,19 +34,15 @@ const makeJsonPlaceholder = Effect.gen(function*(_) { const client = defaultClient.pipe( HttpClient.mapRequest(HttpClientRequest.prependUrl("https://jsonplaceholder.typicode.com")) ) - const todoClient = client.pipe( - HttpClient.mapEffect(HttpClientResponse.schemaBodyJson(Todo)), - HttpClient.scoped - ) - const createTodo = HttpClientRequest.post("/todos").pipe( - HttpClient.schemaFunction( - todoClient, - Todo.pipe(Schema.omit("id")) + const createTodo = (todo: typeof TodoWithoutId.Type) => + HttpClientRequest.post("/todos").pipe( + HttpClientRequest.schemaBodyJson(TodoWithoutId)(todo), + Effect.flatMap(client.execute), + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)), + Effect.scoped ) - ) return { client, - todoClient, createTodo } as const }) @@ -56,7 +55,7 @@ describe("HttpClient", () => { it("google", () => Effect.gen(function*(_) { const response = yield* _( - HttpClientRequest.get("https://www.google.com/"), + HttpClient.get("https://www.google.com/"), Effect.flatMap((_) => _.text), Effect.scoped ) @@ -91,7 +90,7 @@ describe("HttpClient", () => { it("google stream", () => Effect.gen(function*(_) { const response = yield* _( - HttpClientRequest.get(new URL("https://www.google.com/")), + HttpClient.get(new URL("https://www.google.com/")), Effect.map((_) => _.stream), Stream.unwrapScoped, Stream.runFold("", (a, b) => a + new TextDecoder().decode(b)) @@ -102,7 +101,10 @@ describe("HttpClient", () => { it("jsonplaceholder", () => Effect.gen(function*() { const jp = yield* JsonPlaceholder - const response = yield* jp.todoClient.get("/todos/1") + const response = yield* jp.client.get("/todos/1").pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)), + Effect.scoped + ) expect(response.id).toBe(1) }).pipe(Effect.provide(JsonPlaceholderLive), Effect.runPromise)) @@ -118,14 +120,13 @@ describe("HttpClient", () => { }).pipe(Effect.provide(JsonPlaceholderLive), Effect.runPromise)) it("jsonplaceholder schemaJson", () => - Effect.gen(function*(_) { - const jp = yield* _(JsonPlaceholder) - const client = HttpClient.mapEffect(jp.client, HttpClientResponse.schemaJson(OkTodo)).pipe( - HttpClient.scoped, - HttpClient.map((_) => _.body) + Effect.gen(function*() { + const jp = yield* JsonPlaceholder + const response = yield* jp.client.get("/todos/1").pipe( + Effect.flatMap(HttpClientResponse.schemaJson(OkTodo)), + Effect.scoped ) - const response = yield* client.get("/todos/1") - expect(response.id).toBe(1) + expect(response.body.id).toBe(1) }).pipe(Effect.provide(JsonPlaceholderLive), Effect.runPromise)) it("request processing order", () => @@ -135,11 +136,10 @@ describe("HttpClient", () => { HttpClient.mapRequest(HttpClientRequest.prependUrl("jsonplaceholder.typicode.com")), HttpClient.mapRequest(HttpClientRequest.prependUrl("https://")) ) - const todoClient = client.pipe( - HttpClient.mapEffect(HttpClientResponse.schemaBodyJson(Todo)), - HttpClient.scoped + const response = yield* client.get("/todos/1").pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)), + Effect.scoped ) - const response = yield* todoClient.get("/todos/1") expect(response.id).toBe(1) }).pipe(Effect.provide(FetchHttpClient.layer), Effect.runPromise)) diff --git a/packages/rpc-http/src/HttpRpcResolver.ts b/packages/rpc-http/src/HttpRpcResolver.ts index 76caed75b8..5fea8629a8 100644 --- a/packages/rpc-http/src/HttpRpcResolver.ts +++ b/packages/rpc-http/src/HttpRpcResolver.ts @@ -19,7 +19,7 @@ import * as Stream from "effect/Stream" * @since 1.0.0 */ export const make = >( - client: Client.HttpClient.Service + client: Client.HttpClient ): RequestResolver.RequestResolver< Rpc.Request>, Serializable.SerializableWithResult.Context> diff --git a/packages/rpc-http/src/HttpRpcResolverNoStream.ts b/packages/rpc-http/src/HttpRpcResolverNoStream.ts index 746b8a6243..6850d52810 100644 --- a/packages/rpc-http/src/HttpRpcResolverNoStream.ts +++ b/packages/rpc-http/src/HttpRpcResolverNoStream.ts @@ -18,7 +18,7 @@ import * as Schedule from "effect/Schedule" * @since 1.0.0 */ export const make = >( - client: Client.HttpClient.Service + client: Client.HttpClient ): RequestResolver.RequestResolver< Rpc.Request>, Serializable.SerializableWithResult.Context>