packages feed

dormouse-client-0.1.0.0: src/Dormouse/Client/MonadIOImpl.hs

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE TypeFamilies #-}

module Dormouse.Client.MonadIOImpl
  ( sendHttp
  , genClientRequestFromUrlComponents
  ) where

import Control.Exception.Safe (MonadThrow, throw)
import Control.Monad.IO.Class
import Control.Monad.Reader
import Data.Function ((&))
import Data.Functor (($>))
import Data.IORef
import Data.Maybe (fromMaybe)
import qualified Data.Map.Strict as Map
import Data.Text.Encoding (encodeUtf8)
import Data.Word (Word8)
import Data.ByteString as B
import Dormouse.Client.Class
import Dormouse.Client.Exception (UnexpectedStatusCodeException(..))
import Dormouse.Client.Methods
import Dormouse.Client.Payload
import Dormouse.Client.Status
import Dormouse.Client.Types
import Dormouse.Uri
import Dormouse.Uri.Encode
import Dormouse.Url
import qualified Network.HTTP.Client as C
import qualified Network.HTTP.Types as T
import qualified Network.HTTP.Types.Status as NC
import Streamly
import qualified Streamly.Prelude as S
import qualified Streamly.External.ByteString as SEB
import qualified Streamly.Internal.Memory.ArrayStream as SIMA

givesPopper :: SerialT IO Word8 -> C.GivesPopper ()
givesPopper rawStream k = do
  let initialStream = SIMA.arraysOf 32768 rawStream
  streamState <- newIORef initialStream
  let popper = do
        stream <- readIORef streamState
        test <- S.uncons stream
        case test of
          Just (elems, stream') -> writeIORef streamState stream' $> SEB.fromArray elems
          Nothing               -> return B.empty
  k popper

translateRequestBody :: RawRequestPayload -> C.RequestBody
translateRequestBody (DefinedContentLength size stream) = C.RequestBodyStream (fromIntegral size) (givesPopper stream)
translateRequestBody (ChunkedTransfer stream)           = C.RequestBodyStreamChunked (givesPopper stream)

genClientRequestFromUrlComponents :: AnyUrl -> C.Request
genClientRequestFromUrlComponents url =
  let (scheme, comps) = case url of
        AnyUrl (HttpUrl uc)  -> (HttpScheme, uc)
        AnyUrl (HttpsUrl uc) -> (HttpsScheme, uc)
      authority = urlAuthority comps
      path = urlPath comps
      queryParams = urlQuery comps
      host = T.urlEncode False . encodeUtf8 . unHost . authorityHost $ authority
      (isSecure, port) = case scheme of
        HttpScheme -> (False, fromMaybe 80 $ authorityPort authority)
        HttpsScheme -> (True, fromMaybe 443 $  authorityPort authority)
      queryText = fromMaybe "" queryParams in
  C.defaultRequest
    { C.host = host
    , C.path = encodePath path
    , C.secure = isSecure
    , C.port = fromIntegral port
    , C.queryString = encodeQuery queryText
    }

responseStream :: C.Response C.BodyReader -> SerialT IO Word8
responseStream resp = 
    S.repeatM (C.brRead $ C.responseBody resp)
  & S.takeWhile (not . B.null)
  & S.concatMap (S.unfold SEB.read)

sendHttp :: (HasDormouseClientConfig env, MonadReader env m, MonadIO m, MonadThrow m, IsUrl url) => HttpRequest url method RawRequestPayload contentTag acceptTag -> (HttpResponse (SerialT IO Word8) -> IO (HttpResponse b)) -> m (HttpResponse b)
sendHttp HttpRequest { requestMethod = method, requestUrl = url, requestBody = reqBody, requestHeaders = reqHeaders} deserialiseResp = do
  manager <- clientManager <$> reader getDormouseClientConfig
  let initialRequest = genClientRequestFromUrlComponents $ asAnyUrl url
  let request = initialRequest { C.method = methodAsByteString method, C.requestBody = translateRequestBody reqBody, C.requestHeaders = Map.toList reqHeaders }
  response <- liftIO $ C.withResponse request manager (\resp -> do
      let respHeaders = Map.fromList $ C.responseHeaders resp
      let statusCode = NC.statusCode . C.responseStatus $ resp
      deserialiseResp $ HttpResponse 
        { responseStatusCode = statusCode
        , responseHeaders = respHeaders
        , responseBody = responseStream resp
        }
      ) 
  case responseStatusCode response of
    Successful -> return response
    _          -> throw $ UnexpectedStatusCodeException (responseStatusCode response)