packages feed

polysemy-account-api-0.2.0.0: lib/Polysemy/Account/Api/NativeContext.hs

{-# options_haddock prune #-}

-- | Description: Full Polysemy runners for Servant servers
module Polysemy.Account.Api.NativeContext where

import Control.Monad.Trans.Except (ExceptT (ExceptT))
import Exon.Quote (exon)
import qualified Log
import Network.Wai (Application)
import qualified Network.Wai.Handler.Warp as Warp
import Network.Wai.Handler.Warp (
  defaultSettings,
  setBeforeMainLoop,
  setGracefulShutdownTimeout,
  setHost,
  setInstallShutdownHandler,
  setPort,
  )
import qualified Network.Wai.Middleware.RequestLogger as Logger
import Network.Wai.Middleware.RequestLogger (destination, mkRequestLogger)
import qualified Polysemy.Conc.Effect.Interrupt as Interrupt
import Polysemy.Final (withWeavingToFinal)
import Servant (
  Context,
  DefaultErrorFormatters,
  ErrorFormatters,
  Handler (Handler),
  HasContextEntry,
  HasServer,
  Server,
  ServerError,
  ServerT,
  err500,
  hoistServerWithContext,
  serveWithContext,
  type (.++),
  )
import qualified Sync
import System.Log.FastLogger (fromLogStr)

import Polysemy.Account.Data.Port (Port (Port))

-- | A dummy value used to indicate that the server has fully started up, using 'Sync'.
data ServerReady = ServerReady
  deriving stock (Eq, Show)

logErrors ::
  Member Log r =>
  Sem r (Either ServerError a) ->
  Sem r (Either ServerError a)
logErrors ma =
  ma >>= \case
    Right a -> pure (Right a)
    Left err -> Left err <$ Log.error (show err)

lowerServer ::
  ∀ (api :: Type) context r s .
  Functor s =>
  Member Log r =>
  HasServer api context =>
  s () ->
  (∀ a . s (Sem r a) -> IO (s a)) ->
  (∀ x . s x -> Maybe x) ->
  ServerT api (Sem (Stop ServerError : r)) ->
  Server api
lowerServer s lower ins srv =
  hoistServerWithContext (Proxy @api) (Proxy @context) handle srv
  where
    handleErrors =
      logErrors . runStop @ServerError
    cons =
      Handler . ExceptT
    handle :: ∀ x . Sem (Stop ServerError : r) x -> Handler x
    handle ma =
      cons (err <$> lower (handleErrors ma <$ s))
      where
        err =
          ins >>> \case
            Just a -> a
            Nothing -> Left err500

-- | Run a Servant server using a callback in @'Final' 'IO'@, sending logs to 'Log'.
runServerSem ::
  ∀ (api :: Type) context r a .
  HasServer api context =>
  HasContextEntry (context .++ DefaultErrorFormatters) ErrorFormatters =>
  Members [Log, Embed IO, Final IO] r =>
  ServerT api (Sem (Stop ServerError : r)) ->
  Context context ->
  (Application -> IO a) ->
  Sem r a
runServerSem srv context f =
  withWeavingToFinal \ s lower ins ->
    (<$ s) <$> f (serveWithContext (Proxy @api) context (lowerServer @api @context s lower ins srv))

toHandler :: IO (Maybe (Either ServerError a)) -> Handler a
toHandler =
  Handler . ExceptT . fmap (fromMaybe (Left err500))

-- | Run a Servant server using Warp in @'Final' 'IO'@, sending logs to 'Log', registering the shutdown handler with
-- 'Interrupt'.
runServer ::
  ∀ (api :: Type) context r .
  HasServer api context =>
  HasContextEntry (context .++ DefaultErrorFormatters) ErrorFormatters =>
  Members [Sync ServerReady, Log, Interrupt, Final IO] r =>
  ServerT api (Sem (Stop ServerError : r)) ->
  Context context ->
  Port ->
  Sem r ()
runServer srv context (Port port) = do
  Log.info [exon|server port: #{show port}|]
  withWeavingToFinal \ s wv ins -> do
    let
      app =
        serveWithContext (Proxy @api) context (hoistServerWithContext (Proxy @api) (Proxy @context) hoist srv)
      hoist :: Sem (Stop ServerError : r) a -> Handler a
      hoist =
        toHandler . fmap ins . wv . (<$ s) . logErrors . runStop @ServerError
      shut h =
        void (wv (Interrupt.register "api" h <$ s))
      settings =
        setHost "*6" $
        setPort (fromIntegral port) $
        setBeforeMainLoop (void (wv (Sync.putBlock ServerReady <$ s))) $
        setInstallShutdownHandler shut $
        setGracefulShutdownTimeout (Just 0) $
        defaultSettings
      log msg =
        void (wv ((Log.debug (decodeUtf8 (fromLogStr msg))) <$ s))
    logger <- mkRequestLogger def { destination = Logger.Callback log }
    (<$ s) <$> Warp.runSettings settings (logger app)