Skip to content

Commit

Permalink
refactor: simplify response manipulation API
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshuaAmaju committed Aug 28, 2024
1 parent 10ad516 commit 4a12bc5
Show file tree
Hide file tree
Showing 11 changed files with 205 additions and 97 deletions.
5 changes: 5 additions & 0 deletions .changeset/red-ants-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"fetch-prime": minor
---

Simplify response manipulation API
30 changes: 16 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,27 @@ import * as E from "fp-ts/Either";
import { fetch } from "fetch-prime/Fetch";
import adapter from "fetch-prime/Adapters/Platform";

const response = await fetch("/users")(adapter);
const request = await fetch("/users")(adapter);
const {response} = request;

if (E.isRight(response) && response.right.ok) {
const users = await response.right.json();
}

// or
import { chain } from "fetch-prime/Function";
import * as Response from "fetch-prime/Response";
import { andThen } from "fetch-prime/Function";
import {filterStatusOk} from "fetch-prime/Response";

const request = await fetch("/users")(adapter);
const ok = andThen(request.response, filterStatusOk);
const users = await andThen(ok, (res) => res.json());

const result = await fetch("/users")(adapter);
const ok = E.chainW(Response.filterStatusOk)(result);
const users = await chain(ok, (res) => res.json());
// or
const response = await fetch("/users")(adapter);
const users = await response.ok((res) => res.json());
```

### With interceptor
## With interceptor

```ts
import * as Interceptor from "fetch-prime/Interceptor";
Expand Down Expand Up @@ -86,10 +91,7 @@ Instead of checking if the response is ok i.e 200

```ts
const response = await fetch("/users")(adapter);

if (E.isRight(response) && response.right.ok) {
const users = await response.json();
}
const users = await response.ok(res => res.json());
```

We can delegate that to a response interceptor that performs that check.
Expand All @@ -99,10 +101,10 @@ const interceptors = Interceptor.of(StatusOK);

const interceptor = Interceptor.make(interceptors);

const adapter = interceptor(adapter);
const adapter = interceptor(Adapter);

const request = await fetch("/users")(adapter);
const users = await chain(request, (res) => res.json());
const response = await fetch("/users")(adapter);
const users = await response.json();
// ...
```

Expand Down
6 changes: 3 additions & 3 deletions src/Fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { Either } from "fp-ts/Either";
import { HttpError } from "./internal/error.js";
import * as core from "./internal/fetch.js";
import { HttpRequest } from "./internal/request.js";
import { HttpResponse } from "./internal/response/index.js";
import { HttpResponseEither } from "./internal/response/index.js";

/**
* @since 0.0.1
Expand Down Expand Up @@ -42,8 +42,8 @@ export const fetch_: (
*/
export const fetch: (
url: string | URL,
init?: RequestInit | undefined
) => <E>(fetch: Fetch<E>) => Promise<Either<E | HttpError, HttpResponse>> =
init?: RequestInit
) => <E>(fetch: Fetch<E>) => Promise<HttpResponseEither<E | HttpError>> =
core.fetch;

/**
Expand Down
12 changes: 10 additions & 2 deletions src/Function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,20 @@ import { dual } from "./internal/utils.js";
* @since 0.0.1
* @category combinator
*/
export const chain: {
export const andThen: {
<E1, A, B>(fn: (res: A) => Either<E1, B>): <E>(
response: Either<E, A>
) => Either<E | E1, B>;
<E, A, E1, B>(response: Either<E, A>, fn: (res: A) => Either<E1, B>): Either<
E | E1,
B
>;

<E1, A, B>(fn: (res: A) => Promise<Either<E1, B>>): <E>(
response: Either<E, A>
) => Promise<Either<E | E1, B>>;
<E, A, E1, B>(
response: Either<E, A>,
fn: (res: A) => Promise<Either<E1, B>>
): Promise<Either<E | E1, B>>;
} = dual(2, (response, fn) => core.chain(response, fn));
} = dual(2, (response, fn) => core.andThen(response, fn));
6 changes: 6 additions & 0 deletions src/Response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ export {
* @category model
*/
HttpResponse,

/**
* @since 0.1.0
* @category model
*/
HttpResponseEither,
} from "./internal/response/index.js";

/**
Expand Down
6 changes: 4 additions & 2 deletions src/internal/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { map as mapE, isLeft, Either } from "fp-ts/Either";

import { Fetch } from "../Fetch.js";
import { HttpResponse } from "./response/index.js";
import { HttpResponse, HttpResponseEither } from "./response/index.js";
import { HttpError } from "./error.js";

export const raw = (url: string | URL, init?: RequestInit) => {
Expand All @@ -11,7 +11,9 @@ export const raw = (url: string | URL, init?: RequestInit) => {
export const fetch = (url: string | URL, init?: RequestInit) => {
return async <E>(fetch: Fetch<E>) => {
const res = await fetch(url, init);
return mapE((res: Response) => new HttpResponse(res))(res);
return new HttpResponseEither(
mapE((res: Response) => new HttpResponse(res))(res)
);
};
};

Expand Down
9 changes: 4 additions & 5 deletions src/internal/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ import { isLeft, Either } from "fp-ts/Either";
// return result;
// };

export const chain = async <E, A, E1, B>(
export const andThen = <E, A, E1, B>(
response: Either<E, A>,
fn: (res: A) => Promise<Either<E1, B>>
): Promise<Either<E | E1, B>> => {
fn: (res: A) => Either<E1, B> | Promise<Either<E1, B>>
): Either<E | E1, B> | Promise<Either<E | E1, B>> => {
if (isLeft(response)) return response;
const result = await fn(response.right);
return result;
return fn(response.right);
};
88 changes: 87 additions & 1 deletion src/internal/response/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { left, right, bimap, tryCatch, Either } from "fp-ts/Either";
import {
map,
left,
right,
bimap,
chainW,
isLeft,
tryCatch,
Either,
} from "fp-ts/Either";

import { decode } from "../utils.js";
import { StatusCode, StatusNotOK, StatusOK } from "./types.js";
import { andThen } from "../function.js";

export class StatusError {
readonly _tag = "StatusError";
Expand Down Expand Up @@ -122,3 +132,79 @@ export class HttpResponse {
return text(this.response);
}
}

export class HttpResponseEither<E> {
constructor(readonly response: Either<E, HttpResponse>) {}

private map<A>(fn: (r: HttpResponse) => A) {
return map((r: HttpResponse) => fn(r))(this.response);
}

private chain<E1, A>(fn: (r: HttpResponse) => Either<E | E1, A>) {
return chainW((r: HttpResponse) => fn(r))(this.response);
}

async ok<E1, A>(
fn: (self: HttpResponse) => Promise<Either<E1, A>>
): Promise<Either<E | E1 | HttpResponse, A>> {
const res = this.response;
if (isLeft(res)) return res;
return res.right.ok ? fn(res.right) : left(res.right);
}

get headers() {
return this.map((_) => _.headers);
}

get redirected() {
return this.map((_) => _.redirected);
}

get status() {
return this.map((_) => _.status);
}

get statusText() {
return this.map((_) => _.statusText);
}

get type() {
return this.map((_) => _.type);
}

get url() {
return this.map((_) => _.url);
}

get body() {
return this.map((_) => _.body);
}

get bodyUsed() {
return this.map((_) => _.bodyUsed);
}

clone() {
return this.chain((_) => _.clone());
}

arrayBuffer() {
return andThen(this.response, (_) => arrayBuffer(_.response));
}

blob() {
return andThen(this.response, (_) => blob(_.response));
}

formData() {
return andThen(this.response, (_) => formData(_.response));
}

json() {
return andThen(this.response, (_) => json(_.response));
}

text() {
return andThen(this.response, (_) => text(_.response));
}
}
22 changes: 11 additions & 11 deletions test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { describe, expect, test } from "vitest";
import { pipe } from "fp-ts/function";
import * as E from "fp-ts/Either";

import { chain } from '../src/Function.js'
import { andThen } from "../src/Function.js";
import PlatformAdapter from "../src/Adapters/Platform.js";
import * as Http from "../src/Client.js";
import * as Interceptor from "../src/Interceptor.js";
Expand All @@ -23,9 +23,9 @@ test("should make client with http methods", async () => {
const adapter = Interceptor.make(interceptors)(PlatformAdapter);

const res = await Http.get("/users/2")(adapter);
const json = await chain(res, Response.json)
const json = await andThen(res, Response.json);

const result = (json as Extract<typeof result, { _tag: "Right" }>)
const result = json as Extract<typeof result, { _tag: "Right" }>;

expect(result.right.data.id).toBe(2);
});
Expand All @@ -35,7 +35,7 @@ test("should make client with base URL for every request", async () => {

const res = await Http.get("/users/2")(client);

const result = await chain(res, Response.json)
const result = await andThen(res, Response.json);

expect((result as E.Right<any>).right.data.id).toBe(2);
});
Expand All @@ -46,7 +46,7 @@ test("should make client with interceptors", async () => {

const res = await Http.get("/users/2")(client);

const result = await chain(res, Response.json)
const result = await andThen(res, Response.json);

expect((result as E.Right<any>).right.data.id).toBe(2);
});
Expand All @@ -56,7 +56,7 @@ test("should attach JSON body and headers", async () => {
let headers;

const spy = (chain: Interceptor.Chain) => {
body = chain.request.init?.body
body = chain.request.init?.body;
headers = new Headers(chain.request.init?.headers);
return chain.proceed(chain.request);
};
Expand All @@ -68,11 +68,11 @@ test("should attach JSON body and headers", async () => {

const adapter = Interceptor.make(interceptors)(PlatformAdapter);

const body_json = json({ name: "morpheus", job: "leader" })
const body_json = json({ name: "morpheus", job: "leader" });

const res = await Http.post("/users", body_json)(adapter);

const result = await chain(res, Response.json)
const result = await andThen(res, Response.json);

expect((result as E.Right<any>).right).toMatchObject({
name: "morpheus",
Expand Down Expand Up @@ -104,7 +104,7 @@ test("should attach JSON body and headers with custom headers", async () => {
body: json({ name: "morpheus", job: "leader" }),
})(adapter);

const result = await chain(res, Response.json)
const result = await andThen(res, Response.json);

expect((result as E.Right<any>).right).toMatchObject({
name: "morpheus",
Expand Down Expand Up @@ -137,7 +137,7 @@ describe("method", () => {
let method;

const check = async (chain: Interceptor.Chain) => {
method = chain.request.init?.method
method = chain.request.init?.method;
return E.right(new globalThis.Response(""));
};

Expand All @@ -154,7 +154,7 @@ describe("method", () => {
let method;

const check = async (chain: Interceptor.Chain) => {
method = chain.request.init?.method
method = chain.request.init?.method;
return E.right(new globalThis.Response(""));
};

Expand Down
Loading

0 comments on commit 4a12bc5

Please sign in to comment.