Eventually-decentralized project hosting and management platform

[[ 🗃 ^WvWbo vervis ]] :: [📥 Inbox] [📤 Outbox] [🐤 Followers] [🤝 Collaborators] [🛠 Changes]

Clone

HTTPS: darcs clone https://vervis.peers.community/repos/WvWbo

SSH: darcs clone USERNAME@vervis.peers.community:WvWbo

Tags

TODO

src / Yesod /

MonadSite.hs

{- This file is part of Vervis.
 -
 - Written in 2019, 2020 by fr33domlover <fr33domlover@riseup.net>.
 -
 - ♡ Copying is an act of love. Please copy, reuse and share.
 -
 - The author(s) have dedicated all copyright and related and neighboring
 - rights to this software to the public domain worldwide. This software is
 - distributed without any warranty.
 -
 - You should have received a copy of the CC0 Public Domain Dedication along
 - with this software. If not, see
 - <http://creativecommons.org/publicdomain/zero/1.0/>.
 -}

-- | A typeclass providing a subset of what 'HandlerFor' does, allowing to
-- write monadic actions that can run both inside a request handler and outside
-- of the web server context.
module Yesod.MonadSite
    ( Site (..)
    , MonadSite (..)
    , askUrlRender
    , asksSite
    , runSiteDB
    , runSiteDBExcept
    , runDBExcept
    , WorkerT ()
    , runWorkerT
    , WorkerFor
    , runWorker
    , runWorkerExcept
    , forkWorker
    , asyncWorker
    )
where

import Control.Monad.Fail
import Control.Monad.IO.Class
import Control.Monad.IO.Unlift
import Control.Monad.Logger.CallStack
import Control.Monad.Trans.Class
import Control.Monad.Trans.Except
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.Reader
import Data.Functor
import Data.Text (Text)
import Database.Persist.Sql
import UnliftIO.Async
import UnliftIO.Exception
import UnliftIO.Concurrent
import Yesod.Core hiding (logError)
import Yesod.Core.Types
import Yesod.Persist.Core

import qualified Control.Monad.Trans.RWS.Lazy as RWSL
import qualified Data.Text as T

class PersistConfig (SitePersistConfig site) => Site site where
    type SitePersistConfig site
    siteApproot       :: site -> Text
    sitePersistConfig :: site -> SitePersistConfig site
    sitePersistPool   :: site -> PersistConfigPool (SitePersistConfig site)
    siteLogger        :: site -> Logger

class (MonadIO m, MonadLogger m) => MonadSite m where
    type SiteEnv m
    askSite            :: m (SiteEnv m)
    askUrlRenderParams :: m (Route (SiteEnv m) -> [(Text, Text)] -> Text)
    {-
    forkSite     :: (SomeException -> m ()) -> m () -> m ()
    asyncSite    :: m a -> m (m (Either SomeException a))
    -}

askUrlRender :: MonadSite m => m (Route (SiteEnv m) -> Text)
askUrlRender = do
    render <- askUrlRenderParams
    return $ \ route -> render route []

instance MonadSite m => MonadSite (ReaderT r m) where
    type SiteEnv (ReaderT r m) = SiteEnv m
    askSite                    = lift askSite
    askUrlRenderParams         = lift askUrlRenderParams

instance MonadSite m => MonadSite (MaybeT m) where
    type SiteEnv (MaybeT m) = SiteEnv m
    askSite                 = lift askSite
    askUrlRenderParams      = lift askUrlRenderParams

instance MonadSite m => MonadSite (ExceptT e m) where
    type SiteEnv (ExceptT e m) = SiteEnv m
    askSite                    = lift askSite
    askUrlRenderParams         = lift askUrlRenderParams

instance (Monoid w, MonadSite m) => MonadSite (RWSL.RWST r w s m) where
    type SiteEnv (RWSL.RWST r w s m) = SiteEnv m
    askSite                          = lift askSite
    askUrlRenderParams               = lift askUrlRenderParams

asksSite :: MonadSite m => (SiteEnv m -> a) -> m a
asksSite f = f <$> askSite

runSiteDB
    :: (MonadUnliftIO m, MonadSite m, Site (SiteEnv m))
    => PersistConfigBackend (SitePersistConfig (SiteEnv m)) m a
    -> m a
runSiteDB action = do
    site <- askSite
    runPool (sitePersistConfig site) action (sitePersistPool site)

newtype FedError = FedError Text deriving Show

instance Exception FedError

runSiteDBExcept
    :: ( MonadUnliftIO m
       , MonadSite m
       , SiteEnv m ~ site
       , Site site
       , MonadIO (PersistConfigBackend (SitePersistConfig site) m)
       )
    => ExceptT Text (PersistConfigBackend (SitePersistConfig site) m) a
    -> ExceptT Text m a
runSiteDBExcept action = do
    result <-
        lift $ try $ runSiteDB $ either abort return =<< runExceptT action
    case result of
        Left (FedError t) -> throwE t
        Right r -> return r
    where
    abort = throwIO . FedError

runDBExcept
    :: ( Site site
       , MonadIO (PersistConfigBackend (SitePersistConfig site) (HandlerFor site))
       )
    => ExceptT Text (PersistConfigBackend (SitePersistConfig site) (HandlerFor site)) a
    -> ExceptT Text (HandlerFor site) a
runDBExcept = runSiteDBExcept

instance MonadSite (HandlerFor site) where
    type SiteEnv (HandlerFor site) = site
    askSite = getYesod
    askUrlRenderParams = getUrlRenderParams
    {-
    forkSite = forkHandler
    asyncSite action = do
        mvar <- newEmptyMVar
        let handle e = putMVar mvar $ Left e
        forkHandler handle $ do
            result <- action
            putMVar mvar $ Right result
        return $ liftIO $ readMVar mvar
    -}

instance MonadSite (WidgetFor site) where
    type SiteEnv (WidgetFor site) = site
    askSite = getYesod
    askUrlRenderParams = getUrlRenderParams

newtype WorkerT site m a = WorkerT
    { unWorkerT :: LoggingT (ReaderT site m) a
    }
    deriving
        ( Functor, Applicative, Monad, MonadFail, MonadIO, MonadLogger
        , MonadLoggerIO
        )

instance MonadUnliftIO m => MonadUnliftIO (WorkerT site m) where
    askUnliftIO =
        WorkerT $ withUnliftIO $ \ u ->
            return $ UnliftIO $ unliftIO u . unWorkerT
    withRunInIO inner =
        WorkerT $ withRunInIO $ \ run -> inner (run . unWorkerT)

instance MonadTrans (WorkerT site) where
    lift = WorkerT . lift . lift

instance (MonadUnliftIO m, Yesod site, Site site) => MonadSite (WorkerT site m) where
    type SiteEnv (WorkerT site m) = site
    askSite = WorkerT $ lift ask
    askUrlRenderParams = do
        site <- askSite
        return $ yesodRender site (siteApproot site)
    {-
    forkSite handler action = void $ forkFinally action handler'
        where
        handler' (Left e)  = handler e
        handler' (Right _) = pure ()
    asyncSite action = waitCatch <$> async action
    -}

runWorkerT :: (Yesod site, Site site) => WorkerT site m a -> site -> m a
runWorkerT (WorkerT action) site = runReaderT (runLoggingT action logFunc) site
    where
    logFunc = messageLoggerSource site (siteLogger site)

type WorkerFor site = WorkerT site IO

runWorker :: (Yesod site, Site site) => WorkerFor site a -> site -> IO a
runWorker = runWorkerT

runWorkerExcept action = do
    site <- askSite
    ExceptT $ liftIO $ runWorker (runExceptT action) site

forkWorker
    :: (MonadSite m, Yesod site, Site site, SiteEnv m ~ site)
    => Text
    -> WorkerFor site ()
    -> m ()
forkWorker err worker = do
    site <- askSite
    void $ liftIO $ forkFinally (runWorker worker site) (handler site)
    where
    handler site r = flip runWorker site $
        case r of
            Left e ->
                logError $
                    "Worker thread threw exception: " <> err <> ": " <>
                    T.pack (displayException e)
            Right _ -> return ()

asyncWorker
    :: (MonadSite m, SiteEnv m ~ site, Yesod site, Site site)
    => WorkerFor site a
    -> m (m (Either SomeException a))
asyncWorker worker = do
    site <- askSite
    liftIO $ waitCatch <$> async (runWorker worker site)
[See repo JSON]