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