{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveTraversable #-}
{-# OPTIONS_GHC -fno-warn-unused-binds -fno-warn-unused-imports #-}
module BitMEX.Client where
import BitMEX.Core
import BitMEX.Logging
import BitMEX.MimeTypes
import qualified Control.Exception.Safe as E
import qualified Control.Monad.IO.Class as P
import qualified Control.Monad as P
import qualified Data.Aeson.Types as A
import qualified Data.ByteString.Char8 as BC
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy.Char8 as BCL
import qualified Data.Proxy as P (Proxy(..))
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Network.HTTP.Client as NH
import qualified Network.HTTP.Client.MultipartFormData as NH
import qualified Network.HTTP.Types as NH
import qualified Web.FormUrlEncoded as WH
import qualified Web.HttpApiData as WH
import Data.Function ((&))
import Data.Monoid ((<>))
import Data.Text (Text)
import GHC.Exts (IsString(..))
dispatchLbs
:: (Produces req accept, MimeType contentType)
=> NH.Manager
-> BitMEXConfig
-> BitMEXRequest req contentType res accept
-> IO (NH.Response BCL.ByteString)
dispatchLbs manager config request = do
initReq <- _toInitRequest config request
dispatchInitUnsafe manager config initReq
data MimeResult res =
MimeResult { mimeResult :: Either MimeError res
, mimeResultResponse :: NH.Response BCL.ByteString
}
deriving (Show, Functor, Foldable, Traversable)
data MimeError =
MimeError {
mimeError :: String
, mimeErrorResponse :: NH.Response BCL.ByteString
} deriving (Eq, Show)
dispatchMime
:: forall req contentType res accept. (Produces req accept, MimeUnrender accept res, MimeType contentType)
=> NH.Manager
-> BitMEXConfig
-> BitMEXRequest req contentType res accept
-> IO (MimeResult res)
dispatchMime manager config request = do
httpResponse <- dispatchLbs manager config request
let statusCode = NH.statusCode . NH.responseStatus $ httpResponse
parsedResult <-
runConfigLogWithExceptions "Client" config $
do if (statusCode >= 400 && statusCode < 600)
then do
let s = "error statusCode: " ++ show statusCode ++ show httpResponse ++ show (NH.responseBody httpResponse)
_log "Client" levelError (T.pack s)
pure (Left (MimeError s httpResponse))
else case mimeUnrender (P.Proxy :: P.Proxy accept) (NH.responseBody httpResponse) of
Left s -> do
_log "Client" levelError (T.pack s)
pure (Left (MimeError s httpResponse))
Right r -> pure (Right r)
return (MimeResult parsedResult httpResponse)
dispatchMime'
:: (Produces req accept, MimeUnrender accept res, MimeType contentType)
=> NH.Manager
-> BitMEXConfig
-> BitMEXRequest req contentType res accept
-> IO (Either MimeError res)
dispatchMime' manager config request = do
MimeResult parsedResult _ <- dispatchMime manager config request
return parsedResult
dispatchLbsUnsafe
:: (MimeType accept, MimeType contentType)
=> NH.Manager
-> BitMEXConfig
-> BitMEXRequest req contentType res accept
-> IO (NH.Response BCL.ByteString)
dispatchLbsUnsafe manager config request = do
initReq <- _toInitRequest config request
dispatchInitUnsafe manager config initReq
dispatchInitUnsafe
:: NH.Manager
-> BitMEXConfig
-> InitRequest req contentType res accept
-> IO (NH.Response BCL.ByteString)
dispatchInitUnsafe manager config (InitRequest req) = do
runConfigLogWithExceptions src config $
do _log src levelInfo requestLogMsg
_log src levelDebug requestDbgLogMsg
res <- P.liftIO $ NH.httpLbs req manager
_log src levelInfo (responseLogMsg res)
_log src levelDebug ((T.pack . show) res)
return res
where
src = "Client"
endpoint =
T.pack $
BC.unpack $
NH.method req <> " " <> NH.host req <> NH.path req <> NH.queryString req
requestLogMsg = "REQ:" <> endpoint
requestDbgLogMsg =
"Headers=" <> (T.pack . show) (NH.requestHeaders req) <> " Body=" <>
(case NH.requestBody req of
NH.RequestBodyLBS xs -> T.decodeUtf8 (BL.toStrict xs)
_ -> "<RequestBody>")
responseStatusCode = (T.pack . show) . NH.statusCode . NH.responseStatus
responseLogMsg res =
"RES:statusCode=" <> responseStatusCode res <> " (" <> endpoint <> ")"
newtype InitRequest req contentType res accept = InitRequest
{ unInitRequest :: NH.Request
} deriving (Show)
_toInitRequest
:: (MimeType accept, MimeType contentType)
=> BitMEXConfig
-> BitMEXRequest req contentType res accept
-> IO (InitRequest req contentType res accept)
_toInitRequest config req0 = do
runConfigLogWithExceptions "Client" config $ do
parsedReq <- P.liftIO $ NH.parseRequest $ BCL.unpack $ BCL.append (configHost config) (BCL.concat (rUrlPath req0))
req1 <- P.liftIO $ _applyAuthMethods req0 config
P.when
(configValidateAuthMethods config && (not . null . rAuthTypes) req1)
(E.throw $ AuthMethodException $ "AuthMethod not configured: " <> (show . head . rAuthTypes) req1)
let req2 = req1 & _setContentTypeHeader & _setAcceptHeader
reqHeaders = ("User-Agent", WH.toHeader (configUserAgent config)) : paramsHeaders (rParams req2)
reqQuery = NH.renderQuery True (paramsQuery (rParams req2))
pReq = parsedReq { NH.method = (rMethod req2)
, NH.requestHeaders = reqHeaders
, NH.queryString = reqQuery
}
outReq <- case paramsBody (rParams req2) of
ParamBodyNone -> pure (pReq { NH.requestBody = mempty })
ParamBodyB bs -> pure (pReq { NH.requestBody = NH.RequestBodyBS bs })
ParamBodyBL bl -> pure (pReq { NH.requestBody = NH.RequestBodyLBS bl })
ParamBodyFormUrlEncoded form -> pure (pReq { NH.requestBody = NH.RequestBodyLBS (WH.urlEncodeForm form) })
ParamBodyMultipartFormData parts -> NH.formDataBody parts pReq
pure (InitRequest outReq)
modifyInitRequest :: InitRequest req contentType res accept -> (NH.Request -> NH.Request) -> InitRequest req contentType res accept
modifyInitRequest (InitRequest req) f = InitRequest (f req)
modifyInitRequestM :: Monad m => InitRequest req contentType res accept -> (NH.Request -> m NH.Request) -> m (InitRequest req contentType res accept)
modifyInitRequestM (InitRequest req) f = fmap InitRequest (f req)
runConfigLog
:: P.MonadIO m
=> BitMEXConfig -> LogExec m
runConfigLog config = configLogExecWithContext config (configLogContext config)
runConfigLogWithExceptions
:: (E.MonadCatch m, P.MonadIO m)
=> T.Text -> BitMEXConfig -> LogExec m
runConfigLogWithExceptions src config = runConfigLog config . logExceptions src