packages feed

webgear-server-1.5.0: src/WebGear/Server/MIMETypes.hs

-- | Parsing and rendering of MIME types
module WebGear.Server.MIMETypes (
  -- * Parsing and rendering MIME types
  BodyUnrender (..),
  BodyRender (..),

  -- * FormData utils
  inMemoryBackend,
  tempFileBackend,
) where

import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.Trans.Resource (MonadResource, getInternalState, liftResourceT)
import qualified Data.Aeson as Aeson
import Data.Bifunctor (Bifunctor (first))
import qualified Data.Binary.Builder as B
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import Data.Text (Text, pack)
import Data.Text.Conversions (FromText (..), ToText (..))
import qualified Data.Text.Encoding as Text
import qualified Data.Text.Lazy as LText
import qualified Data.Text.Lazy.Encoding as LText
import qualified Network.HTTP.Media as HTTP
import Network.Wai.Parse (BackEnd, lbsBackEnd, parseRequestBodyEx, tempFileBackEnd)
import Web.FormUrlEncoded (
  FromForm (..),
  ToForm (..),
  urlDecodeForm,
  urlEncodeFormStable,
 )
import WebGear.Core.MIMETypes (
  FormData (..),
  FormDataResult (..),
  FormURLEncoded (..),
  HTML,
  JSON,
  MIMEType (..),
  OctetStream,
  PlainText,
 )
import WebGear.Core.Request (Request (..), getRequestBody)
import WebGear.Core.Response (Response, ResponseBody (..))

{- | Instances of this class are used to parse the body of a request
as specified by a MIME type.
-}
class (MIMEType mt) => BodyUnrender m mt a where
  {- | Parse a request body. Return a 'Left' value with error messages
  in case of failure.
  -}
  bodyUnrender :: mt -> Request -> m (Either Text a)

{- | Instances of this class serializes a value to a response body as
specified by a MIME type.
-}
class (MIMEType mt) => BodyRender m mt a where
  {- | Render a value in the format specified by the media type.

  Returns the response body and the media type to be used in the
  "Content-Type" header. This could be a variant of the original
  media type with additional parameters.
  -}
  bodyRender :: mt -> Response -> a -> m (HTTP.MediaType, ResponseBody)

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

instance (MonadIO m, FromForm a) => BodyUnrender m FormURLEncoded a where
  bodyUnrender :: FormURLEncoded -> Request -> m (Either Text a)
  bodyUnrender FormURLEncoded request = do
    body <- liftIO $ getRequestBody request
    pure $ urlDecodeForm body >>= fromForm

instance (Monad m, ToForm a) => BodyRender m FormURLEncoded a where
  bodyRender :: FormURLEncoded -> Response -> a -> m (HTTP.MediaType, ResponseBody)
  bodyRender FormURLEncoded _response a = do
    let body = ResponseBodyBuilder $ B.fromLazyByteString $ urlEncodeFormStable $ toForm a
    pure (mimeType FormURLEncoded, body)

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

instance (MonadIO m) => BodyUnrender m HTML BS.ByteString where
  bodyUnrender :: HTML -> Request -> m (Either Text BS.ByteString)
  bodyUnrender _ request = do
    body <- liftIO $ getRequestBody request
    pure $ Right $ LBS.toStrict body

instance (Monad m) => BodyRender m HTML BS.ByteString where
  bodyRender :: HTML -> Response -> BS.ByteString -> m (HTTP.MediaType, ResponseBody)
  bodyRender html _response a = do
    let body = ResponseBodyBuilder $ B.fromByteString a
    pure (mimeType html, body)

instance (MonadIO m) => BodyUnrender m HTML LBS.ByteString where
  bodyUnrender :: HTML -> Request -> m (Either Text LBS.ByteString)
  bodyUnrender _ request = do
    body <- liftIO $ getRequestBody request
    pure $ Right body

instance (Monad m) => BodyRender m HTML LBS.ByteString where
  bodyRender :: HTML -> Response -> LBS.ByteString -> m (HTTP.MediaType, ResponseBody)
  bodyRender html _response a = do
    let body = ResponseBodyBuilder $ B.fromLazyByteString a
    pure (mimeType html, body)

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

instance (MonadIO m, Aeson.FromJSON a) => BodyUnrender m JSON a where
  bodyUnrender :: JSON -> Request -> m (Either Text a)
  bodyUnrender _ request = do
    s <- liftIO $ getRequestBody request
    pure $ first pack $ Aeson.eitherDecode s

instance (Monad m, Aeson.ToJSON a) => BodyRender m JSON a where
  bodyRender :: JSON -> Response -> a -> m (HTTP.MediaType, ResponseBody)
  bodyRender json _response a = do
    let body = ResponseBodyBuilder $ Aeson.fromEncoding $ Aeson.toEncoding a
    pure (mimeType json, body)

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

-- | A backend that stores all files in memory
inMemoryBackend :: BackEnd LBS.ByteString
inMemoryBackend = lbsBackEnd

-- | A backend that stores files in a temp directory.
tempFileBackend :: (MonadResource m) => m (BackEnd FilePath)
tempFileBackend = do
  st <- liftResourceT getInternalState
  pure $ tempFileBackEnd st

instance (MonadIO m) => BodyUnrender m (FormData m a) (FormDataResult a) where
  bodyUnrender :: FormData m a -> Request -> m (Either Text (FormDataResult a))
  bodyUnrender FormData{parseOptions, backendOptions} request = do
    (formDataParams, formDataFiles) <- do
      be <- backendOptions
      liftIO $ parseRequestBodyEx parseOptions be $ toWaiRequest request
    pure $ Right FormDataResult{formDataParams, formDataFiles}

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

instance (MonadIO m) => BodyUnrender m OctetStream BS.ByteString where
  bodyUnrender :: OctetStream -> Request -> m (Either Text BS.ByteString)
  bodyUnrender _ request = do
    body <- liftIO $ getRequestBody request
    pure $ Right $ LBS.toStrict body

instance (Monad m) => BodyRender m OctetStream BS.ByteString where
  bodyRender :: OctetStream -> Response -> BS.ByteString -> m (HTTP.MediaType, ResponseBody)
  bodyRender os _response a = do
    let body = ResponseBodyBuilder $ B.fromByteString a
    pure (mimeType os, body)

instance (MonadIO m) => BodyUnrender m OctetStream LBS.ByteString where
  bodyUnrender :: OctetStream -> Request -> m (Either Text LBS.ByteString)
  bodyUnrender _ request = do
    body <- liftIO $ getRequestBody request
    pure $ Right body

instance (Monad m) => BodyRender m OctetStream LBS.ByteString where
  bodyRender :: OctetStream -> Response -> LBS.ByteString -> m (HTTP.MediaType, ResponseBody)
  bodyRender os _response a = do
    let body = ResponseBodyBuilder $ B.fromLazyByteString a
    pure (mimeType os, body)

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

instance (MonadIO m, FromText a) => BodyUnrender m PlainText a where
  bodyUnrender :: PlainText -> Request -> m (Either Text a)
  bodyUnrender _ request = do
    body <- liftIO $ getRequestBody request
    pure $ case LText.decodeUtf8' body of
      Left e -> Left $ pack $ show e
      Right t -> Right $ fromText $ LText.toStrict t

instance (Monad m, ToText a) => BodyRender m PlainText a where
  bodyRender :: PlainText -> Response -> a -> m (HTTP.MediaType, ResponseBody)
  bodyRender txt _response a = do
    let body = ResponseBodyBuilder $ Text.encodeUtf8Builder $ toText a
    pure (mimeType txt, body)