packages feed

hyperbole-0.1.1: src/Effectful/Wai.hs

{-# LANGUAGE FieldSelectors #-}
{-# LANGUAGE LambdaCase #-}

module Effectful.Wai where

import Control.Monad (when)
import Data.ByteString (ByteString)
import Data.ByteString.Lazy qualified as L
import Data.Text (Text)
import Effectful
import Effectful.Dispatch.Dynamic
import Effectful.Error.Static
import Effectful.State.Static.Local
import Network.HTTP.Types (Status, status200, status500)
import Network.HTTP.Types.Header (HeaderName)
import Network.Wai as Wai
import Web.FormUrlEncoded
import Web.HttpApiData (FromHttpApiData)
import Web.View


data Wai :: Effect where
  ResHeader :: HeaderName -> ByteString -> Wai m ()
  ResBody :: ContentType -> L.ByteString -> Wai m ()
  ResStatus :: Status -> Wai m ()
  ReqBody :: Wai m L.ByteString
  Request :: Wai m Request
  Continue :: Wai m ()
  Interrupt :: Interrupt -> Wai m a


type instance DispatchOf Wai = 'Dynamic


data Handler = Handler
  { request :: Request
  , cachedRequestBody :: L.ByteString
  , status :: Status
  , headers :: [(HeaderName, ByteString)]
  , contentType :: ContentType
  , body :: L.ByteString
  }


data ContentType
  = ContentHtml
  | ContentText


formData :: (Wai :> es) => Eff es Form
formData = do
  bd <- send ReqBody
  let ef = urlDecodeForm bd
  either (send . Interrupt . ParseError) pure ef


parseFormData :: (Wai :> es, FromForm a) => Eff es a
parseFormData = do
  f <- formData
  either (send . Interrupt . ParseError) pure $ fromForm f


formParam :: (Wai :> es, FromHttpApiData a) => Text -> Eff es a
formParam k = do
  f <- formData
  either (send . Interrupt . ParseError) pure $ parseUnique k f


requestBody :: (Wai :> es) => Eff es L.ByteString
requestBody = send ReqBody


runWai
  :: (IOE :> es)
  => Request
  -> Eff (Wai : es) a
  -> Eff es (Either Interrupt Handler)
runWai req = reinterpret runLocal $ \_ -> \case
  Request -> do
    gets request
  ReqBody -> do
    cacheReqBody
    gets cachedRequestBody
  ResHeader k v -> modify
    $ \r -> r{headers = (k, v) : r.headers}
  ResStatus s -> modify
    $ \r -> r{status = s}
  ResBody ct bd -> modify
    $ \r -> r{body = bd, contentType = ct, status = status200}
  Continue -> do
    h <- get
    throwError $ RespondNow h
  Interrupt e -> do
    throwError e
 where
  runLocal =
    runErrorNoCallStack @Interrupt
      . execState @Handler (emptyResponse req)

  cacheReqBody :: forall es. (IOE :> es, State Handler :> es) => Eff es ()
  cacheReqBody = do
    r <- get
    when (L.null r.cachedRequestBody) $ do
      rb <- liftIO $ Wai.consumeRequestBodyLazy req
      put $ r{cachedRequestBody = rb}


-- Ends computation with whatever has already been set on the response
continue :: (Wai :> es) => Eff es ()
continue = send Continue


emptyResponse :: Request -> Handler
emptyResponse r = Handler r "" status500 [] ContentText "Response not set"


data Interrupt
  = NotFound
  | Redirect Url
  | ParseError Text
  | RespondNow Handler


notFound :: (Wai :> es) => Eff es a
notFound = send $ Interrupt NotFound


redirect :: (Wai :> es) => Url -> Eff es ()
redirect u = do
  send $ Interrupt $ Redirect u