diff --git a/.gitignore b/.gitignore index c6745e0..dbd523a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ /src/.webpack.js /setupPath.sh /.idea +.psc-ide-port +.vscode/ \ No newline at end of file diff --git a/bower.json b/bower.json index 9574d11..ef44c47 100644 --- a/bower.json +++ b/bower.json @@ -21,18 +21,19 @@ "output" ], "dependencies": { - "purescript-console": "^3.0.0", - "purescript-prelude": "^3.1.0", - "purescript-either": "^3.1.0", - "purescript-argonaut-core": "^3.1.0", - "purescript-globals": "^3.0.0", - "purescript-foldable-traversable": "^3.6.1", - "purescript-nullable": "^3.0.0", - "purescript-dom": "^4.9.0", - "purescript-affjax": "^5.0.0", - "purescript-argonaut-generic-codecs": "^6.0.4" + "purescript-console": "^4.1.0", + "purescript-prelude": "^4.0.1", + "purescript-either": "^4.0.0", + "purescript-foldable-traversable": "^4.0.0", + "purescript-generics-rep": "^6.0.0", + "purescript-effect": "^2.0.0", + "purescript-aff": "^5.0.0", + "purescript-exceptions": "^4.0.0", + "purescript-web-xhr": "^2.0.0", + "purescript-argonaut-generic": "https://github.com/CarstenKoenig/purescript-argonaut-generic.git#6402a87c8a35b4028429d270ca23af4eefd7e192", + "purescript-affjax": "^6.0.0" }, "devDependencies": { - "purescript-psci-support": "^3.0.0" + "purescript-psci-support": "^4.0.0" } -} +} \ No newline at end of file diff --git a/src/Servant/PureScript/Affjax.js b/src/Servant/PureScript/Affjax.js deleted file mode 100644 index 8446e1c..0000000 --- a/src/Servant/PureScript/Affjax.js +++ /dev/null @@ -1,96 +0,0 @@ -/* This file is copied from https://raw.githubusercontent.com/slamdata/purescript-affjax/master/src/Network/HTTP/Affjax.js - * contained in purescript-affjax by slamdata. Therefore this file is licensed under the Apache License 2.0. - * at the time of reading it will most likely be heavily modified already - so if something does not work right, I am the one to blame ;-) - */ -/* global exports */ -/* global XMLHttpRequest */ -/* global module */ -"use strict"; - -// module Servant.PureScript.Affjax - -// jshint maxparams: 5 -exports._ajax = function (mkHeader, options, canceler, errback, callback) { - var platformSpecific = { }; - if (typeof module !== "undefined" && module.require) { - // We are on node.js - platformSpecific.newXHR = function () { - var XHR = module.require("xhr2"); - return new XHR(); - }; - - platformSpecific.fixupUrl = function (url) { - var urllib = module.require("url"); - var u = urllib.parse(url); - u.protocol = u.protocol || "http:"; - u.hostname = u.hostname || "localhost"; - return urllib.format(u); - }; - - 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; - }; - } - - return function () { - var xhr = platformSpecific.newXHR(); - var fixedUrl = platformSpecific.fixupUrl(options.url); - xhr.open(options.method || "GET", fixedUrl, true, options.username, options.password); - if (options.headers) { - try { - for (var i = 0, header; (header = options.headers[i]) != null; i++) { - xhr.setRequestHeader(header.field, header.value); - } - } - catch (e) { - errback(e)(); - } - } - xhr.onerror = function () { - errback(new Error("AJAX request failed: " + options.method + " " + options.url))(); - }; - xhr.onload = function () { - callback({ - status: xhr.status, - headers: xhr.getAllResponseHeaders().split("\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)); - }), - response: platformSpecific.getResponse(xhr) - })(); - }; - xhr.responseType = options.responseType; - xhr.withCredentials = options.withCredentials; - xhr.send(options.content); - return canceler(xhr); - }; -}; - -// jshint maxparams: 4 -exports._cancelAjax = function (xhr, cancelError, errback, callback) { - return function () { - try { xhr.abort(); } catch (e) { return callback(false)(); } - return callback(true)(); - }; -}; - -exports.unsafeToString = function (obj) { - return JSON.stringify(obj, null, 4) -} diff --git a/src/Servant/PureScript/Affjax.purs b/src/Servant/PureScript/Affjax.purs deleted file mode 100644 index 0ee21dd..0000000 --- a/src/Servant/PureScript/Affjax.purs +++ /dev/null @@ -1,123 +0,0 @@ -{-- - This file contains code copied from the purescript-affjax project from slamdata. - It is therefore licensed under Apache License version 2.0. ---} - -module Servant.PureScript.Affjax where - -import Prelude - -import Control.Monad.Aff (Aff, Canceler(Canceler), makeAff) -import Control.Monad.Aff.Class (liftAff, class MonadAff) -import Control.Monad.Eff (Eff) -import Control.Monad.Eff.Exception (message, Error) -import Control.Monad.Error.Class (throwError, catchError, class MonadError) -import DOM.XHR.Types (XMLHttpRequest) -import Data.Either (Either(Left, Right)) -import Data.Function.Uncurried (Fn5, runFn5, Fn4, runFn4) -import Data.Maybe (Maybe(..)) -import Data.Monoid (mempty) -import Data.Nullable (Nullable, toNullable) -import Network.HTTP.Affjax (AffjaxResponse, AJAX) -import Network.HTTP.ResponseHeader (ResponseHeader, responseHeader) - -newtype AjaxError = AjaxError - { request :: AjaxRequest - , description :: ErrorDescription - } - -data ErrorDescription = UnexpectedHTTPStatus (AffjaxResponse String) - | ParsingError String - | DecodingError String - | ConnectionError String - -type AjaxRequest = - { method :: String - , url :: String - , headers :: Array { field :: String, value :: String } - , content :: Nullable String - , responseType :: String - , username :: Nullable String - , password :: Nullable String - , withCredentials :: Boolean - } - -makeAjaxError :: AjaxRequest -> ErrorDescription -> AjaxError -makeAjaxError req desc = AjaxError { request : req - , description : desc - } - -runAjaxError :: AjaxError -> { request :: AjaxRequest, description :: ErrorDescription} -runAjaxError (AjaxError err) = err - -errorToString :: AjaxError -> String -errorToString = unsafeToString - -requestToString :: AjaxRequest -> String -requestToString = unsafeToString - -responseToString :: AffjaxResponse String -> String -responseToString = unsafeToString - -defaultRequest :: AjaxRequest -defaultRequest = { - method : "GET" - , url : "" - , headers : [ {field : "Accept", value : "application/json"} - , {field : "content-type", value : "application/json"}] - , content : toNullable (Nothing :: Maybe String) - , responseType : "text" - , username : toNullable (Nothing :: Maybe String) - , password : toNullable (Nothing :: Maybe String) - , withCredentials : false - } - - --- | Do an affjax call but report Aff exceptions in our own MonadError -affjax - :: forall eff m - . MonadError AjaxError m - => MonadAff (ajax :: AJAX | eff) m - => AjaxRequest - -> m (AffjaxResponse String) -affjax req = toAjaxError <=< liftAff <<< toEither $ makeAff \cb -> ajax req (cb <<< Left) (cb <<< Right) - where - toEither :: forall a. Aff (ajax :: AJAX | eff) a -> Aff (ajax :: AJAX | eff) (Either String a) - toEither action = catchError (Right <$> action) $ \e -> - pure $ Left (message e) - - toAjaxError :: forall a. Either String a -> m a - toAjaxError r = case r of - Left err -> throwError $ AjaxError - { request : req - , description : ConnectionError err - } - Right v -> pure v - -ajax :: forall e. - AjaxRequest - -> (Error -> Eff (ajax :: AJAX | e) Unit) - -> (AffjaxResponse String -> Eff (ajax :: AJAX | e) Unit) - -> Eff (ajax :: AJAX | e) (Canceler (ajax :: AJAX | e)) -ajax req eb cb = runFn5 _ajax responseHeader req cancelAjax eb cb - - -foreign import _ajax - :: forall e. Fn5 (String -> String -> ResponseHeader) - AjaxRequest - (XMLHttpRequest -> Canceler (ajax :: AJAX | e)) - (Error -> Eff (ajax :: AJAX | e) Unit) - (AffjaxResponse String -> Eff (ajax :: AJAX | e) Unit) - (Eff (ajax :: AJAX | e) (Canceler (ajax :: AJAX | e))) - -cancelAjax :: forall e. XMLHttpRequest -> Canceler (ajax :: AJAX | e) -cancelAjax xhr = Canceler \err -> makeAff (\cb -> mempty <$ runFn4 _cancelAjax xhr err (cb <<< Left) (const <<< cb $ Right unit)) - -foreign import _cancelAjax - :: forall e. Fn4 XMLHttpRequest - Error - (Error -> Eff (ajax :: AJAX | e) Unit) - (Boolean -> Eff (ajax :: AJAX | e) Unit) - (Eff (ajax :: AJAX | e) Unit) - -foreign import unsafeToString :: forall obj. obj -> String diff --git a/src/Servant/PureScript/Ajax.purs b/src/Servant/PureScript/Ajax.purs new file mode 100644 index 0000000..f78abcd --- /dev/null +++ b/src/Servant/PureScript/Ajax.purs @@ -0,0 +1,84 @@ +{-- + This file contains code copied from the purescript-affjax project from slamdata. + It is therefore licensed under Apache License version 2.0. +--} + +module Servant.PureScript.Ajax where + +import Prelude + +import Control.Monad.Error.Class (class MonadError, catchError, throwError) +import Data.Argonaut.Decode.Generic.Rep (class DecodeRep, genericDecodeJson) +import Data.Either (Either(..)) +import Data.Generic.Rep (class Generic) +import Effect.Aff (Aff, message) +import Effect.Aff.Class (class MonadAff, liftAff) +import Network.HTTP.Affjax (AffjaxRequest, AffjaxResponse, affjax) +import Network.HTTP.Affjax.Response as Response +import Servant.PureScript.JsUtils (unsafeToString) + + +newtype AjaxError res + = AjaxError + { request :: AffjaxRequest + , description :: ErrorDescription res + } + +data ErrorDescription res + = UnexpectedHTTPStatus (AffjaxResponse res) + | ParsingError String + | DecodingError String + | ConnectionError String + + +makeAjaxError :: forall res. AffjaxRequest -> ErrorDescription res -> AjaxError res +makeAjaxError req desc = + AjaxError + { request : req + , description : desc + } + +runAjaxError :: forall res. AjaxError res -> { request :: AffjaxRequest, description :: ErrorDescription res } +runAjaxError (AjaxError err) = err + +errorToString :: forall res. AjaxError res -> String +errorToString = unsafeToString + +requestToString :: AffjaxRequest -> String +requestToString = unsafeToString + +responseToString :: forall res. AffjaxResponse res -> String +responseToString = unsafeToString + + +-- | Do an affjax call but report Aff exceptions in our own MonadError +ajax :: forall m res rep. Generic res rep => DecodeRep rep => MonadError (AjaxError res) m => MonadAff m + => AffjaxRequest -> m (AffjaxResponse res) +ajax req = do + jsonResponse <- liftWithError $ affjax Response.json req + decoded <- toDecodingError $ genericDecodeJson jsonResponse.response + pure + { status: jsonResponse.status + , statusText: jsonResponse.statusText + , headers: jsonResponse.headers + , response: decoded + } + where + liftWithError :: forall a. Aff a -> m a + liftWithError action = do + res <- liftAff $ toEither action + toAjaxError res + + toEither :: forall a. Aff a -> Aff (Either String a) + toEither action = catchError (Right <$> action) $ \e -> + pure $ Left (message e) + + toAjaxError :: forall a. Either String a -> m a + toAjaxError r = case r of + Left err -> throwError $ makeAjaxError req $ ConnectionError err + Right v -> pure v + + toDecodingError :: forall a. Either String a -> m a + toDecodingError r = case r of + Left err -> throwError $ makeAjaxError req $ DecodingError err + Right v -> pure v diff --git a/src/Servant/PureScript/JsUtils.js b/src/Servant/PureScript/JsUtils.js new file mode 100644 index 0000000..9e75ccf --- /dev/null +++ b/src/Servant/PureScript/JsUtils.js @@ -0,0 +1,10 @@ +/* JsUtils exports */ +"use strict"; + +// module JsUtils + +exports.encodeUriComponent = encodeURIComponent; + +exports.unsafeToString = function (obj) { + return JSON.stringify(obj, null, 4) +} \ No newline at end of file diff --git a/src/Servant/PureScript/JsUtils.purs b/src/Servant/PureScript/JsUtils.purs new file mode 100644 index 0000000..0f72742 --- /dev/null +++ b/src/Servant/PureScript/JsUtils.purs @@ -0,0 +1,8 @@ +-- | This module defines types for some global Javascript functions +-- | and values. +module Servant.PureScript.JsUtils where + +-- | uri component encoding +foreign import encodeUriComponent :: String -> String + +foreign import unsafeToString :: forall obj. obj -> String \ No newline at end of file diff --git a/src/Servant/PureScript/Settings.purs b/src/Servant/PureScript/Settings.purs index 9928cc1..3aacaf7 100644 --- a/src/Servant/PureScript/Settings.purs +++ b/src/Servant/PureScript/Settings.purs @@ -5,21 +5,21 @@ module Servant.PureScript.Settings where -import Prelude -import Data.Argonaut.Generic.Aeson as Aeson -import Data.Argonaut.Core (Json) +import Data.Argonaut.Core (Json, stringify) +import Data.Argonaut.Decode.Generic.Rep (class DecodeRep, genericDecodeJson) +import Data.Argonaut.Encode.Generic.Rep (class EncodeRep, genericEncodeJson) import Data.Either (Either) -import Data.Generic (class Generic, GenericSpine(SString), toSpine) -import Global (encodeURIComponent) - +import Data.Generic.Rep (class Generic) +import Prelude (identity, (<<<)) +import Servant.PureScript.JsUtils (encodeUriComponent) -- encodeJson, decodeJson, toURLPiece have to be wrapped in newtype. See: -- https://github.com/purescript/purescript/issues/1957 -newtype SPSettingsEncodeJson_ = SPSettingsEncodeJson_ (forall a. Generic a => a -> Json) -newtype SPSettingsDecodeJson_ = SPSettingsDecodeJson_ (forall a. Generic a => Json -> Either String a) -newtype SPSettingsToUrlPiece_ = SPSettingsToUrlPiece_ (forall a. Generic a => a -> URLPiece) -newtype SPSettingsEncodeHeader_ = SPSettingsEncodeHeader_ (forall a. Generic a => a -> URLPiece) +newtype SPSettingsEncodeJson_ = SPSettingsEncodeJson_ (forall a rep. Generic a rep => EncodeRep rep => a -> Json) +newtype SPSettingsDecodeJson_ = SPSettingsDecodeJson_ (forall a rep. Generic a rep => DecodeRep rep => Json -> Either String a) +newtype SPSettingsToUrlPiece_ = SPSettingsToUrlPiece_ (forall a. ToUrlPiece a => a -> URLPiece) +newtype SPSettingsEncodeHeader_ = SPSettingsEncodeHeader_ (forall a. ToUrlPiece a => a -> URLPiece) newtype SPSettings_ params = SPSettings_ { encodeJson :: SPSettingsEncodeJson_ @@ -31,26 +31,33 @@ newtype SPSettings_ params = SPSettings_ { type URLPiece = String +class ToUrlPiece a where + toUrlPiece :: a -> URLPiece + +instance stringToUrlPiece :: ToUrlPiece String where + toUrlPiece = identity + +else instance genericRepToUrlPiece :: (Generic a rep, EncodeRep rep) => ToUrlPiece a where + toUrlPiece = stringify <<< genericEncodeJson + -- | Just use the robust JSON format. -gDefaultToURLPiece :: forall a. Generic a => a -> URLPiece +gDefaultToURLPiece :: forall a. ToUrlPiece a => a -> URLPiece gDefaultToURLPiece = gDefaultEncodeHeader -- | Just use the robust JSON format. -gDefaultEncodeHeader :: forall a. Generic a => a -> URLPiece -gDefaultEncodeHeader v = - case toSpine v of - SString s -> s -- Special case string - just use it as is (http-api-data compatibility). - _ -> show <<< Aeson.encodeJson $ v +gDefaultEncodeHeader :: forall a. ToUrlPiece a => a -> URLPiece +gDefaultEncodeHeader = toUrlPiece + -- | Full encoding based on gDefaultToURLPiece -gDefaultEncodeURLPiece :: forall a. Generic a => a -> URLPiece -gDefaultEncodeURLPiece = encodeURIComponent <<< gDefaultToURLPiece +gDefaultEncodeURLPiece :: forall a. ToUrlPiece a => a -> URLPiece +gDefaultEncodeURLPiece = encodeUriComponent <<< gDefaultToURLPiece defaultSettings :: forall params. params -> SPSettings_ params defaultSettings params = SPSettings_ { - encodeJson : SPSettingsEncodeJson_ Aeson.encodeJson - , decodeJson : SPSettingsDecodeJson_ Aeson.decodeJson + encodeJson : SPSettingsEncodeJson_ genericEncodeJson + , decodeJson : SPSettingsDecodeJson_ genericDecodeJson , toURLPiece : SPSettingsToUrlPiece_ gDefaultToURLPiece , encodeHeader : SPSettingsEncodeHeader_ gDefaultEncodeHeader , params : params diff --git a/src/Servant/PureScript/Util.purs b/src/Servant/PureScript/Util.purs index d20f038..8c6cd94 100644 --- a/src/Servant/PureScript/Util.purs +++ b/src/Servant/PureScript/Util.purs @@ -1,56 +1,30 @@ module Servant.PureScript.Util where import Prelude -import Control.Monad.Error.Class (class MonadError, throwError) -import Data.Argonaut.Core (Json) -import Data.Argonaut.Parser (jsonParser) -import Data.Bifunctor (lmap) -import Data.Either (Either(..)) + +import Data.Argonaut.Encode.Generic.Rep (class EncodeRep) import Data.Foldable (intercalate) -import Data.Generic (class Generic) -import Network.HTTP.Affjax (AffjaxResponse) -import Network.HTTP.StatusCode (StatusCode(..)) -import Servant.PureScript.Affjax (makeAjaxError, AjaxError, ErrorDescription(DecodingError, ParsingError, UnexpectedHTTPStatus), AjaxRequest) +import Data.Generic.Rep (class Generic) +import Network.HTTP.Affjax (AffjaxRequest) +import Servant.PureScript.Ajax (AjaxError, ErrorDescription, makeAjaxError) import Servant.PureScript.Settings (SPSettings_(SPSettings_), SPSettingsToUrlPiece_(SPSettingsToUrlPiece_), SPSettingsEncodeHeader_(SPSettingsEncodeHeader_)) --- | Get the result of a request. --- | --- | Reports an error if status code is non success or decoding fails. The taken AjaxRequest is only for error reporting. -getResult - :: forall a m - . Generic a - => MonadError AjaxError m - => AjaxRequest - -> (Json -> Either String a) - -> AffjaxResponse String -> m a -getResult req' decode resp = do - let stCode = case resp.status of StatusCode code -> code - fVal <- if stCode >= 200 && stCode < 300 - then pure resp.response - else throwError $ makeAjaxError req' (UnexpectedHTTPStatus resp) - jVal <- throwLeft <<< lmap (reportRequestError req' ParsingError fVal) <<< jsonParser $ fVal - throwLeft <<< lmap (reportRequestError req' DecodingError (show jVal)) <<< decode $ jVal - -throwLeft :: forall a e m. MonadError e m => Either e a -> m a -throwLeft (Left e) = throwError e -throwLeft (Right a) = pure a - - -encodeListQuery :: forall a b. Generic a => SPSettings_ b -> String -> Array a -> String + +encodeListQuery :: forall a b rep. Generic a rep => EncodeRep rep => SPSettings_ b -> String -> Array a -> String encodeListQuery opts'@(SPSettings_ opts) fName = intercalate "&" <<< map (encodeQueryItem opts' fName) -- | The given name is assumed to be already escaped. -encodeQueryItem :: forall a b. Generic a => SPSettings_ b -> String -> a -> String +encodeQueryItem :: forall a b rep. Generic a rep => EncodeRep rep => SPSettings_ b -> String -> a -> String encodeQueryItem opts'@(SPSettings_ opts) fName val = fName <> "=" <> encodeURLPiece opts' val -- | Call opts.toURLPiece and encode the resulting string with encodeURIComponent. -encodeURLPiece :: forall a params. Generic a => SPSettings_ params -> a -> String +encodeURLPiece :: forall a rep params. Generic a rep => EncodeRep rep => SPSettings_ params -> a -> String encodeURLPiece (SPSettings_ opts) = case opts.toURLPiece of SPSettingsToUrlPiece_ f -> f -encodeHeader :: forall a params. Generic a => SPSettings_ params -> a -> String +encodeHeader :: forall a rep params. Generic a rep => EncodeRep rep => SPSettings_ params -> a -> String encodeHeader (SPSettings_ opts) = case opts.encodeHeader of SPSettingsEncodeHeader_ f -> f -reportRequestError :: AjaxRequest -> (String -> ErrorDescription) -> String -> String -> AjaxError +reportRequestError :: forall res. AffjaxRequest -> (String -> ErrorDescription res) -> String -> String -> AjaxError res reportRequestError req' err source msg = makeAjaxError req' $ reportError err source msg reportError :: forall err. (String -> err) -> String -> String -> err