/* 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"
}
}