diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc070b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/.* +!/.gitignore +/bower_components/ +/node_modules/ +/output/ +/tmp/ diff --git a/README.md b/README.md index 374919b..7ed66c0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,438 @@ -# purescript-affjax -An asynchronous AJAX library built using Aff. +# Module Documentation + +## Module Network.Affjax + +#### `runAffjax` + +``` purescript +runAffjax :: forall e a. AffjaxRequest a -> Aff (ajax :: Ajax | e) AjaxResponse +``` + + + +## Module Network.Affjax.DSL + +#### `AffjaxRequest` + +``` purescript +type AffjaxRequest = FreeC AffjaxRequestF +``` + +A free monad for building AJAX requests + +#### `AffjaxRequestF` + +``` purescript +data AffjaxRequestF a + = SetURL String a + | SetMethod Method a + | AddHeader RequestHeader a + | SetContent (Maybe Content) a + | SetUsername (Maybe String) a + | SetPassword (Maybe String) a +``` + +The request DSL AST. + +#### `affjaxRequest` + +``` purescript +affjaxRequest :: forall a. AffjaxRequest a -> AjaxRequest +``` + +Runs the DSL, producing an `AjaxRequest` object. + +#### `url` + +``` purescript +url :: String -> AffjaxRequest Unit +``` + +Sets the URL for a request. + +#### `method` + +``` purescript +method :: Method -> AffjaxRequest Unit +``` + +Sets the request method based on an HTTP verb. + +#### `header` + +``` purescript +header :: RequestHeader -> AffjaxRequest Unit +``` + +Adds a header to the request. + +#### `content` + +``` purescript +content :: Content -> AffjaxRequest Unit +``` + +Sets the content for the request. + +#### `content'` + +``` purescript +content' :: Maybe Content -> AffjaxRequest Unit +``` + +Sets the content for the request, with the option of setting it to +`Nothing`. + +#### `username` + +``` purescript +username :: String -> AffjaxRequest Unit +``` + +Sets the username for the request. + +#### `username'` + +``` purescript +username' :: Maybe String -> AffjaxRequest Unit +``` + +Sets the username for the request, with the option of setting it to +`Nothing`. + +#### `password` + +``` purescript +password :: String -> AffjaxRequest Unit +``` + +Sets the password for the request. + +#### `password'` + +``` purescript +password' :: Maybe String -> AffjaxRequest Unit +``` + +Sets the password for the request, with the option of setting it to +`Nothing`. + + +## Module Network.Affjax.Request + +#### `Ajax` + +``` purescript +data Ajax :: ! +``` + +The event type for AJAX requests. + +#### `AjaxRequest` + +``` purescript +type AjaxRequest = { password :: Maybe String, username :: Maybe String, content :: Maybe Content, headers :: [RequestHeader], method :: Method, url :: String } +``` + +The parameters for an AJAX request. + +#### `Content` + +``` purescript +data Content + = ArrayViewContent (Exists ArrayView) + | BlobContent Blob + | DocumentContent Document + | TextContent String + | FormDataContent FormData +``` + +The types of data that can be set in an AJAX request. + +#### `AjaxResponse` + +``` purescript +newtype AjaxResponse +``` + +#### `defaultRequest` + +``` purescript +defaultRequest :: AjaxRequest +``` + +A basic request, `GET /` with no particular headers or credentials. + +#### `ajax` + +``` purescript +ajax :: forall e. AjaxRequest -> Aff (ajax :: Ajax | e) AjaxResponse +``` + +Make an AJAX request. + + +## Module Network.HTTP.Method + +#### `Method` + +``` purescript +data Method + = DELETE + | GET + | HEAD + | OPTIONS + | PATCH + | POST + | PUT + | CustomMethod String +``` + + +#### `eqMethod` + +``` purescript +instance eqMethod :: Eq Method +``` + + +#### `showMethod` + +``` purescript +instance showMethod :: Show Method +``` + + +#### `methodToString` + +``` purescript +methodToString :: Method -> String +``` + + + +## Module Network.HTTP.MimeType + +#### `MimeType` + +``` purescript +newtype MimeType + = MimeType String +``` + + +#### `eqMimeType` + +``` purescript +instance eqMimeType :: Eq MimeType +``` + + +#### `showMimeType` + +``` purescript +instance showMimeType :: Show MimeType +``` + + +#### `mimeTypeToString` + +``` purescript +mimeTypeToString :: MimeType -> String +``` + + + +## Module Network.HTTP.RequestHeader + +#### `RequestHeader` + +``` purescript +data RequestHeader + = Accept MimeType + | ContentType MimeType + | RequestHeader String String +``` + + +#### `eqRequestHeader` + +``` purescript +instance eqRequestHeader :: Eq RequestHeader +``` + + +#### `showRequestHeader` + +``` purescript +instance showRequestHeader :: Show RequestHeader +``` + + +#### `requestHeaderName` + +``` purescript +requestHeaderName :: RequestHeader -> String +``` + + +#### `requestHeaderValue` + +``` purescript +requestHeaderValue :: RequestHeader -> String +``` + + + +## Module Network.HTTP.ResponseHeader + +#### `ResponseHeader` + +``` purescript +data ResponseHeader + = ResponseHeader String String +``` + + +#### `eqResponseHeader` + +``` purescript +instance eqResponseHeader :: Eq ResponseHeader +``` + + +#### `showResponseHeader` + +``` purescript +instance showResponseHeader :: Show ResponseHeader +``` + + +#### `responseHeaderName` + +``` purescript +responseHeaderName :: ResponseHeader -> String +``` + + +#### `responseHeaderValue` + +``` purescript +responseHeaderValue :: ResponseHeader -> String +``` + + + +## Module Network.HTTP.StatusCode + +#### `StatusCode` + +``` purescript +newtype StatusCode + = StatusCode Int +``` + + +#### `eqStatusCode` + +``` purescript +instance eqStatusCode :: Eq StatusCode +``` + + +#### `showStatusCode` + +``` purescript +instance showStatusCode :: Show StatusCode +``` + + + +## Module Network.HTTP.MimeType.Common + +#### `applicationFormURLEncoded` + +``` purescript +applicationFormURLEncoded :: MimeType +``` + + +#### `applicationJSON` + +``` purescript +applicationJSON :: MimeType +``` + + +#### `applicationJavascript` + +``` purescript +applicationJavascript :: MimeType +``` + + +#### `applicationOctetStream` + +``` purescript +applicationOctetStream :: MimeType +``` + + +#### `applicationXML` + +``` purescript +applicationXML :: MimeType +``` + + +#### `imageGIF` + +``` purescript +imageGIF :: MimeType +``` + + +#### `imageJPEG` + +``` purescript +imageJPEG :: MimeType +``` + + +#### `imagePNG` + +``` purescript +imagePNG :: MimeType +``` + + +#### `multipartFormData` + +``` purescript +multipartFormData :: MimeType +``` + + +#### `textCSV` + +``` purescript +textCSV :: MimeType +``` + + +#### `textPlain` + +``` purescript +textPlain :: MimeType +``` + + +#### `textXML` + +``` purescript +textXML :: MimeType +``` + + + + diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..3e05acf --- /dev/null +++ b/bower.json @@ -0,0 +1,32 @@ +{ + "name": "purescript-affjax", + "homepage": "https://github.com/slamdata/purescript-affjax", + "description": "An asynchronous AJAX library built using Aff.", + "keywords": [ + "purescript", + "ajax" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "bower_components", + "node_modules", + "output", + "tests", + "tmp", + "bower.json", + "Gruntfile.js", + "package.json" + ], + "dependencies": { + "purescript-aff": "~0.6.0", + "purescript-exceptions": "~0.2.2", + "purescript-free": "~0.4.0", + "purescript-transformers": "~0.5.1", + "purescript-arrays": "~0.3.3", + "purescript-maybe": "~0.2.1", + "purescript-nullable": "~0.1.1", + "purescript-arraybuffer-types": "~0.1.1", + "purescript-dom": "~0.1.2" + } +} diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..1466ca9 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,27 @@ +"use strict"; + +var gulp = require("gulp"); +var plumber = require("gulp-plumber"); +var purescript = require("gulp-purescript"); +var jsvalidate = require("gulp-jsvalidate"); + +gulp.task("make", function() { + return gulp.src(["src/**/*.purs", "bower_components/purescript-*/src/**/*.purs"]) + .pipe(plumber()) + .pipe(purescript.pscMake()); +}); + +gulp.task("jsvalidate", ["make"], function () { + return gulp.src("output/**/*.js") + .pipe(plumber()) + .pipe(jsvalidate()); +}); + +gulp.task("docs", function () { + return gulp.src("src/**/*.purs") + .pipe(plumber()) + .pipe(purescript.pscDocs()) + .pipe(gulp.dest("README.md")); +}); + +gulp.task("default", ["jsvalidate", "docs"]); diff --git a/package.json b/package.json new file mode 100644 index 0000000..327f797 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "private": true, + "devDependencies": { + "gulp": "^3.8.11", + "gulp-jsvalidate": "^1.0.1", + "gulp-plumber": "^1.0.0", + "gulp-purescript": "^0.1.2" + } +} diff --git a/src/Network/Affjax.purs b/src/Network/Affjax.purs new file mode 100644 index 0000000..6bf7ebd --- /dev/null +++ b/src/Network/Affjax.purs @@ -0,0 +1,8 @@ +module Network.Affjax where + +import Control.Monad.Aff +import Network.Affjax.DSL +import Network.Affjax.Request + +runAffjax :: forall e a. AffjaxRequest a -> Aff (ajax :: Ajax | e) AjaxResponse +runAffjax = ajax <<< affjaxRequest diff --git a/src/Network/Affjax/DSL.purs b/src/Network/Affjax/DSL.purs new file mode 100644 index 0000000..5345a7b --- /dev/null +++ b/src/Network/Affjax/DSL.purs @@ -0,0 +1,88 @@ +module Network.Affjax.DSL + ( AffjaxRequest() + , AffjaxRequestF(..) + , affjaxRequest + , url + , method + , header + , content + , content' + , username + , username' + , password + , password' + ) where + +import Control.Monad.Free (FreeC(), liftFC, runFreeCM) +import Control.Monad.State (State(), execState) +import Control.Monad.State.Class (modify) +import Data.Coyoneda (Natural()) +import Data.Maybe (Maybe(..)) +import Network.Affjax.Request +import Network.Affjax.Requestable +import Network.HTTP.Method (Method()) +import Network.HTTP.RequestHeader (RequestHeader()) + +-- | A free monad for building AJAX requests +type AffjaxRequest = FreeC AffjaxRequestF + +-- | The request DSL AST. +data AffjaxRequestF a + = SetURL String a + | SetMethod Method a + | AddHeader RequestHeader a + | SetContent (Maybe AjaxContent) a + | SetUsername (Maybe String) a + | SetPassword (Maybe String) a + +-- | The interpreter for the request DSL AST. +affjaxN :: Natural AffjaxRequestF (State AjaxRequest) +affjaxN (SetURL url a) = const a <$> modify (_ { url = url }) +affjaxN (SetMethod method a) = const a <$> modify (_ { method = method }) +affjaxN (AddHeader header a) = const a <$> modify (\req -> req { headers = header : req.headers }) +affjaxN (SetContent content a) = const a <$> modify (_ { content = content }) +affjaxN (SetUsername username a) = const a <$> modify (_ { username = username }) +affjaxN (SetPassword password a) = const a <$> modify (_ { password = password }) + +-- | Runs the DSL, producing an `AjaxRequest` object. +affjaxRequest :: forall a. AffjaxRequest a -> AjaxRequest +affjaxRequest = (`execState` defaultRequest) <<< runFreeCM affjaxN + +-- | Sets the URL for a request. +url :: String -> AffjaxRequest Unit +url url = liftFC (SetURL url unit) + +-- | Sets the request method based on an HTTP verb. +method :: Method -> AffjaxRequest Unit +method meth = liftFC (SetMethod meth unit) + +-- | Adds a header to the request. +header :: RequestHeader -> AffjaxRequest Unit +header header = liftFC (AddHeader header unit) + +-- | Sets the content for the request. +content :: forall c. (AjaxRequestable c) => c -> AffjaxRequest Unit +content value = content' (Just value) + +-- | Sets the content for the request, with the option of setting it to +-- | `Nothing`. +content' :: forall c. (AjaxRequestable c) => Maybe c -> AffjaxRequest Unit +content' value = liftFC (SetContent (toContent <$> value) unit) + +-- | Sets the username for the request. +username :: String -> AffjaxRequest Unit +username value = username' (Just value) + +-- | Sets the username for the request, with the option of setting it to +-- | `Nothing`. +username' :: Maybe String -> AffjaxRequest Unit +username' value = liftFC (SetUsername value unit) + +-- | Sets the password for the request. +password :: String -> AffjaxRequest Unit +password value = password' (Just value) + +-- | Sets the password for the request, with the option of setting it to +-- | `Nothing`. +password' :: Maybe String -> AffjaxRequest Unit +password' value = liftFC (SetPassword value unit) diff --git a/src/Network/Affjax/Request.purs b/src/Network/Affjax/Request.purs new file mode 100644 index 0000000..2438d4d --- /dev/null +++ b/src/Network/Affjax/Request.purs @@ -0,0 +1,89 @@ +module Network.Affjax.Request + ( Ajax() + , AjaxContent() + , AjaxRequest() + , AjaxResponse() + , defaultRequest + , ajax + ) where + +import Control.Monad.Aff (Aff(), EffA(), makeAff) +import Control.Monad.Eff (Eff()) +import Control.Monad.Eff.Exception (Error()) +import Data.Function (Fn8(), runFn8) +import Data.Maybe (Maybe(..), maybe) +import Data.Nullable (Nullable(), toNullable) +import Network.HTTP.Method (Method(..), methodToString) +import Network.HTTP.RequestHeader (RequestHeader(), requestHeaderName, requestHeaderValue) + +-- | The event type for AJAX requests. +foreign import data Ajax :: ! + +-- | Type subsuming all content types that can be sent in a request. +foreign import data AjaxContent :: * + +-- | The parameters for an AJAX request. +type AjaxRequest = + { url :: String + , method :: Method + , headers :: [RequestHeader] + , content :: Maybe AjaxContent + , username :: Maybe String + , password :: Maybe String + } + +-- TODO: probably not this? Do we want to deal with other responses, include headers, etc? +newtype AjaxResponse = AjaxResponse String + +-- | A basic request, `GET /` with no particular headers or credentials. +defaultRequest :: AjaxRequest +defaultRequest = + { url: "/" + , method: GET + , headers: [] + , content: Nothing + , username: Nothing + , password: Nothing + } + +-- | Make an AJAX request. +ajax :: forall e. AjaxRequest -> Aff (ajax :: Ajax | e) AjaxResponse +ajax req = makeAff $ runFn8 + unsafeAjax req.url + (methodToString req.method) + (runHeader <$> req.headers) + (toNullable req.content) + (toNullable req.username) + (toNullable req.password) + where + runHeader :: RequestHeader -> { head :: String, value :: String } + runHeader h = { head: requestHeaderName h, value: requestHeaderValue h } + +foreign import unsafeAjax + """ + function unsafeAjax (url, method, headers, content, username, password, errback, callback) { + return function () { + var xhr = new XMLHttpRequest(); + xhr.open(method, url, true, username, password); + for (var i = 0, header; header = headers[i]; i++) { + xhr.setRequestHeader(header.head, header.value); + } + xhr.onerror = function (err) { + errback(err)(); + }; + xhr.onload = function () { + if (xhr.status === 200) callback(xhr.response)(); + else errback(new Error("Request returned status " + xhr.status))(); + }; + xhr.send(content); + }; + } + """ :: forall e. Fn8 String + String + [{ head :: String, value :: String }] + (Nullable AjaxContent) + (Nullable String) + (Nullable String) + (Error -> Eff (ajax :: Ajax | e) Unit) + (AjaxResponse -> Eff (ajax :: Ajax | e) Unit) + (EffA (ajax :: Ajax | e) Unit) diff --git a/src/Network/Affjax/Requestable.purs b/src/Network/Affjax/Requestable.purs new file mode 100644 index 0000000..e9dc01c --- /dev/null +++ b/src/Network/Affjax/Requestable.purs @@ -0,0 +1,74 @@ +module Network.Affjax.Requestable + ( AjaxRequestable, requestMimeType, toContent + ) where + +import DOM (Document()) +import DOM.File (Blob()) +import DOM.XHR (FormData()) +import Network.Affjax.Request +import Network.HTTP.MimeType (MimeType()) +import Network.HTTP.MimeType.Common (applicationOctetStream, multipartFormData, textHTML, textPlain) +import qualified Data.ArrayBuffer.Types as A + +class AjaxRequestable a where + requestMimeType :: a -> MimeType + toContent :: a -> AjaxContent + +instance requestableInt8Array :: AjaxRequestable (A.ArrayView A.Int8) where + requestMimeType = const applicationOctetStream + toContent = unsafeToContent + +instance requestableInt16Array :: AjaxRequestable (A.ArrayView A.Int16) where + requestMimeType = const applicationOctetStream + toContent = unsafeToContent + +instance requestableInt32Array :: AjaxRequestable (A.ArrayView A.Int32) where + requestMimeType = const applicationOctetStream + toContent = unsafeToContent + +instance requestableUint8Array :: AjaxRequestable (A.ArrayView A.Uint8) where + requestMimeType = const applicationOctetStream + toContent = unsafeToContent + +instance requestableUint16Array :: AjaxRequestable (A.ArrayView A.Uint16) where + requestMimeType = const applicationOctetStream + toContent = unsafeToContent + +instance requestableUint32Array :: AjaxRequestable (A.ArrayView A.Uint32) where + requestMimeType = const applicationOctetStream + toContent = unsafeToContent + +instance requestableUint8ClampedArray :: AjaxRequestable (A.ArrayView A.Uint8Clamped) where + requestMimeType = const applicationOctetStream + toContent = unsafeToContent + +instance requestableFloat32Array :: AjaxRequestable (A.ArrayView A.Float32) where + requestMimeType = const applicationOctetStream + toContent = unsafeToContent + +instance requestableFloat64Array :: AjaxRequestable (A.ArrayView A.Float64) where + requestMimeType = const applicationOctetStream + toContent = unsafeToContent + +instance requestableBlob :: AjaxRequestable Blob where + requestMimeType = const applicationOctetStream + toContent = unsafeToContent + +instance requestableDocument :: AjaxRequestable Document where + requestMimeType = const textHTML + toContent = unsafeToContent + +instance requestableString :: AjaxRequestable String where + requestMimeType = const textPlain + toContent = unsafeToContent + +instance requestableFormData :: AjaxRequestable FormData where + requestMimeType = const multipartFormData + toContent = unsafeToContent + +foreign import unsafeToContent + """ + function unsafeToContent (x) { + return x; + } + """ :: forall a. a -> AjaxContent diff --git a/src/Network/HTTP/Method.purs b/src/Network/HTTP/Method.purs new file mode 100644 index 0000000..a9a1490 --- /dev/null +++ b/src/Network/HTTP/Method.purs @@ -0,0 +1,36 @@ +module Network.HTTP.Method where + +data Method + = DELETE + | GET + | HEAD + | OPTIONS + | PATCH + | POST + | PUT + | CustomMethod String + +instance eqMethod :: Eq Method where + (==) DELETE DELETE = true + (==) GET GET = true + (==) HEAD HEAD = true + (==) OPTIONS OPTIONS = true + (==) PATCH PATCH = true + (==) POST POST = true + (==) PUT PUT = true + (==) _ _ = false + (/=) x y = not (x == y) + +instance showMethod :: Show Method where + show DELETE = "DELETE" + show GET = "GET" + show HEAD = "HEAD" + show OPTIONS = "OPTIONS" + show PATCH = "PATCH" + show POST = "POST" + show PUT = "PUT" + show (CustomMethod m) = "(CustomMethod " ++ show m ++ ")" + +methodToString :: Method -> String +methodToString (CustomMethod m) = m +methodToString other = show other diff --git a/src/Network/HTTP/MimeType.purs b/src/Network/HTTP/MimeType.purs new file mode 100644 index 0000000..5e8acc2 --- /dev/null +++ b/src/Network/HTTP/MimeType.purs @@ -0,0 +1,13 @@ +module Network.HTTP.MimeType where + +newtype MimeType = MimeType String + +instance eqMimeType :: Eq MimeType where + (==) (MimeType x) (MimeType y) = x == y + (/=) (MimeType x) (MimeType y) = x /= y + +instance showMimeType :: Show MimeType where + show (MimeType h) = "(MimeType " ++ show h ++ ")" + +mimeTypeToString :: MimeType -> String +mimeTypeToString (MimeType s) = s diff --git a/src/Network/HTTP/MimeType/Common.purs b/src/Network/HTTP/MimeType/Common.purs new file mode 100644 index 0000000..976f328 --- /dev/null +++ b/src/Network/HTTP/MimeType/Common.purs @@ -0,0 +1,42 @@ +module Network.HTTP.MimeType.Common where + +import Network.HTTP.MimeType + +applicationFormURLEncoded :: MimeType +applicationFormURLEncoded = MimeType "application/x-www-form-urlencoded" + +applicationJSON :: MimeType +applicationJSON = MimeType "application/json" + +applicationJavascript :: MimeType +applicationJavascript = MimeType "application/javascript" + +applicationOctetStream :: MimeType +applicationOctetStream = MimeType "application/octet-stream" + +applicationXML :: MimeType +applicationXML = MimeType "application/xml" + +imageGIF :: MimeType +imageGIF = MimeType "image/gif" + +imageJPEG :: MimeType +imageJPEG = MimeType "image/jpeg" + +imagePNG :: MimeType +imagePNG = MimeType "image/png" + +multipartFormData :: MimeType +multipartFormData = MimeType "multipart/form-data" + +textCSV :: MimeType +textCSV = MimeType "text/csv" + +textHTML :: MimeType +textHTML = MimeType "text/html" + +textPlain :: MimeType +textPlain = MimeType "text/plain" + +textXML :: MimeType +textXML = MimeType "text/xml" diff --git a/src/Network/HTTP/RequestHeader.purs b/src/Network/HTTP/RequestHeader.purs new file mode 100644 index 0000000..9bf790d --- /dev/null +++ b/src/Network/HTTP/RequestHeader.purs @@ -0,0 +1,30 @@ +module Network.HTTP.RequestHeader where + +import Network.HTTP.MimeType + +data RequestHeader + = Accept MimeType + | ContentType MimeType + | RequestHeader String String + +instance eqRequestHeader :: Eq RequestHeader where + (==) (Accept m1) (Accept m2) = m1 == m2 + (==) (ContentType m1) (ContentType m2) = m1 == m2 + (==) (RequestHeader h1 v1) (RequestHeader h2 v2) = h1 == h2 && v1 == v2 + (==) _ _ = false + (/=) x y = not (x == y) + +instance showRequestHeader :: Show RequestHeader where + show (Accept m) = "(Accept " ++ show m ++ ")" + show (ContentType m) = "(ContentType " ++ show m ++ ")" + show (RequestHeader h v) = "(RequestHeader " ++ show h ++ " " ++ show v ++ ")" + +requestHeaderName :: RequestHeader -> String +requestHeaderName (Accept _) = "Accept" +requestHeaderName (ContentType _) = "ContentType" +requestHeaderName (RequestHeader h _) = h + +requestHeaderValue :: RequestHeader -> String +requestHeaderValue (Accept m) = mimeTypeToString m +requestHeaderValue (ContentType m) = mimeTypeToString m +requestHeaderValue (RequestHeader _ v) = v diff --git a/src/Network/HTTP/ResponseHeader.purs b/src/Network/HTTP/ResponseHeader.purs new file mode 100644 index 0000000..a80d8b8 --- /dev/null +++ b/src/Network/HTTP/ResponseHeader.purs @@ -0,0 +1,16 @@ +module Network.HTTP.ResponseHeader where + +data ResponseHeader = ResponseHeader String String + +instance eqResponseHeader :: Eq ResponseHeader where + (==) (ResponseHeader h1 v1) (ResponseHeader h2 v2) = h1 == h2 && v1 == v2 + (/=) x y = not (x == y) + +instance showResponseHeader :: Show ResponseHeader where + show (ResponseHeader h v) = "(ResponseHeader " ++ show h ++ " " ++ show v ++ ")" + +responseHeaderName :: ResponseHeader -> String +responseHeaderName (ResponseHeader h _) = h + +responseHeaderValue :: ResponseHeader -> String +responseHeaderValue (ResponseHeader _ v) = v