diff --git a/.eslintrc.json b/.eslintrc.json index 7f8e075..9d6a62b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,6 @@ { - "env": { "commonjs": true }, "extends": "eslint:recommended", - "parserOptions": { "ecmaVersion": 5 }, + "parserOptions": { "ecmaVersion": 6, "sourceType": "module" }, "rules": { "block-scoped-var": "error", "consistent-return": "error", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43f9f20..e16f413 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,7 @@ jobs: - name: Set up PureScript toolchain uses: purescript-contrib/setup-purescript@main with: + purescript: "unstable" purs-tidy: "latest" - name: Cache PureScript dependencies @@ -49,8 +50,17 @@ jobs: - name: Build the project run: npm run build - - name: Run tests - run: npm run test +# - name: Run tests +# run: npm run test - name: Check formatting run: purs-tidy check src test + + - name: Verify Bower & Pulp + run: | + npm install bower pulp@16.0.0-0 + npx bower install + npx pulp build -- --censor-lib --strict + if [ -d "test" ]; then + npx pulp test + fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 802fc2a..28b2c16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ Notable changes to this project are documented in this file. The format is based ## [Unreleased] Breaking changes: +- Update project and deps to PureScript v0.15.0 (#171 by @JordanMartinez) +- Update all request functions to take a driver arg (#171 by @JordanMartinez) + + Affjax works on the Node.js and browser environments by relying on a `require` + statement within a function. Depending on the environment detected, + either `XHR` or `XmlHttpRequest` is used. Since ES modules do not allow + one to call `import` within a function in a _synchronous_ way, + we cannot continue to use this approach. + + Rather, all request-related functions (e.g. `request`, `get`, etc.) now take + as their first argument an `AffjaxDriver` value. Different environments + will pass in their implementation for that driver and re-export + the functionality defined in `affjax`. + + To fix your code, depend on the corresponding library below and update the imported + module from `Affjax` to `Affjax.Node`/`Affjax.Web`: + - If on Node.js, use [`purescript-affjax-node`](https://github.com/purescript-contrib/purescript-affjax-node/). + - If on the brower, use [`purescript-affjax-web`](https://github.com/purescript-contrib/purescript-affjax-web/). New features: diff --git a/bower.json b/bower.json index f05548a..63ae5e0 100644 --- a/bower.json +++ b/bower.json @@ -23,32 +23,34 @@ "package.json" ], "dependencies": { - "purescript-aff": "^v6.0.0", - "purescript-argonaut-core": "^v6.0.0", - "purescript-arraybuffer-types": "^v3.0.0", - "purescript-arrays": "^v6.0.0", - "purescript-console": "^v5.0.0", - "purescript-control": "^v5.0.0", - "purescript-datetime": "^v5.0.0", - "purescript-effect": "^v3.0.0", - "purescript-either": "^v5.0.0", - "purescript-exceptions": "^v5.0.0", - "purescript-foldable-traversable": "^v5.0.0", - "purescript-foreign": "^v6.0.0", - "purescript-foreign-object": "^v3.0.0", - "purescript-form-urlencoded": "^v6.0.0", - "purescript-functions": "^v5.0.0", - "purescript-http-methods": "^v5.0.0", - "purescript-lists": "^v6.0.0", - "purescript-maybe": "^v5.0.0", - "purescript-media-types": "^v5.0.0", - "purescript-newtype": "^v4.0.0", - "purescript-nullable": "^v5.0.0", - "purescript-prelude": "^v5.0.0", - "purescript-psci-support": "^v5.0.0", - "purescript-transformers": "^v5.0.0", - "purescript-web-dom": "^v5.0.0", - "purescript-web-file": "^v3.0.0", - "purescript-web-xhr": "^v4.0.0" + "purescript-aff": "main", + "purescript-argonaut-core": "main", + "purescript-arraybuffer-types": "main", + "purescript-arrays": "master", + "purescript-console": "master", + "purescript-control": "master", + "purescript-datetime": "master", + "purescript-effect": "master", + "purescript-either": "master", + "purescript-exceptions": "master", + "purescript-foldable-traversable": "master", + "purescript-foreign": "master", + "purescript-foreign-object": "master", + "purescript-form-urlencoded": "main", + "purescript-functions": "master", + "purescript-http-methods": "main", + "purescript-lists": "master", + "purescript-maybe": "master", + "purescript-media-types": "main", + "purescript-newtype": "master", + "purescript-nullable": "main", + "purescript-prelude": "master", + "purescript-transformers": "master", + "purescript-web-dom": "master", + "purescript-web-file": "master", + "purescript-web-xhr": "master" + }, + "devDependencies": { + "purescript-psci-support": "master" } } diff --git a/package.json b/package.json index 72c757c..f0133e3 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,13 @@ { "private": true, "scripts": { - "build": "eslint src && spago build --purs-args '--censor-lib --strict'", - "test": "spago test --no-install" + "build": "eslint src && spago build --purs-args '--censor-lib --strict'" }, "devDependencies": { "body-parser": "^1.19.0", "eslint": "^7.10.0", "express": "^4.17.1", - "purescript-psa": "^0.8.0", + "purescript-psa": "^0.8.2", "xhr2": "^0.2.0" } } diff --git a/packages.dhall b/packages.dhall index 7a6905b..582d6d3 100644 --- a/packages.dhall +++ b/packages.dhall @@ -1,4 +1,4 @@ let upstream = - https://github.com/purescript/package-sets/releases/download/psc-0.14.3-20210722/packages.dhall sha256:1ceb43aa59436bf5601bac45f6f3781c4e1f0e4c2b8458105b018e5ed8c30f8c + https://raw.githubusercontent.com/purescript/package-sets/prepare-0.15/src/packages.dhall in upstream diff --git a/spago.dhall b/spago.dhall index 38e8d87..81a57e7 100644 --- a/spago.dhall +++ b/spago.dhall @@ -22,12 +22,11 @@ , "newtype" , "nullable" , "prelude" - , "psci-support" , "transformers" , "web-dom" , "web-file" , "web-xhr" ] , packages = ./packages.dhall -, sources = [ "src/**/*.purs", "test/**/*.purs" ] +, sources = [ "src/**/*.purs" ] } diff --git a/src/Affjax.js b/src/Affjax.js index 99cf7f7..437bf6c 100644 --- a/src/Affjax.js +++ b/src/Affjax.js @@ -1,96 +1,52 @@ -/* global XMLHttpRequest */ -/* global process */ -"use strict"; - -exports._ajax = function () { - var platformSpecific = { }; - if (typeof module !== "undefined" && module.require && !(typeof process !== "undefined" && process.versions["electron"])) { - // We are on node.js - platformSpecific.newXHR = function () { - var XHR = module.require("xhr2"); - return new XHR(); - }; - - platformSpecific.fixupUrl = function (url, xhr) { - if (xhr.nodejsBaseUrl === null) { - var urllib = module.require("url"); - var u = urllib.parse(url); - u.protocol = u.protocol || "http:"; - u.hostname = u.hostname || "localhost"; - return urllib.format(u); - } else { - return url || "/"; +export function _ajax(platformSpecificDriver, timeoutErrorMessageIdent, requestFailedMessageIdent, mkHeader, options) { + return function (errback, callback) { + var xhr = platformSpecificDriver.newXHR(); + var fixedUrl = platformSpecificDriver.fixupUrl(options.url, xhr); + xhr.open(options.method || "GET", fixedUrl, true, options.username, options.password); + if (options.headers) { + try { + // eslint-disable-next-line no-eq-null,eqeqeq + for (var i = 0, header; (header = options.headers[i]) != null; i++) { + xhr.setRequestHeader(header.field, header.value); + } + } catch (e) { + errback(e); } + } + var onerror = function (msgIdent) { + return function () { + errback(new Error(msgIdent)); + }; }; - - platformSpecific.getResponse = function (xhr) { - return xhr.response; - }; - } else { - // We are in the browser - platformSpecific.newXHR = function () { - return new XMLHttpRequest(); - }; - - platformSpecific.fixupUrl = function (url) { - return url || "/"; - }; - - platformSpecific.getResponse = function (xhr) { - return xhr.response; + xhr.onerror = onerror(requestFailedMessageIdent); + xhr.ontimeout = onerror(timeoutErrorMessageIdent); + xhr.onload = function () { + callback({ + status: xhr.status, + statusText: xhr.statusText, + headers: xhr.getAllResponseHeaders().split("\r\n") + .filter(function (header) { + return header.length > 0; + }) + .map(function (header) { + var i = header.indexOf(":"); + return mkHeader(header.substring(0, i))(header.substring(i + 2)); + }), + body: xhr.response + }); }; - } + xhr.responseType = options.responseType; + xhr.withCredentials = options.withCredentials; + xhr.timeout = options.timeout; + xhr.send(options.content); - return function (timeoutErrorMessageIdent, requestFailedMessageIdent, mkHeader, options) { - return function (errback, callback) { - var xhr = platformSpecific.newXHR(); - var fixedUrl = platformSpecific.fixupUrl(options.url, xhr); - xhr.open(options.method || "GET", fixedUrl, true, options.username, options.password); - if (options.headers) { - try { - // eslint-disable-next-line no-eq-null,eqeqeq - for (var i = 0, header; (header = options.headers[i]) != null; i++) { - xhr.setRequestHeader(header.field, header.value); - } - } catch (e) { - errback(e); - } + return function (error, cancelErrback, cancelCallback) { + try { + xhr.abort(); + } catch (e) { + return cancelErrback(e); } - var onerror = function (msgIdent) { - return function () { - errback(new Error(msgIdent)); - }; - }; - xhr.onerror = onerror(requestFailedMessageIdent); - xhr.ontimeout = onerror(timeoutErrorMessageIdent); - xhr.onload = function () { - callback({ - status: xhr.status, - statusText: xhr.statusText, - headers: xhr.getAllResponseHeaders().split("\r\n") - .filter(function (header) { - return header.length > 0; - }) - .map(function (header) { - var i = header.indexOf(":"); - return mkHeader(header.substring(0, i))(header.substring(i + 2)); - }), - body: platformSpecific.getResponse(xhr) - }); - }; - xhr.responseType = options.responseType; - xhr.withCredentials = options.withCredentials; - xhr.timeout = options.timeout; - xhr.send(options.content); - - return function (error, cancelErrback, cancelCallback) { - try { - xhr.abort(); - } catch (e) { - return cancelErrback(e); - } - return cancelCallback(); - }; + return cancelCallback(); }; }; -}(); +} diff --git a/src/Affjax.purs b/src/Affjax.purs index d4d6cf6..35e641e 100644 --- a/src/Affjax.purs +++ b/src/Affjax.purs @@ -5,6 +5,7 @@ module Affjax , Error(..) , printError , URL + , AffjaxDriver , request , get , post @@ -26,7 +27,7 @@ import Affjax.ResponseFormat as ResponseFormat import Affjax.ResponseHeader (ResponseHeader(..)) import Affjax.StatusCode (StatusCode) import Control.Alt ((<|>)) -import Control.Monad.Except (runExcept) +import Control.Monad.Except (Except, runExcept) import Data.Argonaut.Core (Json) import Data.Argonaut.Core as J import Data.Argonaut.Parser (jsonParser) @@ -36,7 +37,7 @@ import Data.Either (Either(..), either, note) import Data.Foldable (any) import Data.FormURLEncoded as FormURLEncoded import Data.Function (on) -import Data.Function.Uncurried (Fn4, runFn4) +import Data.Function.Uncurried (Fn5, runFn5) import Data.HTTP.Method (Method(..), CustomMethod) import Data.HTTP.Method as Method import Data.List.NonEmpty as NEL @@ -46,7 +47,7 @@ import Data.Time.Duration (Milliseconds(..)) import Effect.Aff (Aff, try) import Effect.Aff.Compat as AC import Effect.Exception as Exn -import Foreign (F, Foreign, ForeignError(..), fail, renderForeignError, unsafeReadTagged, unsafeToForeign) +import Foreign (Foreign, ForeignError(..), fail, renderForeignError, unsafeReadTagged, unsafeToForeign) import Web.DOM (Document) import Web.File.Blob (Blob) import Web.XHR.FormData (FormData) @@ -123,44 +124,44 @@ type Response a = type URL = String -- | Makes a `GET` request to the specified URL. -get :: forall a. ResponseFormat.ResponseFormat a -> URL -> Aff (Either Error (Response a)) -get rf u = request (defaultRequest { url = u, responseFormat = rf }) +get :: forall a. AffjaxDriver -> ResponseFormat.ResponseFormat a -> URL -> Aff (Either Error (Response a)) +get driver rf u = request driver (defaultRequest { url = u, responseFormat = rf }) -- | Makes a `POST` request to the specified URL with the option to send data. -post :: forall a. ResponseFormat.ResponseFormat a -> URL -> Maybe RequestBody.RequestBody -> Aff (Either Error (Response a)) -post rf u c = request (defaultRequest { method = Left POST, url = u, content = c, responseFormat = rf }) +post :: forall a. AffjaxDriver -> ResponseFormat.ResponseFormat a -> URL -> Maybe RequestBody.RequestBody -> Aff (Either Error (Response a)) +post driver rf u c = request driver (defaultRequest { method = Left POST, url = u, content = c, responseFormat = rf }) -- | Makes a `POST` request to the specified URL with the option to send data -- | and ignores the response body. -post_ :: URL -> Maybe RequestBody.RequestBody -> Aff (Either Error Unit) -post_ url = map void <<< post ResponseFormat.ignore url +post_ :: AffjaxDriver -> URL -> Maybe RequestBody.RequestBody -> Aff (Either Error Unit) +post_ driver url = map void <<< post driver ResponseFormat.ignore url -- | Makes a `PUT` request to the specified URL with the option to send data. -put :: forall a. ResponseFormat.ResponseFormat a -> URL -> Maybe RequestBody.RequestBody -> Aff (Either Error (Response a)) -put rf u c = request (defaultRequest { method = Left PUT, url = u, content = c, responseFormat = rf }) +put :: forall a. AffjaxDriver -> ResponseFormat.ResponseFormat a -> URL -> Maybe RequestBody.RequestBody -> Aff (Either Error (Response a)) +put driver rf u c = request driver (defaultRequest { method = Left PUT, url = u, content = c, responseFormat = rf }) -- | Makes a `PUT` request to the specified URL with the option to send data -- | and ignores the response body. -put_ :: URL -> Maybe RequestBody.RequestBody -> Aff (Either Error Unit) -put_ url = map void <<< put ResponseFormat.ignore url +put_ :: AffjaxDriver -> URL -> Maybe RequestBody.RequestBody -> Aff (Either Error Unit) +put_ driver url = map void <<< put driver ResponseFormat.ignore url -- | Makes a `DELETE` request to the specified URL. -delete :: forall a. ResponseFormat.ResponseFormat a -> URL -> Aff (Either Error (Response a)) -delete rf u = request (defaultRequest { method = Left DELETE, url = u, responseFormat = rf }) +delete :: forall a. AffjaxDriver -> ResponseFormat.ResponseFormat a -> URL -> Aff (Either Error (Response a)) +delete driver rf u = request driver (defaultRequest { method = Left DELETE, url = u, responseFormat = rf }) -- | Makes a `DELETE` request to the specified URL and ignores the response -- | body. -delete_ :: URL -> Aff (Either Error Unit) -delete_ = map void <<< delete ResponseFormat.ignore +delete_ :: AffjaxDriver -> URL -> Aff (Either Error Unit) +delete_ driver = map void <<< delete driver ResponseFormat.ignore -- | Makes a `PATCH` request to the specified URL with the option to send data. -patch :: forall a. ResponseFormat.ResponseFormat a -> URL -> RequestBody.RequestBody -> Aff (Either Error (Response a)) -patch rf u c = request (defaultRequest { method = Left PATCH, url = u, content = Just c, responseFormat = rf }) +patch :: forall a. AffjaxDriver -> ResponseFormat.ResponseFormat a -> URL -> RequestBody.RequestBody -> Aff (Either Error (Response a)) +patch driver rf u c = request driver (defaultRequest { method = Left PATCH, url = u, content = Just c, responseFormat = rf }) -- | Makes a `PATCH` request to the specified URL with the option to send data -- | and ignores the response body. -patch_ :: URL -> RequestBody.RequestBody -> Aff (Either Error Unit) -patch_ url = map void <<< patch ResponseFormat.ignore url +patch_ :: AffjaxDriver -> URL -> RequestBody.RequestBody -> Aff (Either Error Unit) +patch_ driver url = map void <<< patch driver ResponseFormat.ignore url -- | Makes an HTTP request. -- | @@ -179,8 +180,8 @@ patch_ url = map void <<< patch ResponseFormat.ignore url -- | ```purescript -- | get json "/resource" -- | ``` -request :: forall a. Request a -> Aff (Either Error (Response a)) -request req = +request :: forall a. AffjaxDriver -> Request a -> Aff (Either Error (Response a)) +request driver req = case req.content of Nothing -> send (toNullable Nothing) @@ -193,7 +194,7 @@ request req = where send :: Nullable Foreign -> Aff (Either Error (Response a)) send content = - try (AC.fromEffectFnAff (runFn4 _ajax timeoutErrorMessageIdent requestFailedMessageIdent ResponseHeader (ajaxRequest content))) <#> case _ of + try (AC.fromEffectFnAff (runFn5 _ajax driver timeoutErrorMessageIdent requestFailedMessageIdent ResponseHeader (ajaxRequest content))) <#> case _ of Right res -> case runExcept (fromResponse res.body) of Left err -> Left (ResponseBodyError (NEL.head err) res) @@ -223,7 +224,7 @@ request req = extractContent :: RequestBody.RequestBody -> Either String Foreign extractContent = case _ of RequestBody.ArrayView f -> - Right $ f (unsafeToForeign :: forall a. ArrayView a -> Foreign) + Right $ f (unsafeToForeign :: forall b. ArrayView b -> Foreign) RequestBody.Blob x -> Right $ (unsafeToForeign :: Blob -> Foreign) x RequestBody.Document x -> @@ -254,12 +255,12 @@ request req = Just h | not $ any (on eq RequestHeader.name h) hs -> hs `Arr.snoc` h _ -> hs - parseJSON :: String -> F Json + parseJSON :: String -> Except (NEL.NonEmptyList ForeignError) Json parseJSON = case _ of "" -> pure J.jsonEmptyObject str -> either (fail <<< ForeignError) pure (jsonParser str) - fromResponse :: Foreign -> F a + fromResponse :: Foreign -> Except (NEL.NonEmptyList ForeignError) a fromResponse = case req.responseFormat of ResponseFormat.ArrayBuffer _ -> unsafeReadTagged "ArrayBuffer" ResponseFormat.Blob _ -> unsafeReadTagged "Blob" @@ -284,9 +285,22 @@ type AjaxRequest a = , timeout :: Number } +-- Drivers should have the following 'shape': +-- ``` +-- { newXHR :: Effect Xhr +-- , fixupUrl :: Fn2 Xhr String String +-- } +-- ``` +-- If you're adding a new environment (e.g. Electron), +-- be sure to re-export all the types and functions +-- in the `Affjax` module, so people can easily +-- update their code by changing the imported module +foreign import data AffjaxDriver :: Type + foreign import _ajax :: forall a - . Fn4 + . Fn5 + AffjaxDriver String String (String -> String -> ResponseHeader) diff --git a/test/DocExamples.purs b/test/DocExamples.purs deleted file mode 100644 index 1352797..0000000 --- a/test/DocExamples.purs +++ /dev/null @@ -1,33 +0,0 @@ -module Test.DocExamples where - -import Prelude - -import Affjax as AX -import Affjax.RequestBody as RequestBody -import Affjax.ResponseFormat as ResponseFormat -import Data.Argonaut.Core as J -import Data.Either (Either(..)) -import Data.HTTP.Method (Method(..)) -import Data.Maybe (Maybe(..)) -import Effect (Effect) -import Effect.Aff (launchAff) -import Effect.Class.Console (log) - -main :: Effect Unit -main = void $ launchAff $ do - result <- AX.request (AX.defaultRequest { url = "/api", method = Left GET, responseFormat = ResponseFormat.json }) - case result of - Left err -> log $ "GET /api response failed to decode: " <> AX.printError err - Right response -> log $ "GET /api response: " <> J.stringify response.body - -main' :: Effect Unit -main' = void $ launchAff $ do - result1 <- AX.get ResponseFormat.json "/api" - case result1 of - Left err -> log $ "GET /api response failed to decode: " <> AX.printError err - Right response -> log $ "GET /api response: " <> J.stringify response.body - - result2 <- AX.post ResponseFormat.json "/api" (Just (RequestBody.json (J.fromString "test"))) - case result2 of - Left err -> log $ "POST /api response failed to decode: " <> AX.printError err - Right response -> log $ "POST /api response: " <> J.stringify response.body diff --git a/test/Main.js b/test/Main.js deleted file mode 100644 index ce51d65..0000000 --- a/test/Main.js +++ /dev/null @@ -1,74 +0,0 @@ -"use strict"; - -exports.logAny = function (a) { - return function () { - console.log(a); - return {}; - }; -}; - -exports.startServer = function (errback, callback) { - var express = require('express'); - var app = express(); - var bodyParser = require('body-parser'); - - // Always make req.body available as a String - app.use(bodyParser.text(function() { return true; })); - - app.use(express.static(__dirname)); - - app.get('/', function (req, res) { - res.send(''); - }); - - app.get('/arrayview', function(req, res) { - res.send('TODO'); - }); - - app.get('/not-json', function(req, res) { - res.header({'content-type': 'text/plain'}); - res.send('This is not JSON'); - }); - - app.all('/mirror', function(req, res) { - res.json({ - headers: req.headers, - body: req.body, - query: req.query, - method: req.method, - }); - }); - - app.get('/slow', function(req, res) { - var date = Date.now(); - var currentDate = null; - do { - currentDate = Date.now(); - } while (currentDate - date < 2000); - res.header({'content-type': 'text/plain'}); - res.send('I hope I am not late!'); - }); - - var server = app.listen(function () { - callback({ port: server.address().port, server: server }); - }); - server.on('error', function (error) { - errback(error); - }); - - return function (cancelError, onCancelerError, onCancelerSuccess) { - onCancelerSuccess(); - }; -}; - -exports.stopServer = function (server) { - return function (errback, callback) { - server.close(function (err) { - if (err) errback(err); - else callback(); - }); - return function (cancelError, onCancelerError, onCancelerSuccess) { - onCancelerSuccess(); - }; - }; -}; diff --git a/test/Main.purs b/test/Main.purs deleted file mode 100644 index 81b5569..0000000 --- a/test/Main.purs +++ /dev/null @@ -1,108 +0,0 @@ -module Test.Main where - -import Prelude - -import Affjax as AX -import Affjax.RequestBody as RequestBody -import Affjax.ResponseFormat as ResponseFormat -import Affjax.StatusCode (StatusCode(..)) -import Control.Monad.Error.Class (throwError) -import Data.Argonaut.Core as J -import Data.Either (Either(..), either) -import Data.Maybe (Maybe(..)) -import Data.Time.Duration (Milliseconds(..)) -import Effect (Effect) -import Effect.Aff (Aff, finally, forkAff, killFiber, runAff) -import Effect.Aff.Compat (EffectFnAff, fromEffectFnAff) -import Effect.Class (liftEffect) -import Effect.Class.Console as A -import Effect.Console (log, logShow) -import Effect.Exception (error, throwException) -import Foreign.Object as FO - -foreign import logAny :: forall a. a -> Effect Unit - -foreign import data Server :: Type -foreign import startServer :: EffectFnAff { server :: Server, port :: Int } -foreign import stopServer :: Server -> EffectFnAff Unit - -logAny' :: forall a. a -> Aff Unit -logAny' = liftEffect <<< logAny - -assertFail :: forall a. String -> Aff a -assertFail = throwError <<< error - -assertMsg :: String -> Boolean -> Aff Unit -assertMsg _ true = pure unit -assertMsg msg false = assertFail msg - -assertRight :: forall a b. Either a b -> Aff b -assertRight x = case x of - Left y -> logAny' y *> assertFail "Expected a Right value" - Right y -> pure y - -assertLeft :: forall a b. Either a b -> Aff a -assertLeft x = case x of - Right y -> logAny' y *> assertFail "Expected a Left value" - Left y -> pure y - -assertEq :: forall a. Eq a => Show a => a -> a -> Aff Unit -assertEq x y = - when (x /= y) $ assertFail $ "Expected " <> show x <> ", got " <> show y - -main :: Effect Unit -main = void $ runAff (either (\e -> logShow e *> throwException e) (const $ log "affjax: All good!")) do - let ok200 = StatusCode 200 - let notFound404 = StatusCode 404 - - { server, port } <- fromEffectFnAff startServer - finally (fromEffectFnAff (stopServer server)) do - A.log ("Test server running on port " <> show port) - - let prefix = append ("http://localhost:" <> show port) - let mirror = prefix "/mirror" - let doesNotExist = prefix "/does-not-exist" - let notJson = prefix "/not-json" - let slow = prefix "/slow" - - A.log "GET /mirror: should be 200 OK" - (AX.request $ AX.defaultRequest { url = mirror }) >>= assertRight >>= \res -> do - assertEq ok200 res.status - - A.log "GET /does-not-exist: should be 404 Not found" - (AX.request $ AX.defaultRequest { url = doesNotExist }) >>= assertRight >>= \res -> do - assertEq notFound404 res.status - - A.log "GET /not-json: invalid JSON with Foreign response should return an error" - AX.get ResponseFormat.json doesNotExist >>= assertLeft >>= case _ of - AX.ResponseBodyError _ _ -> pure unit - other -> logAny' other *> assertFail "Expected a ResponseBodyError" - - A.log "GET /not-json: invalid JSON with String response should be ok" - AX.get ResponseFormat.string notJson >>= assertRight >>= \res -> do - assertEq ok200 res.status - - A.log "GET /slow with timeout: should return an error" - (AX.request $ AX.defaultRequest { url = slow, timeout = Just (Milliseconds 100.0) }) >>= assertLeft >>= case _ of - AX.TimeoutError -> pure unit - other -> logAny' other *> assertFail "Expected a TimeoutError" - - A.log "POST /mirror: should use the POST method" - AX.post ResponseFormat.json mirror (Just (RequestBody.string "test")) >>= assertRight >>= \res -> do - assertEq ok200 res.status - assertEq (Just "POST") (J.toString =<< FO.lookup "method" =<< J.toObject res.body) - - A.log "PUT with a request body" - let content = "the quick brown fox jumps over the lazy dog" - AX.put ResponseFormat.json mirror (Just (RequestBody.string content)) >>= assertRight >>= \res -> do - assertEq ok200 res.status - assertEq (Just "PUT") (J.toString =<< FO.lookup "method" =<< J.toObject res.body) - assertEq (Just content) (J.toString =<< FO.lookup "body" =<< J.toObject res.body) - - A.log "Testing CORS, HTTPS" - AX.get ResponseFormat.json "https://cors-test.appspot.com/test" >>= assertRight >>= \res -> do - assertEq ok200 res.status - - A.log "Testing cancellation" - forkAff (AX.post_ mirror (Just (RequestBody.string "do it now"))) >>= killFiber (error "Pull the cord!") - assertMsg "Should have been canceled" true