packages feed

minion-wai-extra-0.1.0.0: src/Web/Minion/Request/Multipart.hs

module Web.Minion.Request.Multipart (
  Multipart (..),
  multipartBody,
  Backend (..),
  Tmp,
  Mem,
  MultipartData (..),
  FromMultipart (..),
  MultipartM,
  getParam,
  lookupParam,
  getFile,
  lookupFile,
  Wai.File,
  Wai.Param,
) where

import Data.ByteString.Lazy qualified as Bytes.Lazy
import Data.Kind (Type)
import Network.Wai.Parse qualified as Wai

import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.Trans.Class (MonadTrans (..))
import Control.Monad.Trans.Except (Except, except, runExcept)
import Control.Monad.Trans.Reader (ReaderT (runReaderT), ask)
import Control.Monad.Trans.Resource
import Data.ByteString qualified as Bytes
import Data.String.Conversions (ConvertibleStrings (..))
import Data.Text (Text)
import Data.Text.Encoding qualified as Text.Encode
import Network.HTTP.Types qualified as Http
import Web.Minion.Args (WithReq)
import Web.Minion.Introspect qualified as I
import Web.Minion.Request (IsRequest (..))
import Web.Minion.Router

data Tmp
data Mem

newtype Multipart backend a = Multipart a

instance IsRequest (Multipart backend a) where
  type RequestValue (Multipart backend a) = a
  getRequestValue (Multipart a) = a

type MultipartM backend = ReaderT (MultipartData backend) (Except Text)

class (MonadIO m) => Backend m backend where
  type BackendFile backend :: Type
  waiBackend :: m (Wai.BackEnd (BackendFile backend))

instance (MonadResource m) => Backend m Tmp where
  type BackendFile Tmp = FilePath
  waiBackend = liftResourceT do
    Wai.tempFileBackEnd <$> getInternalState

instance (MonadIO m) => Backend m Mem where
  type BackendFile Mem = Bytes.Lazy.ByteString
  waiBackend = pure Wai.lbsBackEnd

data MultipartData backend = MultipartData
  { params :: [Wai.Param]
  , files :: [Wai.File (BackendFile backend)]
  }

class FromMultipart backend a where
  fromMultipart :: MultipartM backend a

instance FromMultipart backend (MultipartData backend) where
  fromMultipart = ask

{- | Extracts multipart data from request body

@
... /> 'multipartBody' \@'Tmp' @Foo .> ...
@
-}
multipartBody ::
  forall backend r m i ts.
  (I.Introspection i I.Request (Multipart backend r)) =>
  (MonadThrow m) =>
  (FromMultipart backend r) =>
  (Backend m backend) =>
  -- | .
  ValueCombinator i (WithReq m (Multipart backend r)) ts m
multipartBody = Request \makeError req -> do
  backend <- (waiBackend @m @backend)
  (params, files) <- liftIO $ Wai.parseRequestBody backend req
  case runExcept $ runReaderT (fromMultipart @backend @r) MultipartData{..} of
    Left e -> throwM $ makeError req Http.status400 (convertString e)
    Right v -> pure $ Multipart v

{- |
@
instance 'FromMultipart' 'Tmp' MyData where
  'fromMultipart' = do
    param1 <- 'getParam' "param1"
    param2 <- 'getParam' "param2"
    pure MyData {..}
@
-}
getParam :: Bytes.ByteString -> MultipartM backend Bytes.ByteString
getParam a =
  ask
    >>= lift
      . except
      . maybe (Left $ ("Param not found: " <>) $ Text.Encode.decodeUtf8 a) Right
      . lookup a
      . params

{- |
@
instance 'FromMultipart' 'Tmp' MyData where
  'fromMultipart' = do
    param1 <- 'getParam' "param1"
    param2 <- 'getParam' "param2"
    pure MyData {..}
@
-}
lookupParam :: Bytes.ByteString -> MultipartM backend (Maybe Bytes.ByteString)
lookupParam a =
  ask
    >>= lift
      . except
      . Right
      . lookup a
      . params

{- |
@
instance 'FromMultipart' 'Tmp' MyData where
  'fromMultipart' = do
    file1 <- 'lookupFile' "file1"
    file2 <- 'lookupFile' "file2"
    pure MyData {..}
@
-}
lookupFile :: Bytes.ByteString -> MultipartM backend (Maybe (Wai.FileInfo (BackendFile backend)))
lookupFile a =
  ask
    >>= lift
      . except
      . Right
      . lookup a
      . files

{- |
@
instance 'FromMultipart' 'Tmp' MyData where
  'fromMultipart' = do
    file1 <- 'getFile' "file1"
    file2 <- 'getFile' "file2"
    pure MyData {..}
@
-}
getFile :: Bytes.ByteString -> MultipartM backend (Wai.FileInfo (BackendFile backend))
getFile a =
  ask
    >>= lift
      . except
      . maybe (Left $ ("File not found: " <>) $ Text.Encode.decodeUtf8 a) Right
      . lookup a
      . files