packages feed

salmon-ops-0.1.0.0: src/Salmon/Reporter.hs

-- https://www.youtube.com/watch?v=qzOQOmmkKEM&feature=emb_logo

module Salmon.Reporter (
    Reporter,
    ReporterM (..),
    silent,
    reportIf,
    reportWhen,
    reportBoth,
    reportPick,

    -- * common utilities
    reportPrint,
    reportHPrint,
    reportHPut,
    encodeJSON,
    pulls,

    -- * re-exports
    Contravariant (..),
    Divisible (..),
    Decidable (..),
) where

import Control.Monad ((>=>))
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Aeson (ToJSON, encode)
import Data.ByteString.Lazy (ByteString, hPut)
import Data.Functor.Contravariant
import Data.Functor.Contravariant.Divisible
import System.IO (Handle, hPrint)

type Reporter = ReporterM IO

newtype ReporterM m a = ReporterM {runReporter :: (a -> m ())}

instance Contravariant (ReporterM m) where
    contramap f (ReporterM g) = ReporterM (g . f)

instance (Applicative m) => Divisible (ReporterM m) where
    conquer = silent
    divide = reportSplit

instance (Applicative m) => Decidable (ReporterM m) where
    lose _ = silent
    choose = reportPick

-- | Disable Tracing.
{-# INLINE silent #-}
silent :: (Applicative m) => ReporterM m a
silent = ReporterM (const $ pure ())

{- | Splits a reporter into two chunks that are run sequentially.

This name can be confusing but it has to be thought backwards for Contravariant logging:
We compose a target reporter from two reporters but we split the content of the report.

Note that the split function may actually duplicate inputs (that's how reportBoth works).
-}
{-# INLINEABLE reportSplit #-}
reportSplit :: (Applicative m) => (c -> (a, b)) -> ReporterM m a -> ReporterM m b -> ReporterM m c
reportSplit split (ReporterM f1) (ReporterM f2) = ReporterM (go . split)
  where
    go (b, c) = f1 b *> f2 c

{- | If you are given two reporters and want to pass both.
Composition occurs in sequence.
-}
{-# INLINEABLE reportBoth #-}
reportBoth :: (Applicative m) => ReporterM m a -> ReporterM m a -> ReporterM m a
reportBoth t1 t2 = reportSplit (\x -> (x, x)) t1 t2

{- | Picks a reporter based on the emitted object.
Example logic that can be built is reportIf that silent messages.
-}
{-# INLINEABLE reportPick #-}
reportPick :: (Applicative m) => (c -> Either a b) -> ReporterM m a -> ReporterM m b -> ReporterM m c
reportPick split (ReporterM f1) (ReporterM f2) = ReporterM $ \a ->
    let e = split a
     in either f1 f2 e

-- | Filter by dynamically testing values.
{-# INLINEABLE reportIf #-}
reportIf :: forall m a. (Applicative m) => (a -> Bool) -> ReporterM m a -> ReporterM m a
reportIf predicate t = reportPick f silent t
  where
    f :: a -> Either () a
    f x = if predicate x then Right x else Left ()

-- | Like @reportIf@ but using a @Predicate@.
{-# INLINEABLE reportWhen #-}
reportWhen :: forall m a. (Applicative m) => Predicate a -> ReporterM m a -> ReporterM m a
reportWhen (Predicate predicate) t =
    reportIf predicate t

-- | A reporter that prints emitted events.
reportPrint :: (MonadIO m, Show a) => ReporterM m a
reportPrint = ReporterM (liftIO . print)

-- | A reporter that prints emitted to some handle.
reportHPrint :: (MonadIO m, Show a) => Handle -> ReporterM m a
reportHPrint handle = ReporterM (liftIO . hPrint handle)

-- | A reporter that puts some ByteString to some handle.
reportHPut :: (MonadIO m) => Handle -> ReporterM m ByteString
reportHPut handle = ReporterM (liftIO . hPut handle)

-- | A conversion encoding values to JSON.
{-# INLINE encodeJSON #-}
encodeJSON :: (ToJSON a) => ReporterM m ByteString -> ReporterM m a
encodeJSON = contramap encode

{- | Pulls a value to complete a report when a report occurs.

This function allows to combines pushed values with pulled values.  Hence,
performing some scheduling between behaviours.
Typical usage would be to annotate a report with a background value, or perform
data augmentation in a pipelines of reports.

Note that if you rely on this function you need to pay attention of the
blocking effect of 'pulls': the reported value c is not forwarded until a
value b is available.
-}
{-# INLINE pulls #-}
pulls :: (Monad m) => (c -> m b) -> ReporterM m b -> ReporterM m c
pulls act (ReporterM f1) = ReporterM $ act >=> f1