packages feed

ytl-0.1.0.0: src/Yesod/Trans/Class/Writer.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

module Yesod.Trans.Class.Writer
  ( WriterSite (..)
  , runWriterSite

  , SiteWriter (..)
  ) where

import Yesod.Site.Class
import Yesod.Trans.Class
import Yesod.Trans.Class.Reader

import Data.Copointed
import Data.IORef
import Yesod.Core
  ( liftIO
  , RenderRoute (..)
  )

-- TODO: Decide if we want a lazy/strict distinction

-- | The class of sites which have some writing output
class (Monoid w) => SiteWriter w site | site -> w where
  {-# MINIMAL (writer | tell), listen, pass #-}

  -- | Write something to the output, returning a wrapped value
  writer :: (MonadSite m) => (a, w) -> m site a
  writer (a, w) = tell w >> pure a

  -- | Write something to the output
  tell :: (MonadSite m) => w -> m site ()
  tell w = writer ((), w)

  -- | Run a computation, returning the resulting contents of the output
  listen :: (MonadSite m) => m site a -> m site (a, w)

  -- | Run a computation which modifies the output
  pass :: MonadSite m => m site (a, w -> w) -> m site a

instance {-# OVERLAPPABLE #-}
  (SiteTrans t, SiteWriter w site) => SiteWriter w (t site) where
  writer = lift . writer
  tell = lift . tell

  listen = mapSiteT listen
  pass = mapSiteT pass

-- | A site transformation which extends a site with some writing output
newtype WriterSite w site = WriterSite
  { unWriterSite :: ReaderSite (IORef w) site
    -- "Is an IORef safe in Yesod sites?"
    --
    -- Yes, because the Yesod code uses one to build its web pages.
  }

-- | Compute the effect of a 'WriterSite', getting back the output after having
-- run the computation
runWriterSite
  :: (MonadSite m, Monoid w)
  => m (WriterSite w site) a
  -> m site (a, w)
runWriterSite inner = do
  wRef <- liftIO (newIORef mempty)
  a <- runReaderSite wRef $ withSiteT WriterSite $ inner
  w <- liftIO $ readIORef wRef
  pure (a, w)

instance Copointed (WriterSite w) where
  copoint = copoint . unWriterSite

instance {-# OVERLAPPING #-} (Monoid w) => SiteWriter w (WriterSite w site) where
  tell v = withSiteT unWriterSite do
    wRef <- ask
    liftIO $ modifyIORef' wRef (<> v)

  listen argM = do
    a <- argM
    withSiteT unWriterSite do
      wRef <- ask
      w <- liftIO $ readIORef wRef
      pure (a, w)

  pass modM = do
    (a, f) <- modM
    withSiteT unWriterSite do
      wRef <- ask
      liftIO $ modifyIORef' wRef f
      pure a

instance RenderRoute site => RenderRoute (WriterSite w site) where
  newtype Route (WriterSite w site) = WriterRoute (Route (ReaderSite (IORef w) site))
  renderRoute (WriterRoute route) = renderRoute route

deriving instance Eq (Route site) => Eq (Route (WriterSite w site))

instance SiteTrans (WriterSite w) where
  lift = withSiteT unWriterSite . lift

  mapSiteT runner argM = do
    withSiteT unWriterSite $ mapSiteT runner $ withSiteT WriterSite argM