packages feed

eved-0.0.2.1: src/Web/Eved/Server.hs

{-# LANGUAGE DeriveFunctor         #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE LambdaCase            #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE RankNTypes            #-}
{-# LANGUAGE TupleSections         #-}
{-# LANGUAGE TypeApplications      #-}

module Web.Eved.Server
    where

import           Control.Applicative  ((<*))
import           Control.Exception    (Exception, SomeException (..), catch,
                                       handle, throwIO, try)
import           Control.Monad
import qualified Data.ByteString.Lazy as LBS
import qualified Data.List            as List
import qualified Data.List.NonEmpty   as NE
import           Data.Maybe           (catMaybes, fromMaybe)
import           Data.Text            (Text)
import           Data.Text.Encoding   (encodeUtf8)
import           Network.HTTP.Media   (renderHeader)
import           Network.HTTP.Types   (Header, Status, badRequest400, hAccept,
                                       hContentType, internalServerError500,
                                       methodNotAllowed405, notAcceptable406,
                                       notFound404, queryToQueryText,
                                       renderStdMethod, unsupportedMediaType415)

import qualified Data.CaseInsensitive as CI
import qualified Web.Eved.ContentType as CT
import qualified Web.Eved.Header      as H
import           Web.Eved.Internal
import qualified Web.Eved.QueryParam  as QP
import qualified Web.Eved.UrlElement  as UE


import           Network.Wai          (Application, Response, lazyRequestBody,
                                       pathInfo, queryString, requestHeaders,
                                       requestMethod, responseLBS)

data RequestData a
  = BodyRequestData (LBS.ByteString -> Either Text a)
  | PureRequestData a
  deriving Functor

newtype EvedServerT m a = EvedServerT
    { unEvedServerT :: (forall a. m a -> IO a) -> [Text] -> RequestData a -> Application }

server :: (forall a. m a -> IO a) -> a -> EvedServerT m a -> Application
server nt handlers api req resp =
    unEvedServerT api nt (List.dropWhileEnd (== "") (pathInfo req)) (PureRequestData handlers) req resp
        `catch` (\case
            PathError -> resp $ responseLBS notFound404 [] "Not Found"
            CaptureError err -> resp $ responseLBS badRequest400 [] (LBS.fromStrict $ encodeUtf8 err)
            QueryParamParseError err -> resp $ responseLBS badRequest400 [] (LBS.fromStrict $ encodeUtf8 err)
            NoContentMatchError -> resp $ responseLBS unsupportedMediaType415 [] "Unsupported Media Type"
            NoAcceptMatchError -> resp $ responseLBS notAcceptable406 [] "Not Acceptable"
            NoMethodMatchError -> resp $ responseLBS methodNotAllowed405 [] "Method Not Allowed")
        `catch` (resp . serverErrorToResponse)
        `catch` (\(UserApplicationError err) -> resp $ serverErrorToResponse $ defaultErrorHandler err)

data RoutingError
    = PathError
    | CaptureError Text
    | HeaderParseError Text
    | QueryParamParseError Text
    | NoContentMatchError
    | NoAcceptMatchError
    | NoMethodMatchError
    deriving (Show, Eq, Ord)

newtype UserApplicationError a = UserApplicationError a
    deriving Show

instance Exception RoutingError
instance Exception a => Exception (UserApplicationError a)

instance Eved (EvedServerT m) m where
    a .<|> b = EvedServerT $ \nt path requestData req resp -> do
        let applicationA = unEvedServerT a nt path (fmap (\(l :<|> _) -> l) requestData)
            applicationB = unEvedServerT b nt path (fmap (\(_ :<|> r) -> r) requestData)
        eApplicationAResult <- try @RoutingError $ applicationA req resp
        case eApplicationAResult of
          Right a -> pure a
          Left errA -> do
            eApplicationBResult <- try @RoutingError $ applicationB req resp
            case eApplicationBResult of
              Right b   -> pure b
              Left errB -> if errA > errB then throwIO errA else throwIO errB

    lit s next = EvedServerT $ \nt path action ->
        case path of
            x:rest | x == s -> unEvedServerT next nt rest action
            _               -> \_ _ -> throwIO PathError

    capture _s el next = EvedServerT $ \nt path action ->
        case path of
          x:rest ->
              case UE.fromUrlPiece el x of
                Right arg ->
                    unEvedServerT next nt rest $ fmap ($ arg) action
                Left err -> \_ _ -> throwIO $ CaptureError err
          _ -> \_ _ -> throwIO PathError

    reqBody ctypes next = EvedServerT $ \nt path action req -> do
        let mContentTypeBS = lookup hContentType $ requestHeaders req
        case mContentTypeBS of
              Just contentTypeBS ->
                case CT.chooseContentCType ctypes mempty contentTypeBS of
                      Just bodyParser ->
                          unEvedServerT next nt path (addBodyParser bodyParser action) req
                      Nothing ->
                            \_ -> throwIO NoContentMatchError
              Nothing ->
                  unEvedServerT next nt path (addBodyParser (CT.fromContentType (NE.head ctypes) . (mempty,)) action) req

       where
            addBodyParser bodyParser action =
                case action of
                  BodyRequestData fn -> BodyRequestData $ \bodyText -> fn bodyText <*> bodyParser bodyText
                  PureRequestData v -> BodyRequestData (fmap v . bodyParser)

    queryParam s qp next = EvedServerT $ \nt path action req ->
       let queryText = queryToQueryText (queryString req)
           params = fromMaybe "true" . snd <$> filter (\(k, _) -> k == s) queryText
       in case QP.fromQueryParam qp params of
            Right a  -> unEvedServerT next nt path (fmap ($a) action) req
            Left err -> \_ -> throwIO $ QueryParamParseError err

    header headerName h next = EvedServerT $ \nt path action req ->
       let ciHeaderName = CI.mk (encodeUtf8 headerName)
           mHeader = lookup ciHeaderName (requestHeaders req)
       in case H.fromHeaderValue h mHeader of
          Right a  -> unEvedServerT next nt path (fmap ($ a) action) req
          Left err -> \_ -> throwIO $ HeaderParseError err

    verb method status ctypes = EvedServerT $ \nt path action req resp -> do
        unless (null path) $
            throwIO PathError

        unless (renderStdMethod method == requestMethod req) $
            throwIO NoMethodMatchError

        (ctype, renderContent) <-
                case lookup hAccept $ requestHeaders req of
                      Just acceptBS ->
                        case CT.chooseAcceptCType ctypes acceptBS of
                          Just (ctype, renderContent) -> pure (ctype, renderContent)
                          Nothing -> throwIO NoAcceptMatchError
                      Nothing ->
                          let ctype = NE.head ctypes
                          in pure (NE.head $ CT.mediaTypes ctype, CT.toContentType ctype)

        (rHeaders, rBody) <- renderContent <$>
            case action of
                BodyRequestData fn -> do
                    reqBody <- lazyRequestBody req
                    case fn reqBody of
                        Right res ->
                            nt res `catch` (\(SomeException e) -> throwIO $ UserApplicationError e)
                        Left err ->
                            throwIO $ ServerError
                              { errorStatus = badRequest400
                              , errorBody = LBS.fromStrict $ encodeUtf8 err
                              , errorHeaders = []
                              }
                PureRequestData a ->
                    nt a `catch` (\(SomeException e) -> throwIO $ UserApplicationError e)

        resp $ responseLBS status ((hContentType, renderHeader ctype):rHeaders) rBody

serverErrorToResponse :: ServerError -> Response
serverErrorToResponse err =
    responseLBS (errorStatus err) (errorHeaders err) (errorBody err)

data ServerError = ServerError
    { errorStatus  :: Status
    , errorBody    :: LBS.ByteString
    , errorHeaders :: [Header]
    } deriving Show
instance Exception ServerError

defaultErrorHandler :: SomeException -> ServerError
defaultErrorHandler _ =
    ServerError
        { errorStatus = internalServerError500
        , errorBody = "Internal Server Error"
        , errorHeaders = []
        }