tonatona-servant-0.2.0.0: src/Tonatona/Servant.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE TypeApplications #-}
module Tonatona.Servant
( Tonatona.Servant.run
, runWithHandlers
, redirect
, Config(..)
, Host(..)
, Port
, Protocol(..)
) where
import RIO
import Data.Default (def)
import Data.Kind (Type)
import Network.HTTP.Types.Header
import Network.Wai (Middleware)
import Network.Wai.Handler.Warp (Port)
import qualified Network.Wai.Handler.Warp as Warp
import Network.Wai.Middleware.RequestLogger (OutputFormat(..), logStdout, logStdoutDev, mkRequestLogger, outputFormat)
import Network.Wai.Middleware.RequestLogger.JSON (formatAsJSONWithHeaders)
import Servant
import TonaParser (Parser, (.||), argLong, envVar, optionalVal)
import Tonatona (HasConfig(..), HasParser(..))
import qualified Tonatona.Logger as TonaLogger
{-| Main function.
-}
run ::
forall (api :: Type) env.
(HasServer api '[], HasConfig env Config, HasConfig env TonaLogger.Config)
=> ServerT api (RIO env)
-> RIO env ()
run =
runWithHandlers @api []
{-| Main function which allows you to pass error handlers.
-}
runWithHandlers ::
forall (api :: Type) env.
(HasServer api '[], HasConfig env Config, HasConfig env TonaLogger.Config)
=> (forall a. [RIO.Handler (RIO env) a])
-> ServerT api (RIO env)
-> RIO env ()
runWithHandlers handlers servantServer = do
env <- ask
conf <- asks config
loggingMiddleware <- reqLogMiddleware
let app = runServant @api env handlers servantServer
liftIO $ Warp.run (port conf) $ loggingMiddleware app
runServant ::
forall (api :: Type) env. (HasServer api '[])
=> env
-> (forall a. [RIO.Handler (RIO env) a])
-> ServerT api (RIO env)
-> Application
runServant env handlers servantServer =
serve (Proxy @api) $ hoistServer (Proxy @api) (transformation . t) servantServer
where
t :: forall a. RIO env a -> RIO env a
t = flip catches handlers
transformation
:: forall a. RIO env a -> Servant.Handler a
transformation action = do
let
ioAction = Right <$> runRIO env action
eitherRes <- liftIO $ ioAction `catch` \(e :: ServerError) -> pure $ Left e
case eitherRes of
Right res -> pure res
Left servantErr -> throwError servantErr
redirect :: ByteString -> RIO env a
redirect redirectLocation =
throwM $
err302
{ errHeaders = [(hLocation, redirectLocation)]
}
reqLogMiddleware :: (HasConfig env TonaLogger.Config) => RIO env Middleware
reqLogMiddleware = do
TonaLogger.Config {mode, verbose} <- asks config
case (mode, verbose) of
(TonaLogger.Development, TonaLogger.Verbose True) ->
liftIO mkLogStdoutVerbose
(TonaLogger.Development, TonaLogger.Verbose False) ->
pure logStdoutDev
(_, TonaLogger.Verbose True) ->
pure logStdoutDev
(_, TonaLogger.Verbose False) ->
pure logStdout
mkLogStdoutVerbose :: IO Middleware
mkLogStdoutVerbose = do
mkRequestLogger def
{ outputFormat = CustomOutputFormatWithDetailsAndHeaders formatAsJSONWithHeaders
}
-- Config
-- | This defines the host part of a URL.
--
-- For example, in the URL https://some.url.com:8090/, the host is
-- @some.url.com@.
newtype Host = Host
{ unHost :: Text
} deriving (Eq, IsString, Read, Show)
-- | This defines the protocol part of a URL.
--
-- For example, in the URL https://some.url.com:8090/, the protocol is
-- @https@.
newtype Protocol = Protocol
{ unProtocol :: Text
} deriving (Eq, IsString, Read, Show)
data Config = Config
{ host :: Host
, protocol :: Protocol
, port :: Port
}
deriving (Show)
instance HasParser Host where
parser = Host <$>
optionalVal
"Host name to serve"
(argLong "host" .|| envVar "HOST")
"localhost"
instance HasParser Protocol where
parser = Protocol <$>
optionalVal
"Protocol to serve"
(argLong "protocol" .|| envVar "PROTOCOL")
"http"
portParser :: Parser Port
portParser =
optionalVal
"Port to serve"
(argLong "port" .|| envVar "PORT")
8000
instance HasParser Config where
parser = Config
<$> parser
<*> parser
<*> portParser