packages feed

exh-1.0.0: src/Control/Effect/Exh.hs

{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE UndecidableInstances #-}

-- | Internal module
module Control.Effect.Exh where

import Conduit
import Control.Concurrent
  ( MVar,
    newMVar,
    putMVar,
    readMVar,
    takeMVar,
  )
import Control.Effect
import Control.Effect.Bracket
import Control.Effect.Error
import Control.Effect.Reader
import Control.Monad
import Control.Monad.Trans.Cont
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Network.HTTP.Client hiding (Cookie)
import Network.HTTP.Client.MultipartFormData

data Http :: Effect where
  FormRequest :: String -> Http m Request
  GenBoundary :: Http m ByteString
  RespOpen :: Request -> Http m (Response BodyReader)
  RespClose :: Response a -> Http m ()

formRequest :: Effs '[Http, Error HttpException] m => String -> m Request
formRequest = send . FormRequest
{-# INLINE formRequest #-}

genBoundary :: Eff Http m => m ByteString
genBoundary = send GenBoundary
{-# INLINE genBoundary #-}

respOpen :: Effs '[Http, Error HttpException] m => Request -> m (Response BodyReader)
respOpen = send . RespOpen
{-# INLINE respOpen #-}

respClose :: Eff Http m => Response a -> m ()
respClose = send . RespClose
{-# INLINE respClose #-}

data HttpH

instance (Effs '[Embed IO, Reader Manager] m) => Handler HttpH Http m where
  effHandler (FormRequest s) = embed @IO $ parseRequest s
  effHandler GenBoundary = embed webkitBoundary
  effHandler (RespOpen req) = ask >>= embed . responseOpen req
  effHandler (RespClose resp) = embed $ responseClose resp
  {-# INLINEABLE effHandler #-}

type HttpToIOC = InterpretC HttpH Http

httpToIO :: (Effs '[Embed IO, Reader Manager] m, Threaders '[ReaderThreads] m p) => HttpToIOC m a -> m a
httpToIO = interpretViaHandler
{-# INLINE httpToIO #-}

--------------------------------------------------
--

data Cookie :: Effect where
  TakeCookie :: Cookie m CookieJar
  ReadCookie :: Cookie m CookieJar
  PutCookie :: CookieJar -> Cookie m ()

takeCookie :: Eff Cookie m => m CookieJar
takeCookie = send TakeCookie
{-# INLINE takeCookie #-}

readCookie :: Eff Cookie m => m CookieJar
readCookie = send ReadCookie
{-# INLINE readCookie #-}

putCookie :: Eff Cookie m => CookieJar -> m ()
putCookie = send . PutCookie
{-# INLINE putCookie #-}

data CookieH

instance Effs '[Reader (MVar CookieJar), Embed IO] m => Handler CookieH Cookie m where
  effHandler TakeCookie = ask >>= embed . takeMVar
  effHandler ReadCookie = ask >>= embed . readMVar
  effHandler (PutCookie c) = do
    ref <- ask
    embed $ putMVar ref c
  {-# INLINEABLE effHandler #-}

type CookieToReaderC = InterpretC CookieH Cookie

type CookieToIOC = CompositionC '[CookieToReaderC, ReaderC (MVar CookieJar)]

cookieToIO :: (Eff (Embed IO) m, Threaders '[ReaderThreads] m p) => CookieToIOC m a -> m a
cookieToIO m = do
  ref <- embed $ newMVar mempty
  runReader ref $
    interpretViaHandler $
      runComposition m
{-# INLINE cookieToIO #-}

--------------------------------------------------
--

data ConduitIO :: Effect where
  RunConduitIO :: ConduitT () Void IO a -> ConduitIO m a

runConduitIO :: Eff ConduitIO m => ConduitT () Void IO a -> m a
runConduitIO = send . RunConduitIO
{-# INLINE runConduitIO #-}

data ConduitIOH

instance Eff (Embed IO) m => Handler ConduitIOH ConduitIO m where
  effHandler (RunConduitIO c) = embed $ runConduit c
  {-# INLINEABLE effHandler #-}

type ConduitIOToIOC = InterpretC ConduitIOH ConduitIO

conduitIOToIO :: Eff (Embed IO) m => ConduitIOToIOC m a -> m a
conduitIOToIO = interpretViaHandler
{-# INLINE conduitIOToIO #-}

--------------------------------------------------
--

type ExhC = CompositionC '[HttpToIOC, ConduitIOToIOC, CookieToIOC]

exhToIO :: (Effs '[Embed IO, Reader Manager] m, Threaders '[ReaderThreads] m p) => ExhC m a -> m a
exhToIO m =
  cookieToIO $
    conduitIOToIO $
      httpToIO $
        runComposition m
{-# INLINE exhToIO #-}

--------------------------------------------------
--

attachFormData :: Eff Http m => [PartM m] -> Request -> m Request
attachFormData parts req = do
  boundary <- genBoundary
  formDataBodyWithBoundary boundary parts req

bodyReaderSource :: BodyReader -> ConduitT i ByteString IO ()
bodyReaderSource br = loop
  where
    loop = do
      bs <- lift br
      unless (B.null bs) $ do
        yield bs
        loop

resetCookie :: Eff Cookie m => m ()
resetCookie = takeCookie >> putCookie mempty
{-# INLINEABLE resetCookie #-}

modifyJar ::
  Effs '[Http, Cookie, Error HttpException, Bracket] m =>
  Request ->
  m ()
modifyJar req =
  bracketOnError
    takeCookie
    putCookie
    $ \jar -> do
      let req' = req {cookieJar = Just jar}
      bracket
        (respOpen req')
        respClose
        (putCookie . responseCookieJar)
{-# INLINEABLE modifyJar #-}

openWithJar ::
  Effs '[Http, Cookie, Error HttpException] m => Request -> m (Response (ConduitT i ByteString IO ()))
openWithJar req = do
  jar <- readCookie
  resp <- respOpen (req {cookieJar = Just jar})
  pure $ fmap bodyReaderSource resp

withSource ::
  Effs '[Http, Cookie, Error HttpException, Bracket] m =>
  Request ->
  ContT r m (Response (ConduitT i ByteString IO ()))
withSource req = ContT $ \k -> do
  jar <- readCookie
  bracket
    (respOpen (req {cookieJar = Just jar}))
    respClose
    (k . fmap bodyReaderSource)
{-# INLINEABLE withSource #-}