Cleanup stack.yaml
[haskell/symantic-http.git] / Symantic / HTTP / Client.hs
index b84197f1f362689843945e60b170cf33474d6977..9f502122726138d165c77bb4bbf448116818b93c 100644 (file)
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveFunctor #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
 {-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE Rank2Types #-}
 {-# LANGUAGE StrictData #-}
-{-# LANGUAGE TypeFamilies #-}
 {-# OPTIONS_GHC -fno-warn-orphans #-}
-module Symantic.HTTP.Client where
+module Symantic.HTTP.Client
+ ( module Symantic.HTTP.Client
+ , module Symantic.HTTP.Client.Connection
+ ) where
 
-import Control.Applicative (Applicative(..), Alternative(..))
-import Control.Monad (Monad(..), unless)
-import Control.Monad.Trans.Class (MonadTrans(..))
-import Data.Bool
 import Data.Default.Class (Default(..))
-import Data.Either (Either(..))
-import Data.Eq (Eq(..))
-import Data.Foldable (null, for_, toList, any)
-import Data.Function (($), (.), on)
-import Data.Functor (Functor, (<$>))
-import Data.Maybe (Maybe(..), maybe)
-import Data.Ord (Ord(..))
+import Data.Function (($), (.), id)
+import Data.Functor ((<$>))
+import Data.Maybe (Maybe(..))
 import Data.Proxy (Proxy(..))
 import Data.Semigroup (Semigroup(..))
-import Data.Sequence (Seq)
-import Data.String (IsString(..))
-import Data.Text (Text)
-import Data.Tuple (fst)
-import System.IO (IO)
-import Text.Read (readMaybe)
-import Text.Show (Show(..))
-import qualified Control.Concurrent.STM as STM
-import qualified Control.Exception as Exn
-import qualified Control.Monad.Classes as MC
-import qualified Control.Monad.Trans.Except as E
-import qualified Control.Monad.Trans.Reader as R
+import GHC.Exts (IsList(..))
 import qualified Data.ByteString as BS
-import qualified Data.ByteString.Builder as BSB
-import qualified Data.ByteString.Lazy as BSL
+import qualified Data.ByteString.Base64 as BS64
 import qualified Data.List as List
+import qualified Data.List.NonEmpty as NonEmpty
 import qualified Data.Sequence as Seq
-import qualified Data.Text as T
-import qualified Data.Time.Clock as Time
+import qualified Data.Text.Encoding as Text
 import qualified Network.HTTP.Client as Client
-import qualified Network.HTTP.Media as Media
 import qualified Network.HTTP.Types as HTTP
-import qualified Network.URI as URI
+import qualified Web.HttpApiData as Web
 
-import Symantic.HTTP.Media
-import Symantic.HTTP.Mime
-import Symantic.HTTP.URI
+import Symantic.HTTP.API
+import Symantic.HTTP.MIME
+import Symantic.HTTP.Client.Connection
 
 -- * Type 'Client'
-newtype Client a = Client { unClient :: R.ReaderT ClientEnv (E.ExceptT ClientError IO) a }
- deriving (Functor, Applicative, Monad)
-type instance MC.CanDo Client (MC.EffReader ClientEnv)   = 'True
-type instance MC.CanDo Client (MC.EffExcept ClientError) = 'True
-type instance MC.CanDo Client (MC.EffExec   IO)          = 'True
-instance MC.MonadExceptN 'MC.Zero ClientError Client where
-       throwN px = Client . lift . MC.throwN px
-instance MC.MonadReaderN 'MC.Zero ClientEnv Client where
-       askN px = Client $ MC.askN px
-instance MC.MonadExecN 'MC.Zero IO Client where
-       execN _px = Client . lift . lift
-
--- | Try clients in order, last error is preserved.
-instance Alternative Client where
-       empty = MC.throw $ ClientError_EmptyClient
-       x <|> y = Client $ do
-               env <- MC.ask
-               MC.exec (runClient env x) >>= \case
-                Right xa -> return xa
-                Left _err -> unClient y
-
-client ::
- forall mt a.
- MimeUnserialize mt a =>
- (ClientRequestType mt a -> ClientRequest) -> Client a
-client req = do
-       clientRes <- doClientRequest $ req ClientRequestType
-       clientResMimeUnserialize (Proxy::Proxy mt) clientRes
-
-runClient :: ClientEnv -> Client a -> IO (Either ClientError a)
-runClient env (Client c) = E.runExceptT $ R.runReaderT c env
-
-runClientRequest :: ClientEnv -> ClientRequest -> IO (Either ClientError ClientResponse)
-runClientRequest env req = runClient env (doClientRequest req)
-
--- ** Type 'ClientRequestType'
-data ClientRequestType mt a = ClientRequestType
-
--- ** Type 'ClientEnv'
-data ClientEnv = ClientEnv
- { clientEnv_manager   :: Client.Manager
- , clientEnv_baseURI   :: URI
- , clientEnv_cookieJar :: Maybe (STM.TVar Client.CookieJar)
+-- | @'Client' a k@ is a recipe to produce a 'ClientRequest'
+-- from arguments 'a' (one per number of alternative routes).
+--
+-- 'Client' is analogous to a printf using a format customized for HTTP routing.
+newtype Client a k
+ =      Client
+ {    unClient :: (ClientModifier -> k) -> a -- Right Kan extension
  }
-clientEnv :: Client.Manager -> URI -> ClientEnv
-clientEnv clientEnv_manager clientEnv_baseURI =
-       ClientEnv
-        { clientEnv_cookieJar = Nothing
-        , ..
-        }
-
--- ** Type 'ClientError'
-data ClientError
-   -- | The server returned an error response
-   = ClientError_FailureResponse ClientResponse
-   -- | The body could not be decoded at the expected type
-   | ClientError_DecodeFailure Text ClientResponse
-   -- | The content-type of the response is not supported
-   | ClientError_UnsupportedContentType MediaType ClientResponse
-   -- | The content-type header is invalid
-   | ClientError_InvalidContentTypeHeader ClientResponse
-   -- | There was a connection error, and no response was received
-   | ClientError_ConnectionError Client.HttpException
-   -- | 'Client' is 'empty'
-   | ClientError_EmptyClient
-   deriving (Eq, Show{-, Generic, Typeable-})
-instance Exn.Exception ClientError
-instance Eq Client.HttpException where
-       (==) = (==) `on` show
-
--- * Type 'ClientRequest'
-data ClientRequest = ClientRequest
- { clientReqHttpVersion :: HTTP.HttpVersion
- , clientReqMethod      :: HTTP.Method
- , clientReqPath        :: BSB.Builder
- , clientReqQueryString :: Seq HTTP.QueryItem
- , clientReqAccept      :: Seq Media.MediaType
- , clientReqHeaders     :: Seq HTTP.Header
- , clientReqBody        :: Maybe (Client.RequestBody, Media.MediaType)
- }
-instance Default ClientRequest where
-       def = ClientRequest
-        { clientReqHttpVersion = HTTP.http11
-        , clientReqMethod      = HTTP.methodGet
-        , clientReqPath        = ""
-        , clientReqQueryString = Seq.empty
-        , clientReqAccept      = Seq.empty
-        , clientReqHeaders     = Seq.empty
-        , clientReqBody        = Nothing
-        }
-instance Show ClientRequest where
-       show _ = "ClientRequest"
-
-clientRequest :: URI -> ClientRequest -> Client.Request
-clientRequest baseURI req =
-       Client.defaultRequest
-        { Client.method         = clientReqMethod req
-        , Client.host           = maybe "" (fromString . URI.uriRegName) $ URI.uriAuthority baseURI
-        , Client.port           = case URI.uriPort <$> URI.uriAuthority baseURI of
-                                   Just (':':p) | Just port <- readMaybe p -> port
-                                   _ -> 0
-        , Client.path           = BSL.toStrict $ fromString (URI.uriPath baseURI) <> BSB.toLazyByteString (clientReqPath req)
-        , Client.queryString    = HTTP.renderQuery True . toList $ clientReqQueryString req
-        , Client.requestHeaders = acceptHeader <> contentTypeHeader <> headers
-        , Client.requestBody
-        , Client.secure         = URI.uriScheme baseURI == "https"
-        }
-       where
-       headers = List.filter (\(h, _) -> h/="Accept" && h/="Content-Type") $
-               toList $ clientReqHeaders req
-       
-       acceptHeader | null hs   = []
-                    | otherwise = [("Accept", Media.renderHeader hs)]
-               where
-               hs = toList $ clientReqAccept req
-       
-       (requestBody, contentTypeHeader) =
-               case clientReqBody req of
-                Nothing          -> (Client.RequestBodyLBS "", [])
-                Just (body, typ) -> (body, [(HTTP.hContentType, Media.renderHeader typ)])
-
-setClientRequestBodyLBS :: BSL.ByteString -> MediaType -> ClientRequest -> ClientRequest
-setClientRequestBodyLBS body mt req = req{ clientReqBody =
-       Just (Client.RequestBodyLBS body, mt) }
-
-setClientRequestBody :: Client.RequestBody -> MediaType -> ClientRequest -> ClientRequest
-setClientRequestBody body mt req = req{ clientReqBody = Just (body, mt) }
-
--- ** Type 'ClientResponse'
-type ClientResponse = ClientResponseWithBody BSL.ByteString
-data ClientResponseWithBody a = ClientResponse
- { clientResStatus      :: HTTP.Status
- , clientResHeaders     :: Seq HTTP.Header
- , clientResHttpVersion :: HTTP.HttpVersion
- , clientResBody        :: a
- } deriving (Eq, Show, Functor)
-
-clientResponse :: Client.Response a -> ClientResponseWithBody a
-clientResponse res =
-       ClientResponse
-        { clientResStatus      = Client.responseStatus res
-        , clientResBody        = Client.responseBody res
-        , clientResHeaders     = Seq.fromList $ Client.responseHeaders res
-        , clientResHttpVersion = Client.responseVersion res
-        }
-
-doClientRequest :: ClientRequest -> Client ClientResponse
-doClientRequest clientReq = do
-       ClientEnv{..} <- MC.ask
-       req <-
-               let req = clientRequest clientEnv_baseURI clientReq in
-               case clientEnv_cookieJar of
-                Nothing -> pure req
-                Just cj ->
-                       MC.exec $ do
-                               now <- Time.getCurrentTime
-                               STM.atomically $ do
-                                       oldCookieJar <- STM.readTVar cj
-                                       let (newRequest, newCookieJar) =
-                                               Client.insertCookiesIntoRequest req oldCookieJar now
-                                       STM.writeTVar cj newCookieJar
-                                       pure newRequest
-       lrRes <-
-               MC.exec $ catchConnectionError $
-               Client.httpLbs req clientEnv_manager
-       case lrRes of
-               Left err -> MC.throw err
-               Right res -> do
-                       for_ clientEnv_cookieJar $ \cj ->
-                               MC.exec $ do
-                                       now <- Time.getCurrentTime
-                                       STM.atomically $ STM.modifyTVar' cj (fst . Client.updateCookieJar res req now)
-                       let status    = HTTP.statusCode $ Client.responseStatus res
-                           clientRes = clientResponse res
-                       unless (status >= 200 && status < 300) $
-                               MC.throw $ ClientError_FailureResponse clientRes
-                       return clientRes
-
-catchConnectionError :: IO a -> IO (Either ClientError a)
-catchConnectionError action =
-       Exn.catch (Right <$> action) $ \err ->
-               return $ Left $ ClientError_ConnectionError err
-
--- ** Type 'ClientResponseStreaming'
-newtype ClientResponseStreaming = ClientResponseStreaming
- { runResponseStreaming ::
-     forall a. (ClientResponseWithBody (IO BS.ByteString) -> IO a) -> IO a }
-
-doClientRequestStreaming :: ClientRequest -> Client ClientResponseStreaming
-doClientRequestStreaming clientReq = do
-       ClientEnv{..} <- MC.ask
-       let req = clientRequest clientEnv_baseURI clientReq
-       return $ ClientResponseStreaming $ \k ->
-               Client.withResponse req clientEnv_manager $ \res -> do
-                       let status = HTTP.statusCode $ Client.responseStatus res
-                       unless (status >= 200 && status < 300) $ do
-                               responseBody <- BSL.fromChunks <$> Client.brConsume (Client.responseBody res)
-                               Exn.throw $ ClientError_FailureResponse $ clientResponse res{Client.responseBody}
-                       k $ clientResponse res
-
-clientResContentType ::
- MC.MonadExcept ClientError m =>
- ClientResponse -> m MediaType
-clientResContentType clientRes =
-       case List.lookup "Content-Type" $ toList $ clientResHeaders clientRes of
-        Nothing -> return $ "application"Media.//"octet-stream"
-        Just mt ->
-               case Media.parseAccept mt of
-                Nothing -> MC.throw $ ClientError_InvalidContentTypeHeader clientRes
-                Just mt' -> return mt'
 
-clientResMimeUnserialize ::
- MimeUnserialize mt a =>
- MC.MonadExcept ClientError m =>
- Proxy mt -> ClientResponse -> m a
-clientResMimeUnserialize mt clientRes = do
-       mtRes <- clientResContentType clientRes
-       unless (any (Media.matches mtRes) $ mediaTypes mt) $
-               MC.throw $ ClientError_UnsupportedContentType mtRes clientRes
-       case mimeUnserialize mt $ clientResBody clientRes of
-        Left  err -> MC.throw $ ClientError_DecodeFailure (T.pack err) clientRes
-        Right val -> return val
+-- | @'client' api@ returns the 'ClientRequest'
+-- builders from the given 'api'.
+client :: Client api ClientRequest -> api
+client (Client cmd) = cmd ($ def)
+
+-- ** Type 'ClientModifier'
+type ClientModifier = ClientRequest -> ClientRequest
+
+instance Cat Client where
+       Client x <.> Client y = Client $ \k ->
+               x $ \fx -> y $ \fy -> k $ fy . fx
+instance Alt Client where
+       Client x <!> Client y = Client $ \k ->
+               x k :!: y k
+       {-
+       type AltMerge Client = (:!:)
+       Client x <!> Client y = Client $ \k ->
+               x (\cm -> let n:!:_ = k cm in n) :!:
+               y (\cm -> let _:!:n = k cm in n)
+       -}
+       -- try = id -- FIXME: see what to do
+instance Pro Client where
+       dimap _a2b b2a r = Client $ \k -> unClient r k . b2a
+
+instance HTTP_Path Client where
+       type PathConstraint Client a = Web.ToHttpApiData a
+       segment s = Client $ \k -> k $ \req ->
+               req{ clientReqPath = clientReqPath req <> "/" <> Web.toEncodedUrlPiece s }
+       capture' _n = Client $ \k a -> k $ \req ->
+               req{ clientReqPath = clientReqPath req <> "/" <> Web.toEncodedUrlPiece a }
+       captureAll = Client $ \k ss -> k $ \req ->
+               req{ clientReqPath =
+                       List.foldl' (\acc s -> acc <> "/" <> Web.toEncodedUrlPiece s) "" $
+                       Web.toUrlPiece <$> ss
+                }
+instance HTTP_Header Client where
+       header n = Client $ \k v -> k $ \req ->
+               req{ clientReqHeaders = clientReqHeaders req Seq.|> (n, Web.toHeader v) }
+instance HTTP_BasicAuth Client where
+       type BasicAuthArgs Client a k = BasicAuthUser -> BasicAuthPass -> k
+       basicAuth' realm = Client $ \k user pass -> k $ \req ->
+               req{ clientReqHeaders =
+                       let user_pass = Text.encodeUtf8 $ user<>":"<>pass in
+                       clientReqHeaders req Seq.|>
+                        ( HTTP.hAuthorization
+                        , Web.toHeader $ "Basic " <> BS64.encode user_pass
+                        )
+                }
+instance HTTP_Query Client where
+       type QueryConstraint Client a = Web.ToHttpApiData a
+       queryParams' n = Client $ \k vs -> k $ \req ->
+               req{ clientReqQueryString =
+                       clientReqQueryString req <>
+                       fromList ((\v -> (n, Just $ Text.encodeUtf8 $ Web.toQueryParam v)) <$> vs) }
+instance HTTP_Version Client where
+       version v = Client $ \k -> k $ \req ->
+               req{clientReqHttpVersion = v}
+newtype ClientBodyArg a (ts::[*]) = ClientBodyArg a
+instance HTTP_Body Client where
+       type BodyArg Client = ClientBodyArg
+       type BodyConstraint Client a ts = MimeTypes ts (MimeEncodable a)
+       body' ::
+        forall a ts k repr.
+        BodyConstraint repr a ts =>
+        repr ~ Client =>
+        repr (BodyArg repr a ts -> k) k
+       body'= Client $ \k (ClientBodyArg a) -> k $ \req ->
+               req{clientReqBody =
+                       case NonEmpty.head (mimeTypes @ts) :: MimeType (MimeEncodable a) of
+                        MimeType (mt::Proxy t) ->
+                               Just
+                                ( Client.RequestBodyLBS $ mimeEncode mt a
+                                , mediaType @t )
+                }
+instance HTTP_Response Client where
+       type ResponseConstraint Client a ts = MimeTypes ts (MimeDecodable a)
+       type ResponseArgs Client a ts = Proxy ts -> Proxy a -> ClientRequest
+       type Response Client = ClientRequest
+       response ::
+        forall a ts repr.
+        ResponseConstraint repr a ts =>
+        repr ~ Client =>
+        HTTP.Method ->
+        repr (ResponseArgs repr a ts)
+             (Response repr)
+       response m = Client $ \k Proxy Proxy -> k $ \req ->
+               req
+                { clientReqMethod = m
+                , clientReqAccept =
+                       clientReqAccept req <>
+                       fromList (toList $ mediaTypes @ts @(MimeDecodable a))
+                }
+
+instance Web.ToHttpApiData BS.ByteString where
+       toUrlPiece = Web.toUrlPiece . Text.decodeUtf8
+       toHeader = id