diff --git a/CHANGELOG.md b/CHANGELOG.md
index b4882a41..090a737b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog
+## [5.1.0](https://github.com/contactlab/appy/releases/tag/5.1.0)
+
+**New Feature:**
+
+- Make non-textual requests (#665)
+
+**Dependencies:**
+
+- [Security] Bump json5 from 1.0.1 to 1.0.2 (#656)
+
## [5.0.0](https://github.com/contactlab/appy/releases/tag/5.0.0)
**Breaking:**
diff --git a/docs/changelog.md b/docs/changelog.md
index 0036c3f2..de4f3e8d 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -5,6 +5,16 @@ nav_order: 2
# Changelog
+## [5.1.0](https://github.com/contactlab/appy/releases/tag/5.1.0)
+
+**New Feature:**
+
+- Make non-textual requests (#665)
+
+**Dependencies:**
+
+- [Security] Bump json5 from 1.0.1 to 1.0.2 (#656)
+
## [5.0.0](https://github.com/contactlab/appy/releases/tag/5.0.0)
**Breaking:**
diff --git a/docs/modules/combinators/abort.ts.md b/docs/modules/combinators/abort.ts.md
index 3a9390fd..b8e73259 100644
--- a/docs/modules/combinators/abort.ts.md
+++ b/docs/modules/combinators/abort.ts.md
@@ -77,7 +77,7 @@ users().then((result) => {
**Signature**
```ts
-export declare function withCancel(controller: AbortController): (req: Req) => Req
+export declare const withCancel: (controller: AbortController) => Combinator
```
Added in v3.1.0
@@ -105,7 +105,7 @@ users().then((result) => {
**Signature**
```ts
-export declare function withTimeout(millis: number): (req: Req) => Req
+export declare const withTimeout: (millis: number) => (req: Req) => Req
```
Added in v3.1.0
diff --git a/docs/modules/combinators/body.ts.md b/docs/modules/combinators/body.ts.md
index 0d23716c..260cb5a4 100644
--- a/docs/modules/combinators/body.ts.md
+++ b/docs/modules/combinators/body.ts.md
@@ -45,7 +45,7 @@ Sets the provided `body` (automatically converted to string when JSON) on `Req`
**Signature**
```ts
-export declare function withBody(body: unknown): (req: Req) => Req
+export declare const withBody: (body: unknown) => Combinator
```
Added in v3.0.0
diff --git a/docs/modules/combinators/decoder.ts.md b/docs/modules/combinators/decoder.ts.md
index 7736f5c4..803cf6e9 100644
--- a/docs/modules/combinators/decoder.ts.md
+++ b/docs/modules/combinators/decoder.ts.md
@@ -63,7 +63,7 @@ It automatically sets "JSON" request header's
**Signature**
```ts
-export declare function withDecoder(decoder: Decoder): (req: Req) => Req
+export declare const withDecoder: (decoder: Decoder) => (req: Req) => Req
```
Added in v3.0.0
@@ -77,7 +77,7 @@ Converts a `GenericDecoder` into a `Decoder`.
**Signature**
```ts
-export declare function toDecoder(dec: GenericDecoder, onLeft: (e: L) => Error): Decoder
+export declare const toDecoder: (dec: GenericDecoder, onLeft: (e: L) => Error) => Decoder
```
Added in v3.0.0
diff --git a/docs/modules/combinators/headers.ts.md b/docs/modules/combinators/headers.ts.md
index c471fec9..4423b744 100644
--- a/docs/modules/combinators/headers.ts.md
+++ b/docs/modules/combinators/headers.ts.md
@@ -42,7 +42,7 @@ Merges provided `Headers` with `Req` ones and returns the updated `Req`.
**Signature**
```ts
-export declare function withHeaders(headers: HeadersInit): (req: Req) => Req
+export declare const withHeaders: (headers: HeadersInit) => Combinator
```
Added in v3.0.0
diff --git a/docs/modules/combinators/method.ts.md b/docs/modules/combinators/method.ts.md
index 9eb012df..0e0ba2a5 100644
--- a/docs/modules/combinators/method.ts.md
+++ b/docs/modules/combinators/method.ts.md
@@ -42,7 +42,7 @@ Sets provided method on `Req` and returns the updated `Req`.
**Signature**
```ts
-export declare function withMethod(method: string): (req: Req) => Req
+export declare const withMethod: (method: string) => Combinator
```
Added in v4.0.0
diff --git a/docs/modules/combinators/url-params.ts.md b/docs/modules/combinators/url-params.ts.md
index 48bfaa94..5e5e34c3 100644
--- a/docs/modules/combinators/url-params.ts.md
+++ b/docs/modules/combinators/url-params.ts.md
@@ -56,7 +56,7 @@ Adds provided url search parameters (as `Record`) to `Req`'s inp
**Signature**
```ts
-export declare function withUrlParams(params: Params): (req: Req) => Req
+export declare const withUrlParams: (params: Params) => Combinator
```
Added in v3.0.0
diff --git a/docs/modules/request.ts.md b/docs/modules/request.ts.md
index 7a991ba7..0045f55f 100644
--- a/docs/modules/request.ts.md
+++ b/docs/modules/request.ts.md
@@ -23,6 +23,7 @@ Added in v4.0.0
- [toRequestError](#torequesterror)
- [toResponseError](#toresponseerror)
- [Request](#request)
+ - [Combinator (type alias)](#combinator-type-alias)
- [Req (interface)](#req-interface)
- [ReqInput (type alias)](#reqinput-type-alias)
- [RequestInfoInit (type alias)](#requestinfoinit-type-alias)
@@ -31,6 +32,7 @@ Added in v4.0.0
- [Resp (interface)](#resp-interface)
- [creators](#creators)
- [request](#request)
+ - [requestAs](#requestas)
---
@@ -87,7 +89,7 @@ Creates a `RequestError` object.
**Signature**
```ts
-export declare function toRequestError(error: Error, input: RequestInfoInit): RequestError
+export declare const toRequestError: (error: Error, input: RequestInfoInit) => RequestError
```
Added in v4.0.0
@@ -99,13 +101,25 @@ Creates a `ResponseError` object.
**Signature**
```ts
-export declare function toResponseError(error: Error, response: Response): ResponseError
+export declare const toResponseError: (error: Error, response: Response) => ResponseError
```
Added in v4.0.0
# Request
+## Combinator (type alias)
+
+A combinator is a function to transform/operate on a `Req`.
+
+**Signature**
+
+```ts
+export type Combinator = (req: Req) => Req
+```
+
+Added in v5.1.0
+
## Req (interface)
`Req` encodes a resource's request, or rather, an async operation that can fail or return a `Resp`.
@@ -151,7 +165,7 @@ Normalizes the input of a `Req` to a `RequestInfoInit` tuple even when only a si
**Signature**
```ts
-export declare function normalizeReqInput(input: ReqInput): RequestInfoInit
+export declare const normalizeReqInput: (input: ReqInput) => RequestInfoInit
```
Added in v4.0.0
@@ -185,13 +199,13 @@ Example:
```ts
import { request } from '@contactlab/appy'
-import { fold } from 'fp-ts/Either'
+import { match } from 'fp-ts/Either'
// Default method is GET like original `fetch()`
const users = request('https://reqres.in/api/users')
users().then(
- fold(
+ match(
(err) => console.error(err),
(data) => console.log(data)
)
@@ -205,3 +219,36 @@ export declare const request: Req
```
Added in v4.0.0
+
+## requestAs
+
+Return a `Req` which will be executed using `fetch()` under the hood.
+
+The `data` in the returned `Resp` object is of the type specified in the `type` parameter which is one of [supported `Request` methods](https://developer.mozilla.org/en-US/docs/Web/API/Response#instance_methods).
+
+Example:
+
+```ts
+import { requestAs } from '@contactlab/appy'
+import { match } from 'fp-ts/Either'
+
+// Default method is GET like original `fetch()`
+const users = requestAs('json')('https://reqres.in/api/users')
+
+users().then(
+ match(
+ (err) => console.error(err),
+ (data) => console.log(data)
+ )
+)
+```
+
+**Signature**
+
+```ts
+export declare const requestAs: (
+ type: K
+) => Req>
+```
+
+Added in v5.1.0
diff --git a/docs/modules/response.ts.md b/docs/modules/response.ts.md
index 720cc483..2299c789 100644
--- a/docs/modules/response.ts.md
+++ b/docs/modules/response.ts.md
@@ -30,7 +30,7 @@ Clones a `Response` object with the provided content as body.
**Signature**
```ts
-export declare function cloneResponse(from: Response, content: A): Response
+export declare const cloneResponse: (from: Response, content: A) => Response
```
Added in v4.0.1
diff --git a/package-lock.json b/package-lock.json
index 9ac55409..e05f4161 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@contactlab/appy",
- "version": "5.0.0",
+ "version": "5.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@contactlab/appy",
- "version": "5.0.0",
+ "version": "5.1.0",
"hasInstallScript": true,
"license": "Apache-2.0",
"devDependencies": {
diff --git a/package.json b/package.json
index b549ed48..7e60e60b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@contactlab/appy",
- "version": "5.0.0",
+ "version": "5.1.0",
"description": "A functional wrapper around Fetch API",
"main": "./index.js",
"module": "./_es6/index.js",
diff --git a/src/combinators/abort.ts b/src/combinators/abort.ts
index 53c6565d..d30e6490 100644
--- a/src/combinators/abort.ts
+++ b/src/combinators/abort.ts
@@ -33,7 +33,7 @@
import * as RTE from 'fp-ts/ReaderTaskEither';
import * as TU from 'fp-ts/Tuple';
import {pipe} from 'fp-ts/function';
-import {Req, normalizeReqInput} from '../request';
+import {type Req, type Combinator, normalizeReqInput} from '../request';
/**
* Sets `signal` on `Req` in order to make request cancellable through `AbortController`.
@@ -60,11 +60,8 @@ import {Req, normalizeReqInput} from '../request';
* @category combinators
* @since 3.1.0
*/
-export function withCancel(
- controller: AbortController
-): (req: Req) => Req {
- return setSignal(controller.signal);
-}
+export const withCancel = (controller: AbortController): Combinator =>
+ setSignal(controller.signal);
/**
* Aborts the request if it does not respond within provided milliseconds.
@@ -87,8 +84,9 @@ export function withCancel(
* @category combinators
* @since 3.1.0
*/
-export function withTimeout(millis: number): (req: Req) => Req {
- return req => {
+export const withTimeout =
+ (millis: number) =>
+ (req: Req): Req => {
const controller = new AbortController();
return pipe(
@@ -105,10 +103,9 @@ export function withTimeout(millis: number): (req: Req) => Req {
)
);
};
-}
-function setSignal(signal: AbortSignal): (req: Req) => Req {
- return RTE.local(input =>
+const setSignal = (signal: AbortSignal): Combinator =>
+ RTE.local(input =>
pipe(
normalizeReqInput(input),
// The "weird" merging is due to the mix of the contravariant nature of `Reader`
@@ -119,4 +116,3 @@ function setSignal(signal: AbortSignal): (req: Req) => Req {
TU.mapSnd(init => Object.assign({}, {signal}, init))
)
);
-}
diff --git a/src/combinators/body.ts b/src/combinators/body.ts
index de902ae5..749cb94f 100644
--- a/src/combinators/body.ts
+++ b/src/combinators/body.ts
@@ -25,11 +25,12 @@ import * as RTE from 'fp-ts/ReaderTaskEither';
import * as TU from 'fp-ts/Tuple';
import {pipe} from 'fp-ts/function';
import {
- Req,
- ReqInput,
+ type Req,
+ type ReqInput,
+ type Err,
+ type Combinator,
normalizeReqInput,
- toRequestError,
- Err
+ toRequestError
} from '../request';
/**
@@ -38,12 +39,14 @@ import {
* @category combinators
* @since 3.0.0
*/
-export function withBody(body: unknown): (req: Req) => Req {
- return req => pipe(toBodyInit(body), RTE.chain(setBody(req)));
-}
+export const withBody =
+ (body: unknown): Combinator =>
+ req =>
+ pipe(toBodyInit(body), RTE.chain(setBody(req)));
-function setBody(req: Req): (body: BodyInit) => Req {
- return body =>
+const setBody =
+ (req: Req) =>
+ (body: BodyInit): Req =>
pipe(
req,
RTE.local(input =>
@@ -58,12 +61,11 @@ function setBody(req: Req): (body: BodyInit) => Req {
)
)
);
-}
-function toBodyInit(
+const toBodyInit = (
body: unknown
-): RTE.ReaderTaskEither {
- return pipe(
+): RTE.ReaderTaskEither =>
+ pipe(
RTE.ask(),
RTE.chain(input =>
RTE.fromEither(
@@ -74,12 +76,11 @@ function toBodyInit(
)
)
);
-}
-function toStringWhenJSON(body: unknown): E.Either {
+const toStringWhenJSON = (body: unknown): E.Either => {
if (Object.getPrototypeOf(body).constructor.name !== 'Object') {
return E.right(body as BodyInit); // type assertion mandatory...
}
return pipe(stringify(body), E.mapLeft(E.toError));
-}
+};
diff --git a/src/combinators/decoder.ts b/src/combinators/decoder.ts
index afeefe7c..d26de3e7 100644
--- a/src/combinators/decoder.ts
+++ b/src/combinators/decoder.ts
@@ -9,7 +9,7 @@ import {parse} from 'fp-ts/Json';
import {ReaderEither, mapLeft} from 'fp-ts/ReaderEither';
import * as RTE from 'fp-ts/ReaderTaskEither';
import {pipe} from 'fp-ts/function';
-import {Err, Req, Resp, toResponseError} from '../request';
+import {type Err, type Req, type Resp, toResponseError} from '../request';
import {cloneResponse} from '../response';
import {withHeaders} from './headers';
@@ -39,10 +39,9 @@ export interface Decoder extends GenericDecoder {}
* @category combinators
* @since 3.0.0
*/
-export function withDecoder(
- decoder: Decoder
-): (req: Req) => Req {
- return req =>
+export const withDecoder =
+ (decoder: Decoder) =>
+ (req: Req): Req =>
pipe(
req,
withHeaders({Accept: 'application/json'}),
@@ -60,7 +59,6 @@ export function withDecoder(
)
)
);
-}
/**
* Converts a `GenericDecoder` into a `Decoder`.
@@ -68,14 +66,12 @@ export function withDecoder(
* @category helpers
* @since 3.0.0
*/
-export function toDecoder(
+export const toDecoder = (
dec: GenericDecoder,
onLeft: (e: L) => Error
-): Decoder {
- return pipe(dec, mapLeft(onLeft));
-}
+): Decoder => pipe(dec, mapLeft(onLeft));
-function parseResponse({data}: Resp): E.Either {
+const parseResponse = ({data}: Resp): E.Either => {
if (typeof data === 'object') {
return E.right(data);
}
@@ -84,4 +80,4 @@ function parseResponse({data}: Resp): E.Either {
const prepared = asString.length === 0 ? '{}' : asString;
return pipe(parse(prepared), E.mapLeft(E.toError));
-}
+};
diff --git a/src/combinators/headers.ts b/src/combinators/headers.ts
index a31c90be..3b82fee3 100644
--- a/src/combinators/headers.ts
+++ b/src/combinators/headers.ts
@@ -21,7 +21,7 @@ import {getMonoid} from 'fp-ts/Record';
import {last} from 'fp-ts/Semigroup';
import * as TU from 'fp-ts/Tuple';
import {pipe} from 'fp-ts/function';
-import {Req, normalizeReqInput} from '../request';
+import {type Combinator, normalizeReqInput} from '../request';
type Hs = Record;
@@ -33,16 +33,15 @@ const RML = getMonoid(last());
* @category combinators
* @since 3.0.0
*/
-export function withHeaders(headers: HeadersInit): (req: Req) => Req {
- return RTE.local(input =>
+export const withHeaders = (headers: HeadersInit): Combinator =>
+ RTE.local(input =>
pipe(
normalizeReqInput(input),
TU.mapSnd(init => merge(init, headers))
)
);
-}
-function merge(init: RequestInit, h: HeadersInit): RequestInit {
+const merge = (init: RequestInit, h: HeadersInit): RequestInit => {
// The "weird" `concat` is due to the mix of the contravariant nature of `Reader`
// and the function composition at the base of "combinators".
// Because combinators are applied from right to left, the merging has to be "reversed".
@@ -52,13 +51,12 @@ function merge(init: RequestInit, h: HeadersInit): RequestInit {
typeof init.headers === 'undefined' ? toRecord(h) : concat(h, init.headers);
return {...init, headers};
-}
+};
-function concat(a: HeadersInit, b: HeadersInit): Hs {
- return RML.concat(toRecord(a), toRecord(b));
-}
+const concat = (a: HeadersInit, b: HeadersInit): Hs =>
+ RML.concat(toRecord(a), toRecord(b));
-function toRecord(h: HeadersInit): Hs {
+const toRecord = (h: HeadersInit): Hs => {
if (Array.isArray(h)) {
return h.reduce((acc, [k, v]) => ({...acc, [k]: v}), {});
}
@@ -72,4 +70,4 @@ function toRecord(h: HeadersInit): Hs {
}
return h;
-}
+};
diff --git a/src/combinators/method.ts b/src/combinators/method.ts
index 2a0358ae..d5dfd9a0 100644
--- a/src/combinators/method.ts
+++ b/src/combinators/method.ts
@@ -19,7 +19,7 @@
import * as RTE from 'fp-ts/ReaderTaskEither';
import * as TU from 'fp-ts/Tuple';
import {pipe} from 'fp-ts/function';
-import {Req, normalizeReqInput} from '../request';
+import {type Combinator, normalizeReqInput} from '../request';
/**
* Sets provided method on `Req` and returns the updated `Req`.
@@ -27,8 +27,8 @@ import {Req, normalizeReqInput} from '../request';
* @category combinators
* @since 4.0.0
*/
-export function withMethod(method: string): (req: Req) => Req {
- return RTE.local(input =>
+export const withMethod = (method: string): Combinator =>
+ RTE.local(input =>
pipe(
normalizeReqInput(input),
// The "weird" MERGING is due to the mix of the contravariant nature of `Reader`
@@ -39,4 +39,3 @@ export function withMethod(method: string): (req: Req) => Req {
TU.mapSnd(init => ({method, ...init}))
)
);
-}
diff --git a/src/combinators/url-params.ts b/src/combinators/url-params.ts
index 35280a48..33a3616f 100644
--- a/src/combinators/url-params.ts
+++ b/src/combinators/url-params.ts
@@ -18,12 +18,13 @@
import * as E from 'fp-ts/Either';
import * as RTE from 'fp-ts/ReaderTaskEither';
-import {flow, pipe} from 'fp-ts/function';
+import {pipe} from 'fp-ts/function';
import {
- Err,
- Req,
- ReqInput,
- RequestInfoInit,
+ type Err,
+ type Req,
+ type ReqInput,
+ type RequestInfoInit,
+ type Combinator,
normalizeReqInput,
toRequestError
} from '../request';
@@ -40,68 +41,67 @@ export type Params = Record;
* @category combinators
* @since 3.0.0
*/
-export function withUrlParams(params: Params): (req: Req) => Req {
- return req => pipe(prepare(params), RTE.chain(setURLParams(req)));
-}
+export const withUrlParams =
+ (params: Params): Combinator =>
+ req =>
+ pipe(prepare(params), RTE.chain(setURLParams(req)));
-function setURLParams(req: Req): (input: RequestInfoInit) => Req {
- return input =>
+const setURLParams =
+ (req: Req) =>
+ (input: RequestInfoInit): Req =>
pipe(
req,
RTE.local(() => input)
);
-}
interface RTEInfoInit
extends RTE.ReaderTaskEither {}
-function prepare(params: Params): RTEInfoInit {
- return pipe(RTE.ask(), RTE.chain(applyParams(params)));
-}
+const prepare = (params: Params): RTEInfoInit =>
+ pipe(RTE.ask(), RTE.chain(applyParams(params)));
-function applyParams(params: Params): (input: ReqInput) => RTEInfoInit {
- return flow(
- normalizeReqInput,
- ([info, init]) =>
- pipe(
- getURL(info),
- E.chain(url =>
- // The "weird" merging is due to the mix of the contravariant nature of `Reader`
- // and the function composition at the base of "combinators".
- // Because combinators are applied from right to left, the merging has to be "reversed".
- // This leads to another "weird" behavior for which the url's params provided when `Req` is run
- // win over the ones set with the combinator.
- setParams(merge(new URLSearchParams(params), url.searchParams), url)
+const applyParams =
+ (params: Params) =>
+ (input: ReqInput): RTEInfoInit =>
+ pipe(
+ normalizeReqInput(input),
+ ([info, init]) =>
+ pipe(
+ getURL(info),
+ E.chain(url =>
+ // The "weird" merging is due to the mix of the contravariant nature of `Reader`
+ // and the function composition at the base of "combinators".
+ // Because combinators are applied from right to left, the merging has to be "reversed".
+ // This leads to another "weird" behavior for which the url's params provided when `Req` is run
+ // win over the ones set with the combinator.
+ setParams(merge(new URLSearchParams(params), url.searchParams), url)
+ ),
+ E.bimap(
+ err => toRequestError(err, [info, init]),
+ (url): RequestInfoInit => [url.toString(), init]
+ )
),
- E.bimap(
- err => toRequestError(err, [info, init]),
- (url): RequestInfoInit => [url.toString(), init]
- )
- ),
- RTE.fromEither
- );
-}
+ RTE.fromEither
+ );
type MaybeURL = E.Either;
-function getURL(info: RequestInfo): MaybeURL {
- return E.tryCatch(() => new URL(new Request(info).url), E.toError);
-}
+const getURL = (info: RequestInfo): MaybeURL =>
+ E.tryCatch(() => new URL(new Request(info).url), E.toError);
-function setParams(params: URLSearchParams, url: URL): MaybeURL {
- return E.tryCatch(() => {
+const setParams = (params: URLSearchParams, url: URL): MaybeURL =>
+ E.tryCatch(() => {
const u = new URL(url.toString());
u.search = params.toString();
return u;
}, E.toError);
-}
-function merge(
+const merge = (
source: URLSearchParams,
target: URLSearchParams
-): URLSearchParams {
+): URLSearchParams => {
const result = new URLSearchParams(source.toString());
target.forEach((v, k) => {
@@ -109,4 +109,4 @@ function merge(
});
return result;
-}
+};
diff --git a/src/request.ts b/src/request.ts
index ec1a956d..ff018fac 100644
--- a/src/request.ts
+++ b/src/request.ts
@@ -40,6 +40,14 @@ export type ReqInput = RequestInfo | RequestInfoInit;
*/
export type RequestInfoInit = [RequestInfo, RequestInit];
+/**
+ * A combinator is a function to transform/operate on a `Req`.
+ *
+ * @category Request
+ * @since 5.1.0
+ */
+export type Combinator = (req: Req) => Req;
+
/**
* `Resp` is an object that carries the original `Response` from a `fetch()` call and the actual retrieved `data` (of type `A`).
*
@@ -83,6 +91,69 @@ export interface ResponseError {
response: Response;
}
+type BodyTypeKey = {
+ [K in keyof Response]-?: Response[K] extends () => Promise
+ ? K
+ : never;
+}[keyof Response] &
+ string;
+
+type BodyTypeData = ReturnType<
+ Response[K]
+> extends Promise
+ ? _A
+ : never;
+
+/**
+ * Return a `Req` which will be executed using `fetch()` under the hood.
+ *
+ * The `data` in the returned `Resp` object is of the type specified in the `type` parameter which is one of [supported `Request` methods](https://developer.mozilla.org/en-US/docs/Web/API/Response#instance_methods).
+ *
+ * Example:
+ * ```ts
+ * import {requestAs} from '@contactlab/appy';
+ * import {match} from 'fp-ts/Either';
+ *
+ * // Default method is GET like original `fetch()`
+ * const users = requestAs('json')('https://reqres.in/api/users');
+ *
+ * users().then(
+ * match(
+ * err => console.error(err),
+ * data => console.log(data)
+ * )
+ * );
+ * ```
+ *
+ * @category creators
+ * @since 5.1.0
+ */
+export const requestAs =
+ (type: K): Req> =>
+ input =>
+ () => {
+ const reqInput = normalizeReqInput(input);
+
+ return fetch(...reqInput)
+ .then(async response => {
+ if (!response.ok) {
+ return E.left(
+ toResponseError(
+ new Error(
+ `Request responded with status code ${response.status}`
+ ),
+ response
+ )
+ );
+ }
+
+ const data = await response[type]();
+
+ return E.right({response, data});
+ })
+ .catch(e => E.left(toRequestError(e, reqInput)));
+ };
+
/**
* Makes a request using `fetch()` under the hood.
*
@@ -91,13 +162,13 @@ export interface ResponseError {
* Example:
* ```ts
* import {request} from '@contactlab/appy';
- * import {fold} from 'fp-ts/Either';
+ * import {match} from 'fp-ts/Either';
*
* // Default method is GET like original `fetch()`
* const users = request('https://reqres.in/api/users');
*
* users().then(
- * fold(
+ * match(
* err => console.error(err),
* data => console.log(data)
* )
@@ -107,26 +178,7 @@ export interface ResponseError {
* @category creators
* @since 4.0.0
*/
-export const request: Req = input => () => {
- const reqInput = normalizeReqInput(input);
-
- return fetch(...reqInput)
- .then(async response => {
- if (!response.ok) {
- return E.left(
- toResponseError(
- new Error(`Request responded with status code ${response.status}`),
- response
- )
- );
- }
-
- const data = await response.text();
-
- return E.right({response, data});
- })
- .catch(e => E.left(toRequestError(e, reqInput)));
-};
+export const request: Req = requestAs('text');
/**
* Creates a `RequestError` object.
@@ -134,12 +186,10 @@ export const request: Req = input => () => {
* @category Error
* @since 4.0.0
*/
-export function toRequestError(
+export const toRequestError = (
error: Error,
input: RequestInfoInit
-): RequestError {
- return {type: 'RequestError', error, input};
-}
+): RequestError => ({type: 'RequestError', error, input});
/**
* Creates a `ResponseError` object.
@@ -147,12 +197,10 @@ export function toRequestError(
* @category Error
* @since 4.0.0
*/
-export function toResponseError(
+export const toResponseError = (
error: Error,
response: Response
-): ResponseError {
- return {type: 'ResponseError', response, error};
-}
+): ResponseError => ({type: 'ResponseError', response, error});
/**
* Normalizes the input of a `Req` to a `RequestInfoInit` tuple even when only a single `RequestInfo` is provided.
@@ -160,6 +208,5 @@ export function toResponseError(
* @category Request
* @since 4.0.0
*/
-export function normalizeReqInput(input: ReqInput): RequestInfoInit {
- return Array.isArray(input) ? input : [input, {}];
-}
+export const normalizeReqInput = (input: ReqInput): RequestInfoInit =>
+ Array.isArray(input) ? input : [input, {}];
diff --git a/src/response.ts b/src/response.ts
index 5e5bc741..ff8bca80 100644
--- a/src/response.ts
+++ b/src/response.ts
@@ -13,7 +13,7 @@
* @category helpers
* @since 4.0.1
*/
-export function cloneResponse(from: Response, content: A): Response {
+export const cloneResponse = (from: Response, content: A): Response => {
let body: BodyInit | null;
try {
@@ -28,4 +28,4 @@ export function cloneResponse(from: Response, content: A): Response {
status: from.status,
statusText: from.statusText
});
-}
+};
diff --git a/test/decoder.spec.ts b/test/decoder.spec.ts
index 006f229b..f2b9bb23 100644
--- a/test/decoder.spec.ts
+++ b/test/decoder.spec.ts
@@ -3,7 +3,7 @@ import {right, left} from 'fp-ts/Either';
import * as RTE from 'fp-ts/ReaderTaskEither';
import {pipe} from 'fp-ts/function';
import * as D from 'io-ts/Decoder';
-import {Decoder, toDecoder, withDecoder} from '../src/combinators/decoder';
+import {type Decoder, toDecoder, withDecoder} from '../src/combinators/decoder';
import {withHeaders} from '../src/combinators/headers';
import * as appy from '../src/index';
diff --git a/test/request.spec.ts b/test/request.spec.ts
index 46ed3d99..cbd0bb57 100644
--- a/test/request.spec.ts
+++ b/test/request.spec.ts
@@ -1,11 +1,90 @@
import fetchMock from 'fetch-mock';
-import {right, left} from 'fp-ts/Either';
-import * as AR from '../src/request';
+import {right, left, isLeft} from 'fp-ts/Either';
+import {
+ type RequestInfoInit,
+ request,
+ requestAs,
+ toRequestError,
+ toResponseError
+} from '../src/request';
afterEach(() => {
fetchMock.reset();
});
+const requestAsBlob = requestAs('blob');
+
+test('requestAs() should return a right `Resp` of provided type', async () => {
+ const response = new Response('a list of resources', {
+ status: 200,
+ headers: {}
+ });
+
+ fetchMock.mock('http://localhost/api/resources', response);
+
+ const r = await requestAsBlob('http://localhost/api/resources')();
+
+ if (isLeft(r)) {
+ throw new Error();
+ }
+
+ expect(r.right.response).toEqual(response);
+
+ const data = await r.right.data.text();
+
+ expect(data).toBe('a list of resources');
+});
+
+test('requestAs() should return a left `RequestError` when request fails', async () => {
+ const error = new TypeError('Network error');
+
+ fetchMock.mock('http://localhost/api/resources', {throws: error});
+
+ const r1 = await requestAsBlob('http://localhost/api/resources')();
+
+ expect(r1).toEqual(
+ left({
+ type: 'RequestError',
+ error,
+ input: ['http://localhost/api/resources', {}]
+ })
+ );
+
+ const r2 = await requestAsBlob([
+ 'http://localhost/api/resources',
+ {method: 'GET', headers: {'X-Some-Header': 'some value'}}
+ ])();
+
+ expect(r2).toEqual(
+ left({
+ type: 'RequestError',
+ error,
+ input: [
+ 'http://localhost/api/resources',
+ {method: 'GET', headers: {'X-Some-Header': 'some value'}}
+ ]
+ })
+ );
+});
+
+test('requestAs() should return a left `ResponseError` when response status is not ok', async () => {
+ const response = new Response('a list of resources', {
+ status: 503
+ });
+
+ fetchMock.mock('http://localhost/api/resources', response);
+
+ const r = await requestAsBlob('http://localhost/api/resources')();
+
+ expect(r).toEqual(
+ left({
+ type: 'ResponseError',
+ response,
+ error: new Error(`Request responded with status code 503`)
+ })
+ );
+});
+
test('request() should return a right `Resp` - default GET', async () => {
const response = new Response('a list of resources', {
status: 200,
@@ -14,7 +93,7 @@ test('request() should return a right `Resp` - default GET', async () =>
fetchMock.mock('http://localhost/api/resources', response);
- const r = await AR.request('http://localhost/api/resources')();
+ const r = await request('http://localhost/api/resources')();
expect(r).toEqual(right({response, data: 'a list of resources'}));
});
@@ -24,7 +103,7 @@ test('request() should return a right `Resp` - with POST', async () => {
fetchMock.post('http://localhost/api/post-resources', response);
- const r = await AR.request([
+ const r = await request([
'http://localhost/api/post-resources',
{method: 'POST', body: ''}
])();
@@ -37,7 +116,7 @@ test('request() should return a left `RequestError` when request fails', async (
fetchMock.mock('http://localhost/api/resources', {throws: error});
- const r1 = await AR.request('http://localhost/api/resources')();
+ const r1 = await request('http://localhost/api/resources')();
expect(r1).toEqual(
left({
@@ -47,7 +126,7 @@ test('request() should return a left `RequestError` when request fails', async (
})
);
- const r2 = await AR.request([
+ const r2 = await request([
'http://localhost/api/resources',
{method: 'GET', headers: {'X-Some-Header': 'some value'}}
])();
@@ -71,7 +150,7 @@ test('request() should return a left `ResponseError` when response status is not
fetchMock.mock('http://localhost/api/resources', response);
- const r = await AR.request('http://localhost/api/resources')();
+ const r = await request('http://localhost/api/resources')();
expect(r).toEqual(
left({
@@ -84,12 +163,9 @@ test('request() should return a left `ResponseError` when response status is not
test('toRequestError() should return a RequestError', () => {
const error = new TypeError('something bad happened');
- const input: AR.RequestInfoInit = [
- 'http://localhost/my/api',
- {method: 'GET'}
- ];
+ const input: RequestInfoInit = ['http://localhost/my/api', {method: 'GET'}];
- expect(AR.toRequestError(error, input)).toEqual({
+ expect(toRequestError(error, input)).toEqual({
type: 'RequestError',
error,
input
@@ -100,7 +176,7 @@ test('toResponseError() should return a ResponseError', () => {
const response = new Response('bad', {status: 500});
const badStatus = new Error('Request responded with status 500');
- expect(AR.toResponseError(badStatus, response)).toEqual({
+ expect(toResponseError(badStatus, response)).toEqual({
type: 'ResponseError',
error: badStatus,
response