fix: initial commit

This commit is contained in:
orion 2023-11-18 16:25:25 -06:00
parent cbcc301336
commit 4ac5d9573c
Signed by: orion
GPG Key ID: 6D4165AE4C928719
12 changed files with 778 additions and 4 deletions

View File

@ -1,19 +1,24 @@
package: package:
dependencies: dependencies:
- prelude
- aff - aff
- aff-promise
- arraybuffer
- console
- effect - effect
- either - either
- maybe
- foldable-traversable - foldable-traversable
- console - maybe
- newtype - newtype
- node-buffer
- prelude
- simple-json
- strings - strings
- stringutils - stringutils
- transformers - transformers
- tuples - tuples
- typelevel-prelude - typelevel-prelude
name: project - web-file
name: fetch
workspace: workspace:
extra_packages: {} extra_packages: {}
package_set: package_set:

3
src/HTTP.js Normal file
View File

@ -0,0 +1,3 @@
/** @type {(_: string) => (_: string) => (_: Record<string, string>) => (_: null | string | FormData) => () => Promise<Response>} */
export const fetchImpl = url => method => headers => body => () =>
fetch(url, { redirect: 'manual', body, method, headers })

37
src/HTTP.purs Normal file
View File

@ -0,0 +1,37 @@
module HTTP (fetch, module X) where
import Prelude
import Control.Promise (Promise)
import Control.Promise as Promise
import Data.Newtype (unwrap)
import Data.Nullable (Nullable)
import Data.Nullable as Nullable
import Effect (Effect)
import Effect.Aff.Class (class MonadAff, liftAff)
import Foreign.Object (Object)
import Foreign.Object as Object
import HTTP.Header (headers) as X
import HTTP.Request (class Request, Method(..)) as X
import HTTP.Request as Req
import HTTP.Response (Response)
foreign import fetchImpl :: String -> String -> Object String -> Nullable Req.RawRequestBody -> Effect (Promise Response)
fetch :: forall m a. MonadAff m => Req.Request a => a -> m Response
fetch req = do
url <- Req.requestUrl req
method <- Req.requestMethod req
body <- Req.requestBody req
headers <- Req.requestHeaders req
let
methodStr = case method of
Req.GET -> "GET"
Req.PUT -> "PUT"
Req.POST -> "POST"
Req.PATCH -> "PATCH"
Req.DELETE -> "DELETE"
headers' = Object.fromFoldableWithIndex headers
liftAff $ Promise.toAffE $ fetchImpl (unwrap url) methodStr headers' $ Nullable.toNullable body

56
src/HTTP/Form.js Normal file
View File

@ -0,0 +1,56 @@
/** @type {(_: Record<string, Array<string | Blob>>) => () => FormData} */
export const unsafeMakeFormData = o => () => {
const form = new FormData()
Object.entries(o).forEach(([k, vs]) => {
vs.forEach(v => {
form.append(k, v)
})
})
return form
}
/** @typedef {{filename: string | null, mime: string, buf: ArrayBuffer}} FileRecord */
/** @type {(_: FileRecord) => () => Blob} */
export const unsafeMakeBlob =
({ mime, buf }) =>
() =>
new Blob([buf], { type: mime })
/** @type {(_: FormData) => () => Promise<Record<string, Array<string | FileRecord>>>} */
export const unsafeUnmakeFormData = fd => async () => {
/** @type {Record<string, Array<string | FileRecord>>} */
const rec = {}
for (const [k, ent_] of fd.entries()) {
/** @type {File | Blob | string} */
const ent = ent_
/** @type {string | FileRecord} */
let append = ''
if (ent instanceof File) {
append = {
filename: ent.name,
buf: await ent.arrayBuffer(),
mime: ent.type,
}
} else if (ent instanceof Blob) {
append = { filename: null, buf: await ent.arrayBuffer(), mime: ent.type }
} else {
append = ent
}
if (!append) {
continue
}
if (rec[k]) {
rec[k].push(append)
} else {
rec[k] = [append]
}
}
return rec
}

109
src/HTTP/Form.purs Normal file
View File

@ -0,0 +1,109 @@
module HTTP.Form where
import Prelude
import Control.Monad.Error.Class (liftMaybe, try)
import Control.Monad.Except (runExcept)
import Control.Promise (Promise)
import Control.Promise as Promise
import Data.ArrayBuffer.ArrayBuffer as ArrayBuffer
import Data.ArrayBuffer.Types (ArrayBuffer)
import Data.Either (hush)
import Data.Foldable (foldl)
import Data.FoldableWithIndex (foldlWithIndex)
import Data.Generic.Rep (class Generic)
import Data.Map (Map)
import Data.Map as Map
import Data.Maybe (Maybe(..))
import Data.Newtype (class Newtype, unwrap, wrap)
import Data.Nullable (Nullable)
import Data.Nullable as Nullable
import Data.Show.Generic (genericShow)
import Data.Traversable (for)
import Data.Tuple (Tuple(..))
import Effect (Effect)
import Effect.Aff (Aff)
import Effect.Aff.Class (class MonadAff, liftAff)
import Effect.Class (class MonadEffect, liftEffect)
import Effect.Exception (error)
import Foreign (Foreign, unsafeReadTagged, unsafeToForeign)
import Foreign.Object (Object)
import Foreign.Object as Object
import HTTP.Header as Header
import HTTP.MIME (MIME)
import HTTP.MIME as MIME
import Node.Buffer.Immutable (ImmutableBuffer)
import Simple.JSON (readImpl, unsafeStringify)
import Unsafe.Coerce (unsafeCoerce)
import Web.File.Blob (Blob)
type FileRecord = { filename :: Nullable String, mime :: String, buf :: ArrayBuffer }
foreign import data RawFormData :: Type
foreign import unsafeMakeBlob :: FileRecord -> Effect Blob
foreign import unsafeMakeFormData :: Object (Array Foreign) -> Effect RawFormData
foreign import unsafeUnmakeFormData :: RawFormData -> Effect (Promise (Object (Array Foreign)))
newtype Filename = Filename String
derive instance Generic Filename _
derive instance Newtype Filename _
instance Show Filename where
show = genericShow
derive newtype instance Eq Filename
derive newtype instance Ord Filename
data Value
= ValueString String
| ValueFile (Maybe Filename) ArrayBuffer MIME
valueForeign :: Value -> Effect Foreign
valueForeign (ValueString s) = pure $ unsafeToForeign s
valueForeign (ValueFile filename buf mime) = unsafeToForeign <$> unsafeMakeBlob { filename: Nullable.toNullable $ unwrap <$> filename, buf, mime: MIME.toString mime }
valueFromForeign :: Foreign -> Effect Value
valueFromForeign f = do
let
file :: Maybe { filename :: Nullable String, buf :: Foreign, mime :: String }
file = hush $ runExcept $ readImpl f
string = hush $ runExcept $ unsafeReadTagged "String" f
case file of
Just { filename, buf, mime } ->
let
buf' :: ArrayBuffer
buf' = unsafeCoerce buf
in
pure $ ValueFile (wrap <$> Nullable.toMaybe filename) buf' (MIME.fromString mime)
Nothing -> do
s <- liftMaybe (error $ "invalid form value " <> unsafeStringify f) string
pure $ ValueString s
derive instance Generic Value _
instance Show Value where
show (ValueString s) = "(ValueString " <> show s <> ")"
show (ValueFile filename _ mime) = "(ValueFile (" <> show filename <> ") <ArrayBuffer> (" <> show mime <> "))"
newtype Form = Form (Map String (Array Value))
derive instance Newtype Form _
derive newtype instance Show Form
fromRaw :: forall m. MonadAff m => RawFormData -> m Form
fromRaw f = do
obj <- liftAff $ Promise.toAffE $ unsafeUnmakeFormData f
let
formMap = Map.fromFoldableWithIndex obj
map' <- liftEffect $ for formMap (\vs -> for vs valueFromForeign)
pure $ Form map'
toRawFormData :: forall m. MonadEffect m => Form -> m RawFormData
toRawFormData =
let
collect k o vs = do
o' <- o
vs' <- for vs valueForeign
pure $ Object.insert k vs' o'
in
liftEffect <<< flip bind unsafeMakeFormData <<< foldlWithIndex collect (pure Object.empty) <<< unwrap

94
src/HTTP/Header.purs Normal file
View File

@ -0,0 +1,94 @@
module HTTP.Header where
import Prelude
import Data.Eq.Generic (genericEq)
import Data.Generic.Rep (class Generic)
import Data.Map (Map)
import Data.Map as Map
import Data.Maybe (Maybe, maybe)
import Data.Newtype (class Newtype, unwrap, wrap)
import Data.Show.Generic (genericShow)
import Data.Tuple (Tuple(..))
import Effect (Effect)
import Effect.Class (class MonadEffect, liftEffect)
import HTTP.MIME (MIME)
import HTTP.MIME as MIME
import Node.Buffer as Buffer
import Node.Encoding (Encoding(..))
newtype ContentType = ContentType MIME
derive instance Newtype ContentType _
derive newtype instance Show ContentType
derive newtype instance Eq ContentType
newtype Accept = Accept MIME
derive instance Newtype Accept _
derive newtype instance Show Accept
derive newtype instance Eq Accept
newtype AuthScheme = AuthScheme String
derive instance Newtype AuthScheme _
derive newtype instance Show AuthScheme
derive newtype instance Eq AuthScheme
derive newtype instance Ord AuthScheme
data Authorization
= AuthBasic { username :: String, password :: String }
| AuthBearer String
| AuthCustom AuthScheme String
derive instance Generic Authorization _
instance Show Authorization where
show = genericShow
instance Eq Authorization where
eq = genericEq
authorizationValue :: forall m. MonadEffect m => Authorization -> m String
authorizationValue (AuthBasic { username, password }) = do
buf <- liftEffect $ Buffer.fromString (username <> ":" <> password) UTF8
val <- liftEffect $ Buffer.toString Base64 buf
authorizationValue $ AuthCustom (wrap "Basic") val
authorizationValue (AuthBearer val) = authorizationValue $ AuthCustom (wrap "Bearer") val
authorizationValue (AuthCustom (AuthScheme scheme) val) = pure $ scheme <> " " <> val
newtype Headers = Headers (Map String String)
derive instance Newtype Headers _
derive newtype instance Eq Headers
derive newtype instance Ord Headers
derive newtype instance Show Headers
instance Semigroup Headers where
append (Headers amap) (Headers bmap) = wrap $ Map.union amap bmap
instance Monoid Headers where
mempty = wrap $ Map.empty
class IntoHeaders a where
headers :: forall m. MonadEffect m => a -> m Headers
instance IntoHeaders Headers where
headers = pure
else instance IntoHeaders (Map String String) where
headers = pure <<< wrap
else instance IntoHeaders (Tuple String String) where
headers (Tuple k v) = headers $ Map.singleton k v
else instance IntoHeaders Unit where
headers _ = pure $ wrap Map.empty
else instance IntoHeaders ContentType where
headers = headers <<< Map.singleton "Content-Type" <<< MIME.toString <<< unwrap
else instance IntoHeaders Accept where
headers = headers <<< Map.singleton "Accept" <<< MIME.toString <<< unwrap
else instance IntoHeaders Authorization where
headers = map wrap <<< map (Map.singleton "Authorization") <<< authorizationValue
else instance (IntoHeaders a, IntoHeaders b) => IntoHeaders (Tuple a b) where
headers (Tuple a b) = do
a' <- unwrap <$> headers a
b' <- unwrap <$> headers b
headers $ Map.union a' b'
else instance IntoHeaders a => IntoHeaders (Maybe a) where
headers = maybe (pure mempty) headers

166
src/HTTP/MIME.purs Normal file
View File

@ -0,0 +1,166 @@
module HTTP.MIME where
import Prelude
import Data.Eq.Generic (genericEq)
import Data.Generic.Rep (class Generic)
import Data.Show.Generic (genericShow)
data MIME = Other String | Aac | Abw | Arc | Avif | Avi | Azw | Bin | Bmp | Bz | Bz2 | Cda | Csh | Css | Csv | Doc | Docx | Eot | Epub | Gz | Gif | Html | Ico | Ics | Jar | Jpeg | Js | Json | Jsonld | Midi | Mjs | Mp3 | Mp4 | Mpeg | Mpkg | Odp | Ods | Odt | Oga | Ogv | Ogx | Opus | Otf | Png | Pdf | Php | Ppt | Pptx | Rar | Rtf | Sh | Svg | Tar | Tif | Ts | Ttf | Txt | Vsd | Wav | Weba | Webm | Webp | Woff | Woff2 | Xhtml | Xls | Xlsx | Xml | Xul | Zip | Video3gp | Video3g2 | Archive7z
derive instance Generic MIME _
instance Show MIME where
show = genericShow
instance Eq MIME where
eq = genericEq
toString :: MIME -> String
toString (Other s) = s
toString Aac = "audio/aac"
toString Abw = "application/x-abiword"
toString Arc = "application/x-freearc"
toString Avif = "image/avif"
toString Avi = "video/x-msvideo"
toString Azw = "application/vnd.amazon.ebook"
toString Bin = "application/octet-stream"
toString Bmp = "OS/2 Bitmap Graphics image/bmp"
toString Bz = "application/x-bzip"
toString Bz2 = "application/x-bzip2"
toString Cda = "application/x-cdf"
toString Csh = "application/x-csh"
toString Css = "text/css"
toString Csv = "text/csv"
toString Doc = "application/msword"
toString Docx = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
toString Eot = "application/vnd.ms-fontobject"
toString Epub = "application/epub+zip"
toString Gz = "application/gzip"
toString Gif = "image/gif"
toString Html = "text/html"
toString Ico = "image/vnd.microsoft.icon"
toString Ics = "text/calendar"
toString Jar = "application/java-archive"
toString Jpeg = "image/jpeg"
toString Js = "text/javascript (Specifications: HTML and RFC 9239)"
toString Json = "application/json"
toString Jsonld = "application/ld+json"
toString Midi = "audio/midi"
toString Mjs = "text/javascript"
toString Mp3 = "audio/mpeg"
toString Mp4 = "video/mp4"
toString Mpeg = "video/mpeg"
toString Mpkg = "application/vnd.apple.installer+xml"
toString Odp = "application/vnd.oasis.opendocument.presentation"
toString Ods = "application/vnd.oasis.opendocument.spreadsheet"
toString Odt = "application/vnd.oasis.opendocument.text"
toString Oga = "audio/ogg"
toString Ogv = "video/ogg"
toString Ogx = "application/ogg"
toString Opus = "audio/opus"
toString Otf = "font/otf"
toString Png = "image/png"
toString Pdf = "application/pdf"
toString Php = "application/x-httpd-php"
toString Ppt = "application/vnd.ms-powerpoint"
toString Pptx = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
toString Rar = "application/vnd.rar"
toString Rtf = "application/rtf"
toString Sh = "application/x-sh"
toString Svg = "image/svg+xml"
toString Tar = "application/x-tar"
toString Tif = "image/tiff"
toString Ts = "video/mp2t"
toString Ttf = "font/ttf"
toString Txt = "text/plain"
toString Vsd = "application/vnd.visio"
toString Wav = "audio/wav"
toString Weba = "audio/webm"
toString Webm = "video/webm"
toString Webp = "image/webp"
toString Woff = "font/woff"
toString Woff2 = "font/woff2"
toString Xhtml = "application/xhtml+xml"
toString Xls = "application/vnd.ms-excel"
toString Xlsx = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
toString Xml = "application/xml"
toString Xul = "application/vnd.mozilla.xul+xml"
toString Zip = "application/zip"
toString Video3gp = "video/3gpp"
toString Video3g2 = "video/3gpp2"
toString Archive7z = "application/x-7z-compressed"
fromString :: String -> MIME
fromString "audio/aac" = Aac
fromString "application/x-abiword" = Abw
fromString "application/x-freearc" = Arc
fromString "image/avif" = Avif
fromString "video/x-msvideo" = Avi
fromString "application/vnd.amazon.ebook" = Azw
fromString "application/octet-stream" = Bin
fromString "OS/2 Bitmap Graphics image/bmp" = Bmp
fromString "application/x-bzip" = Bz
fromString "application/x-bzip2" = Bz2
fromString "application/x-cdf" = Cda
fromString "application/x-csh" = Csh
fromString "text/css" = Css
fromString "text/csv" = Csv
fromString "application/msword" = Doc
fromString "application/vnd.openxmlformats-officedocument.wordprocessingml.document" = Docx
fromString "application/vnd.ms-fontobject" = Eot
fromString "application/epub+zip" = Epub
fromString "application/gzip" = Gz
fromString "image/gif" = Gif
fromString "text/html" = Html
fromString "image/vnd.microsoft.icon" = Ico
fromString "text/calendar" = Ics
fromString "application/java-archive" = Jar
fromString "image/jpeg" = Jpeg
fromString "text/javascript (Specifications: HTML and RFC 9239)" = Js
fromString "application/json" = Json
fromString "application/ld+json" = Jsonld
fromString "audio/midi" = Midi
fromString "text/javascript" = Mjs
fromString "audio/mpeg" = Mp3
fromString "video/mp4" = Mp4
fromString "video/mpeg" = Mpeg
fromString "application/vnd.apple.installer+xml" = Mpkg
fromString "application/vnd.oasis.opendocument.presentation" = Odp
fromString "application/vnd.oasis.opendocument.spreadsheet" = Ods
fromString "application/vnd.oasis.opendocument.text" = Odt
fromString "audio/ogg" = Oga
fromString "video/ogg" = Ogv
fromString "application/ogg" = Ogx
fromString "audio/opus" = Opus
fromString "font/otf" = Otf
fromString "image/png" = Png
fromString "application/pdf" = Pdf
fromString "application/x-httpd-php" = Php
fromString "application/vnd.ms-powerpoint" = Ppt
fromString "application/vnd.openxmlformats-officedocument.presentationml.presentation" = Pptx
fromString "application/vnd.rar" = Rar
fromString "application/rtf" = Rtf
fromString "application/x-sh" = Sh
fromString "image/svg+xml" = Svg
fromString "application/x-tar" = Tar
fromString "image/tiff" = Tif
fromString "video/mp2t" = Ts
fromString "font/ttf" = Ttf
fromString "text/plain" = Txt
fromString "application/vnd.visio" = Vsd
fromString "audio/wav" = Wav
fromString "audio/webm" = Weba
fromString "video/webm" = Webm
fromString "image/webp" = Webp
fromString "font/woff" = Woff
fromString "font/woff2" = Woff2
fromString "application/xhtml+xml" = Xhtml
fromString "application/vnd.ms-excel" = Xls
fromString "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" = Xlsx
fromString "application/xml" = Xml
fromString "application/vnd.mozilla.xul+xml" = Xul
fromString "application/zip" = Zip
fromString "video/3gpp" = Video3gp
fromString "video/3gpp2" = Video3g2
fromString "application/x-7z-compressed" = Archive7z
fromString s = Other s

2
src/HTTP/Request.js Normal file
View File

@ -0,0 +1,2 @@
/** @type {(_: Blob) => () => Promise<ArrayBuffer>} */
export const blobArrayBufferImpl = b => () => b.arrayBuffer()

177
src/HTTP/Request.purs Normal file
View File

@ -0,0 +1,177 @@
module HTTP.Request
( class Request
, Body
, RawRequestBody
, Method(..)
, json
, form
, blob
, buffer
, arrayBuffer
, requestBody
, requestHeaders
, requestUrl
, requestMethod
) where
import Prelude
import Control.Promise (Promise)
import Control.Promise as Promise
import Data.ArrayBuffer.ArrayBuffer as ArrayBuffer
import Data.ArrayBuffer.Types (ArrayBuffer)
import Data.Map (Map)
import Data.Map as Map
import Data.Maybe (Maybe(..))
import Data.Newtype (unwrap)
import Data.Tuple (Tuple, fst, snd)
import Data.Tuple.Nested ((/\))
import Effect (Effect)
import Effect.Aff.Class (class MonadAff, liftAff)
import Effect.Class (class MonadEffect, liftEffect)
import HTTP.Form (Form, RawFormData)
import HTTP.Form as Form
import HTTP.Header (ContentType(..), Headers(..))
import HTTP.Header as Header
import HTTP.MIME (MIME)
import HTTP.MIME as MIME
import HTTP.URL (URL)
import Node.Buffer (Buffer)
import Node.Buffer as Buffer
import Node.Encoding (Encoding(..))
import Simple.JSON (class WriteForeign, writeJSON)
import Unsafe.Coerce (unsafeCoerce)
import Web.File.Blob (Blob)
import Web.File.Blob as Blob
foreign import blobArrayBufferImpl :: Blob -> Effect (Promise ArrayBuffer)
foreign import data RawRequestBody :: Type
unsafeFormDataToRawRequestBody :: RawFormData -> RawRequestBody
unsafeFormDataToRawRequestBody = unsafeCoerce
unsafeArrayBufferToRawRequestBody :: ArrayBuffer -> RawRequestBody
unsafeArrayBufferToRawRequestBody = unsafeCoerce
unsafeBlobToRawRequestBody :: forall m. MonadAff m => Blob -> m RawRequestBody
unsafeBlobToRawRequestBody = map unsafeArrayBufferToRawRequestBody <<< liftAff <<< Promise.toAffE <<< blobArrayBufferImpl
data Body
= BodyString String (Maybe ContentType)
| BodyArrayBuffer ArrayBuffer (Maybe ContentType)
| BodyBuffer Buffer (Maybe ContentType)
| BodyBlob Blob
| BodyForm Form
| BodyEmpty
json :: forall a. WriteForeign a => a -> Body
json = flip BodyString (Just $ ContentType MIME.Json) <<< writeJSON
form :: Form -> Body
form = BodyForm
blob :: Blob -> Body
blob = BodyBlob
buffer :: MIME -> Buffer -> Body
buffer mime buf = BodyBuffer buf $ Just $ ContentType mime
arrayBuffer :: MIME -> ArrayBuffer -> Body
arrayBuffer mime buf = BodyArrayBuffer buf $ Just $ ContentType mime
bodyHeaders :: forall m. MonadEffect m => Body -> m Headers
bodyHeaders (BodyForm _) = pure mempty
bodyHeaders (BodyEmpty) = pure mempty
bodyHeaders (BodyString _ ct) = liftEffect $ Header.headers ct
bodyHeaders (BodyBuffer _ ct) = liftEffect $ Header.headers ct
bodyHeaders (BodyArrayBuffer _ ct) = liftEffect $ Header.headers ct
bodyHeaders (BodyBlob b) = Header.headers <<< map (ContentType <<< MIME.fromString <<< unwrap) $ Blob.type_ b
bodyToRaw :: forall m. MonadAff m => Body -> m RawRequestBody
bodyToRaw (BodyString body ct) = flip bind bodyToRaw $ liftEffect $ map (flip BodyBuffer ct) $ Buffer.fromString body UTF8
bodyToRaw (BodyBuffer body ct) = flip bind bodyToRaw $ liftEffect $ map (flip BodyArrayBuffer ct) $ Buffer.toArrayBuffer body
bodyToRaw (BodyArrayBuffer body _) = pure $ unsafeArrayBufferToRawRequestBody body
bodyToRaw (BodyForm form') = map unsafeFormDataToRawRequestBody $ Form.toRawFormData form'
bodyToRaw (BodyBlob body) = unsafeBlobToRawRequestBody body
bodyToRaw BodyEmpty = liftEffect $ map unsafeArrayBufferToRawRequestBody $ ArrayBuffer.empty 0
data Method
= GET
| PUT
| POST
| DELETE
| PATCH
-- | given a tuple of any size with at least 1 value
-- | of type `a`, `extract` the first occurence of `a`
-- | from the tuple
class TupleContaining a tup where
extract :: tup -> a
instance TupleContaining a a where
extract = identity
else instance TupleContaining a (Tuple a b) where
extract = fst
else instance TupleContaining b (Tuple a b) where
extract = snd
else instance TupleContaining a tail => TupleContaining a (Tuple head tail) where
extract (_ /\ tail) = extract tail
class Request :: Type -> Constraint
class Request a where
requestUrl :: forall m. MonadAff m => a -> m URL
requestMethod :: forall m. MonadAff m => a -> m Method
requestBody :: forall m. MonadAff m => a -> m (Maybe RawRequestBody)
requestHeaders :: forall m. MonadAff m => a -> m (Map String String)
instance
( TupleContaining Body a
, TupleContaining URL a
, TupleContaining Method a
, TupleContaining Headers a
) =>
Request a where
requestUrl = pure <<< extract
requestMethod = pure <<< extract
requestBody = map Just <<< bodyToRaw <<< extract
requestHeaders req = do
let (Headers hs) = extract req
(Headers bodyHs) <- bodyHeaders $ extract req
pure $ Map.union hs bodyHs
else instance
( TupleContaining Body a
, TupleContaining URL a
, TupleContaining Method a
) =>
Request a where
requestUrl = pure <<< extract
requestMethod = pure <<< extract
requestBody = map Just <<< bodyToRaw <<< extract
requestHeaders _ = pure Map.empty
else instance
( TupleContaining Headers a
, TupleContaining URL a
, TupleContaining Method a
) =>
Request a where
requestUrl = pure <<< extract
requestMethod = pure <<< extract
requestBody _ = Just <$> bodyToRaw BodyEmpty
requestHeaders = (\(Headers h) -> pure h) <<< extract
else instance
( TupleContaining URL a
, TupleContaining Method a
) =>
Request a where
requestUrl = pure <<< extract
requestMethod = pure <<< extract
requestBody _ = Just <$> bodyToRaw BodyEmpty
requestHeaders _ = pure Map.empty
else instance
( TupleContaining URL a
) =>
Request a where
requestUrl = pure <<< extract
requestMethod _ = pure GET
requestBody _ = Just <$> bodyToRaw BodyEmpty
requestHeaders _ = pure Map.empty

34
src/HTTP/Response.js Normal file
View File

@ -0,0 +1,34 @@
/// <reference lib="dom" />
/** @type {(_: Response) => () => Promise<unknown>} */
export const jsonImpl = rep => () => rep.json()
/** @type {(_: Response) => () => Promise<string>} */
export const textImpl = rep => () => rep.text()
/** @type {(_: Response) => () => Promise<ArrayBuffer>} */
export const abImpl = rep => () => rep.arrayBuffer()
/** @type {(_: Response) => () => Promise<Blob>} */
export const blobImpl = rep => () => rep.blob()
/** @type {(_: Response) => () => Promise<FormData>} */
export const formImpl = rep => () => rep.formData()
/** @type {(_: Response) => () => number} */
export const statusImpl = rep => () => rep.status
/** @type {(_: Response) => () => string} */
export const statusTextImpl = rep => () => rep.statusText
/** @type {(_: Response) => () => Record<string, string>} */
export const headersImpl = rep => () => {
/** @type {Record<string, string>} */
const hs = {}
Array.from(rep.headers.entries()).forEach(([k, v]) => {
hs[k] = v
})
return hs
}

79
src/HTTP/Response.purs Normal file
View File

@ -0,0 +1,79 @@
module HTTP.Response
( Response(..)
, json
, text
, blob
, arrayBuffer
, formData
, headers
, status
, statusText
, guardStatusOk
) where
import Prelude
import Control.Monad.Error.Class (class MonadThrow, liftEither, throwError)
import Control.Monad.Except (runExcept)
import Control.Promise (Promise)
import Control.Promise as Promise
import Data.ArrayBuffer.Types (ArrayBuffer)
import Data.Bifunctor (lmap)
import Data.Int as Int
import Data.Map (Map)
import Data.Map as Map
import Data.Number ((%))
import Effect (Effect)
import Effect.Aff.Class (class MonadAff, liftAff)
import Effect.Class (class MonadEffect, liftEffect)
import Effect.Exception (Error, error)
import Foreign (Foreign)
import Foreign.Object (Object)
import HTTP.Form (Form, RawFormData)
import HTTP.Form as Form
import Simple.JSON (class ReadForeign, readImpl)
import Web.File.Blob (Blob)
foreign import data Response :: Type
foreign import statusImpl :: Response -> Effect Int
foreign import statusTextImpl :: Response -> Effect String
foreign import headersImpl :: Response -> Effect (Object String)
foreign import jsonImpl :: Response -> Effect (Promise Foreign)
foreign import textImpl :: Response -> Effect (Promise String)
foreign import abImpl :: Response -> Effect (Promise ArrayBuffer)
foreign import blobImpl :: Response -> Effect (Promise Blob)
foreign import formImpl :: Response -> Effect (Promise RawFormData)
guardStatusOk :: forall m. MonadAff m => MonadThrow Error m => Response -> m Unit
guardStatusOk rep = do
status' <- status rep
statusText' <- statusText rep
if Int.toNumber status' % 200.0 > 100.0 then
throwError $ error $ "status not OK: " <> show status' <> " " <> statusText'
else
pure unit
json :: forall m @a. MonadAff m => ReadForeign a => Response -> m a
json = liftAff <<< flip bind (liftEither <<< lmap (error <<< show) <<< runExcept <<< readImpl) <<< Promise.toAffE <<< jsonImpl
text :: forall m. MonadAff m => Response -> m String
text = liftAff <<< Promise.toAffE <<< textImpl
blob :: forall m. MonadAff m => Response -> m Blob
blob = liftAff <<< Promise.toAffE <<< blobImpl
arrayBuffer :: forall m. MonadAff m => Response -> m ArrayBuffer
arrayBuffer = liftAff <<< Promise.toAffE <<< abImpl
formData :: forall m. MonadAff m => Response -> m Form
formData = liftAff <<< flip bind Form.fromRaw <<< Promise.toAffE <<< formImpl
headers :: forall m. MonadEffect m => Response -> m (Map String String)
headers = liftEffect <<< map Map.fromFoldableWithIndex <<< headersImpl
status :: forall m. MonadEffect m => Response -> m Int
status = liftEffect <<< statusImpl
statusText :: forall m. MonadEffect m => Response -> m String
statusText = liftEffect <<< statusTextImpl

12
src/HTTP/URL.purs Normal file
View File

@ -0,0 +1,12 @@
module HTTP.URL where
import Prelude
import Data.Newtype (class Newtype)
newtype URL = URL String
derive instance Newtype URL _
derive newtype instance Show URL
derive newtype instance Eq URL
derive newtype instance Ord URL