packages feed

mig-0.2.1.0: src/Mig/Core/ServerFun.hs

{-| Low-level server representarion.
The server is a function from @Request@ to @Response@.

> type ServerFun m = Request -> m (Maybe Response)

To use the mig library with some server library like wai/warp we need
to provide conversion of type @ServerFun@ to the representarion of the given library.
We can convert mig server to @ServerFun@ with function @fromServer@.
The @Maybe@ type in the result encodes missing routes.
-}
module Mig.Core.ServerFun (
  ServerFun,
  sendResponse,
  withBody,
  withRawBody,
  withQuery,
  withQueryFlag,
  withOptional,
  withCapture,
  withHeader,
  withOptionalHeader,
  withCookie,
  withPathInfo,
  withFullPathInfo,
  handleServerError,
) where

import Control.Monad
import Control.Monad.Catch
import Control.Monad.IO.Class
import Data.ByteString (ByteString)
import Data.ByteString.Lazy qualified as BL
import Data.CaseInsensitive qualified as CI
import Data.Either (fromRight)
import Data.Either.Extra (eitherToMaybe)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text.Encoding qualified as Text
import Mig.Core.Class.MediaType
import Mig.Core.Types
import Network.HTTP.Types.Header (HeaderName)
import Network.HTTP.Types.Status (status500)
import Web.FormUrlEncoded (FromForm (..), urlDecodeForm)
import Web.HttpApiData

{-| Low-level representation of the server.
Missing route for a given request returns @Nothing@.
-}
type ServerFun m = Request -> m (Maybe Response)

-- | Reads request body.
withBody :: forall media a m. (MonadIO m, FromReqBody media a) => (a -> ServerFun m) -> ServerFun m
withBody f = withRawBody $ \val -> \req ->
  case fromReqBody @media val of
    Right v -> f v req
    Left err -> pure $ Just $ badRequest @Text $ "Failed to parse request body: " <> err

-- | Reads low-level request body as byte string
withRawBody :: (MonadIO m) => (BL.ByteString -> ServerFun m) -> ServerFun m
withRawBody act = \req -> do
  eBody <- liftIO req.readBody
  case eBody of
    Right body -> act body req
    Left err -> pure $ Just $ setRespStatus status500 (okResponse @Text err)

-- | Reads required query parameter
withQuery :: (Monad m, FromHttpApiData a) => Text -> (a -> ServerFun m) -> ServerFun m
withQuery name act = withQueryBy (join . getQuery name) processResponse
  where
    processResponse = handleMaybeInput errorMessage act

    errorMessage = "Failed to parse arg: " <> name

-- | Reads query flag
withQueryFlag :: Text -> (Bool -> ServerFun m) -> ServerFun m
withQueryFlag name act = \req ->
  let val =
        case getQuery name req of
          Just (Just "") -> True
          Just (Just arg) ->
            case parseQueryParam @Bool arg of
              Right flag -> flag
              Left _ -> False
          Just Nothing -> True -- we interpret empty value as True for a flag
          Nothing -> False
   in act val req

{-| The first maybe means that query with that name is missing
the second maybe is weather value is present or empty in the query
-}
getQuery :: Text -> Request -> Maybe (Maybe Text)
getQuery name req =
  case Map.lookup (Text.encodeUtf8 name) req.query of
    Just mBs ->
      case mBs of
        Just bs -> either (pure Nothing) (Just . Just) $ Text.decodeUtf8' bs
        Nothing -> Just Nothing
    Nothing -> Nothing

handleMaybeInput :: (Applicative m) => Text -> (a -> ServerFun m) -> (Maybe a -> ServerFun m)
handleMaybeInput message act = \case
  Just arg -> \req -> act arg req
  Nothing -> const $ pure $ Just $ badRequest @Text message

-- | reads optional query parameter
withOptional :: (FromHttpApiData a) => Text -> (Maybe a -> ServerFun m) -> ServerFun m
withOptional name act = withQueryBy (join . getQuery name) act

-- | Generic query parameter reader
withQueryBy ::
  (FromHttpApiData a) =>
  (Request -> Maybe Text) ->
  (Maybe a -> ServerFun m) ->
  ServerFun m
withQueryBy getVal act = \req ->
  let -- TODO: do not ignore parse failure
      mArg = either (const Nothing) Just . parseQueryParam =<< getVal req
   in act mArg req

-- | Reads capture from the path
withCapture :: (Monad m, FromHttpApiData a) => Text -> (a -> ServerFun m) -> ServerFun m
withCapture name act = withQueryBy getVal processResponse
  where
    getVal req = Map.lookup name req.capture

    processResponse = handleMaybeInput errorMessage act

    errorMessage = "Failed to parse capture: " <> name

-- | reads request header
withHeader :: (Monad m, FromHttpApiData a) => HeaderName -> (a -> ServerFun m) -> ServerFun m
withHeader name act = withQueryBy getVal processResponse
  where
    getVal req = eitherToMaybe . parseHeader =<< Map.lookup name req.headers

    processResponse = handleMaybeInput errorMessage act

    errorMessage = "Failed to parse header: " <> headerNameToText name

headerNameToText :: CI.CI ByteString -> Text
headerNameToText name = fromRight "" $ Text.decodeUtf8' $ CI.original name

-- | Reads optional request header
withOptionalHeader :: (FromHttpApiData a) => HeaderName -> (Maybe a -> ServerFun m) -> ServerFun m
withOptionalHeader name act = withQueryBy getVal act
  where
    getVal req = eitherToMaybe . parseHeader =<< Map.lookup name req.headers

withCookie :: forall a m. (FromForm a) => (Maybe a -> ServerFun m) -> ServerFun m
withCookie act = withOptionalHeader @Text "Cookie" (act . (parseCookie =<<))
  where
    parseCookie :: Text -> Maybe a
    parseCookie txt = do
      form <- eitherToMaybe $ urlDecodeForm $ BL.fromStrict $ Text.encodeUtf8 txt
      eitherToMaybe $ fromForm form

-- | Reads full path (without qury parameters)
withPathInfo :: ([Text] -> ServerFun m) -> ServerFun m
withPathInfo act = \req -> act req.path req

-- | Reads full path (without qury parameters)
withFullPathInfo :: (Text -> ServerFun m) -> ServerFun m
withFullPathInfo act = \req -> act (toFullPath req) req

-- | Runs response getter action and returns it for any input request
sendResponse :: (Functor m) => m Response -> ServerFun m
sendResponse act = const $ fmap Just act

-- | Handle errors
handleServerError :: (Exception a, MonadCatch m) => (a -> ServerFun m) -> ServerFun m -> ServerFun m
handleServerError handler act = \req ->
  (act req) `catch` (\err -> handler err req)