{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-}
{-# OPTIONS_GHC -Wall -Werror #-}
module Trasa.Client
(
Scheme(..)
, Authority(..)
, Config(..)
, clientWith
) where
import Data.Word (Word16)
import Data.Semigroup ((<>))
import qualified Data.List.NonEmpty as NE
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Binary.Builder as LBS
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.Text.Lazy as LT hiding (singleton)
import qualified Data.Text.Lazy.Builder as LT
import qualified Data.Text.Lazy.Builder.Int as LT
import qualified Data.Map.Strict as M
import Data.CaseInsensitive (CI)
import qualified Network.HTTP.Types.URI as N
import qualified Network.HTTP.Types.Header as N
import qualified Network.HTTP.Types.Status as N
import qualified Network.HTTP.Media as N
import qualified Network.HTTP.Client as N
import Trasa.Core hiding (status,body)
data Scheme = Http | Https
schemeToSecure :: Scheme -> Bool
schemeToSecure = \case
Http -> False
Https -> True
schemeToPort :: Scheme -> Int
schemeToPort = \case
Http -> 80
Https -> 443
data Authority = Authority
{ authorityScheme :: !Scheme
, authorityHost :: !T.Text
, authorityPort :: !(Maybe Word16)
}
encodeAuthority :: T.Text -> Maybe Word16 -> BS.ByteString
encodeAuthority host port =
(TE.encodeUtf8 . LT.toStrict . LT.toLazyText)
(LT.fromText host <> maybe "" (\p -> LT.singleton ':' <> LT.decimal p) port)
encodePathBS :: [T.Text] -> BS.ByteString
encodePathBS = LBS.toStrict . LBS.toLazyByteString . (LBS.putCharUtf8 '/' <>) . N.encodePathSegmentsRelative
encodeQueryBS :: QueryString -> BS.ByteString
encodeQueryBS =
LBS.toStrict .
LBS.toLazyByteString .
N.renderQueryBuilder True .
encodeQuery
encodeAcceptBS :: NE.NonEmpty N.MediaType -> BS.ByteString
encodeAcceptBS = BS.intercalate "; " . fmap N.renderHeader . NE.toList
encodeHeaders
:: NE.NonEmpty N.MediaType
-> Maybe Content
-> M.Map (CI BS.ByteString) T.Text
-> [(CI BS.ByteString,BS.ByteString)]
encodeHeaders accepts mcontent =
M.toList .
M.insert N.hAccept (encodeAcceptBS accepts) .
maybe id (M.insert N.hContentType . N.renderHeader . contentType) mcontent .
fmap TE.encodeUtf8
data Config = Config
{ configAuthority :: !Authority
, configHeaders :: !(M.Map (CI BS.ByteString) T.Text)
, configManager :: !N.Manager
}
clientWith
:: forall route response
. (forall caps qrys req resp. route caps qrys req resp -> MetaClient caps qrys req resp)
-> Config
-> Prepared route response
-> IO (Either TrasaErr response)
clientWith toMeta config =
requestWith toMeta run
where
run :: Method -> Url -> Maybe Content -> NE.NonEmpty N.MediaType -> IO (Either TrasaErr Content)
run method (Url path query) mcontent accepts = do
response <- N.httpLbs req manager
let status = N.responseStatus response
body = N.responseBody response
return $ case status < N.status400 of
True -> case lookup N.hContentType (N.responseHeaders response) of
Nothing -> Left (TrasaErr N.status415 "No content type found")
Just bs -> case N.parseAccept bs of
Nothing -> Left (TrasaErr N.status415 "Could not decode content type")
Just typ -> Right (Content typ body)
False -> Left (TrasaErr status body)
where
Config (Authority scheme host port) headers manager = config
req = N.defaultRequest
{ N.method = TE.encodeUtf8 $ encodeMethod method
, N.secure = schemeToSecure scheme
, N.host = encodeAuthority host port
, N.port = maybe (schemeToPort scheme) fromIntegral port
, N.path = encodePathBS path
, N.queryString = encodeQueryBS query
, N.requestHeaders = encodeHeaders accepts mcontent headers
, N.requestBody = case mcontent of
Nothing -> N.RequestBodyLBS ""
Just (Content _ reqBody) -> N.RequestBodyLBS reqBody
}