/* eslint-disable @typescript-eslint/no-explicit-any */ import type { Predicate } from "@effect-ts/core/Function" import type { ParsedQuery } from "query-string" /* tested in the implementation packages */ /* istanbul ignore file */ export const Method = { GET: null, POST: null, PUT: null, DELETE: null, PATCH: null } export type Method = keyof typeof Method type Indexed = { [a in A]: { [b in B]: any } } type MakeIndexed< A extends string, B extends string, T extends Indexed > = T export type RequestType = "JSON" | "DATA" | "FORM" | "BINARY" export type RequestBodyTypes = MakeIndexed< RequestType, Method, { JSON: { GET: unknown POST: unknown PUT: unknown DELETE: unknown PATCH: unknown } DATA: { GET: ParsedQuery POST: ParsedQuery PUT: ParsedQuery DELETE: ParsedQuery PATCH: ParsedQuery } FORM: { GET: FormData POST: FormData PUT: FormData DELETE: FormData PATCH: FormData } BINARY: { GET: Buffer POST: Buffer PUT: Buffer DELETE: Buffer PATCH: Buffer } } > export type ResponseType = "JSON" | "TEXT" | "BINARY" export type ResponseTypes = MakeIndexed< ResponseType, Method, { JSON: { GET: unknown POST: unknown PUT: unknown DELETE: unknown PATCH: unknown } TEXT: { GET: string POST: string PUT: string DELETE: string PATCH: string } BINARY: { GET: Buffer POST: Buffer PUT: Buffer DELETE: Buffer PATCH: Buffer } } > export interface DataInput { [k: string]: unknown } export type Headers = Record export interface Response { body: Opt headers: Headers status: number } export const HttpErrorReason = { Request: "HttpErrorRequest", Response: "HttpErrorResponse" } as const export type HttpErrorReason = typeof HttpErrorReason export interface HttpResponseError { _tag: HttpErrorReason["Response"] response: Response } export function isHttpResponseError( u: unknown ): u is HttpResponseError { return ( typeof u === "object" && u !== null && (u as any)["_tag"] === HttpErrorReason.Response ) } export interface HttpRequestError { _tag: HttpErrorReason["Request"] error: Error } export function isHttpRequestError(u: unknown): u is HttpRequestError { return ( typeof u === "object" && u !== null && (u as any)["_tag"] === HttpErrorReason.Request ) } export function isHttpError(u: unknown): u is HttpError { return isHttpRequestError(u) || isHttpResponseError(u) } export type HttpError = | HttpRequestError | HttpResponseError export function foldHttpError( onError: (e: Error) => A, onResponseError: (e: Response) => B ): (err: HttpError) => A | B { return err => { switch (err._tag) { case "HttpErrorRequest": return onError(err.error) case "HttpErrorResponse": return onResponseError(err.response) } } } export interface HttpHeaders extends Record {} export const HttpHeaders = Tag() const accessHttpHeaders_ = Effect.environmentWith((env: Context) => env.getOption(HttpHeaders)) export function accessHttpHeadersM( eff: (h: Opt) => Effect ) { return accessHttpHeaders_.flatMap(eff) } export function accessHttpHeaders(eff: (h: Opt) => A) { return accessHttpHeaders_.map(eff) } export interface HttpOps { request( method: M, url: string, requestType: Req, responseType: Resp, headers: Record, body: RequestBodyTypes[Req][M] ): Effect, Response> } export interface Http extends HttpOps {} export const Http = Tag() // const accessHttp = Effect.accessService(Http) export type RequestF = < R, M extends Method, Req extends RequestType, Resp extends ResponseType >( method: M, url: string, requestType: Req, responseType: Resp, body?: RequestBodyTypes[Req][M] ) => Effect< RequestEnv | R, HttpError, Response > export type RequestMiddleware = (request: RequestF) => RequestF export interface MiddlewareStack { stack: RequestMiddleware[] // todo; is optional. } export const MiddlewareStack = Tag() const accessMiddlewareStack_ = Effect.environmentWith((env: Context) => env.getOption(MiddlewareStack)) export function accessMiddlewareStackM( eff: (h: Opt) => Effect ) { return accessMiddlewareStack_.flatMap(eff) } export function accessMiddlewareStack( eff: (h: Opt) => A ) { return accessMiddlewareStack_.map(eff) } export const LiveMiddlewareStack = (stack: RequestMiddleware[] = []) => MiddlewareStack.makeLayer({ stack }) export type RequestEnv = Http function foldMiddlewareStack( env: MiddlewareStack | null, request: RequestF ): RequestF { if (env && env.stack.length > 0) { let r = request for (const middleware of env.stack) { r = middleware(r) } return r } return request } export function requestInner< R, M extends Method, Req extends RequestType, Resp extends ResponseType >( method: M, url: string, requestType: Req, responseType: Resp, body: RequestBodyTypes[Req][M] ): Effect, Response> { return accessHttpHeadersM(headers => Http.accessWithEffect(h => h.request( method, url, requestType, responseType, headers.getOrElse(() => ({})), body ) ) ) } export function request( method: "GET", requestType: Req, responseType: Resp ): ( url: string, body?: RequestBodyTypes[Req]["GET"] ) => Effect< RequestEnv | R, HttpError, Response > export function request( method: "DELETE", requestType: Req, responseType: Resp ): ( url: string, body?: RequestBodyTypes[Req]["DELETE"] ) => Effect< RequestEnv | R, HttpError, Response > export function request< R, M extends Method, Req extends RequestType, Resp extends ResponseType >( method: M, requestType: Req, responseType: Resp ): ( url: string, body: RequestBodyTypes[Req][M] ) => Effect< RequestEnv | R, HttpError, Response > export function request< R, M extends Method, Req extends RequestType, Resp extends ResponseType >( method: M, requestType: Req, responseType: Resp ): ( url: string, body: RequestBodyTypes[Req][M] ) => Effect< RequestEnv | R, HttpError, Response > { return (url, body) => accessMiddlewareStackM(s => foldMiddlewareStack(s.getOrNull, requestInner)( method, url, requestType, responseType, body ) ) } export const get = /*#__PURE__*/ (() => request("GET", "JSON", "JSON"))() export const post = /*#__PURE__*/ (() => request("POST", "JSON", "JSON"))() export const postReturnText = /*#__PURE__*/ (() => request("POST", "JSON", "TEXT"))() export const postData = /*#__PURE__*/ (() => request("POST", "DATA", "JSON"))() export const postBinaryGetBinary = /*#__PURE__*/ (() => request("POST", "BINARY", "BINARY"))() export const patch = /*#__PURE__*/ (() => request("PATCH", "JSON", "JSON"))() export const patchData = /*#__PURE__*/ (() => request("PATCH", "DATA", "JSON"))() export const patchBinaryGetBinary = /*#__PURE__*/ (() => request("PATCH", "BINARY", "BINARY"))() export const put = /*#__PURE__*/ (() => request("PUT", "JSON", "JSON"))() export const putData = /*#__PURE__*/ (() => request("PUT", "DATA", "JSON"))() export const postForm = /*#__PURE__*/ (() => request("POST", "FORM", "JSON"))() export const putForm = /*#__PURE__*/ (() => request("PUT", "FORM", "JSON"))() export const patchForm = /*#__PURE__*/ (() => request("PATCH", "FORM", "JSON"))() export const putBinaryGetBinary = /*#__PURE__*/ (() => request("PUT", "BINARY", "BINARY"))() export const del = /*#__PURE__*/ (() => request("DELETE", "JSON", "JSON"))() export const delForm = /*#__PURE__*/ (() => request("DELETE", "FORM", "JSON"))() export const delData = /*#__PURE__*/ (() => request("DELETE", "DATA", "JSON"))() export const delBinaryGetBinary = /*#__PURE__*/ (() => request("DELETE", "BINARY", "BINARY"))() // TODO !!! export function withHeaders( headers: Record, replace = false ): (eff: Effect) => Effect { return (eff: Effect) => replace ? Effect.environmentWithEffect((r: Context) => eff.provideEnvironment(Context.add(HttpHeaders)(headers)(r))) : Effect.environmentWithEffect((r: Context) => eff.provideEnvironment( Context.add(HttpHeaders)({ ...(r.getOption(HttpHeaders).value ?? {}), ...headers })(r) ) ) } export function withPathHeaders( headers: Record, path: Predicate, replace = false ): RequestMiddleware { return req => (m, u, reqT, respT, b) => path(u) ? withHeaders(headers, replace)(req(m, u, reqT, respT, b)) : req(m, u, reqT, respT, b) } export function foldRequestType( requestType: RequestType, onJson: () => A, onData: () => B, onForm: () => C, onBinary: () => D ): A | B | C | D { switch (requestType) { case "JSON": return onJson() case "DATA": return onData() case "FORM": return onForm() case "BINARY": return onBinary() } } export function foldResponseType( responseType: ResponseType, onJson: () => A, onText: () => B, onBinary: () => C ): A | B | C { switch (responseType) { case "JSON": return onJson() case "TEXT": return onText() case "BINARY": return onBinary() } } export function getMethodAsString(method: Method) { switch (method) { case "GET": return "GET" case "POST": return "POST" case "PUT": return "PUT" case "PATCH": return "PATCH" case "DELETE": return "DELETE" } }