eventium-core-0.2.1: src/Eventium/EventHandler.hs
-- | Defines a universal event handler abstraction.
--
-- An 'EventHandler' is the simplest building block for consuming events.
-- It can be composed via 'Semigroup' (fan-out to multiple handlers),
-- adapted via 'Contravariant' (change the event type), and filtered via
-- 'eventHandlerMapMaybe'.
--
-- This abstraction is used by 'Eventium.EventPublisher.EventPublisher' for
-- synchronous dispatch and by 'Eventium.EventSubscription.EventSubscription'
-- for push-based delivery.
module Eventium.EventHandler
( EventHandler (..),
handleEvents,
eventHandlerMapMaybe,
codecEventHandler,
lenientCodecEventHandler,
embeddedEventHandler,
)
where
import Control.Exception (throw)
import Data.Foldable
import Data.Functor.Contravariant
import Eventium.Codec
import Eventium.TypeEmbedding
-- | An 'EventHandler' consumes events of type @event@ in some monad @m@.
--
-- Instances:
--
-- * 'Contravariant' — adapt the event type via @contramap@
-- * 'Semigroup' — fan-out: @h1 <> h2@ runs both handlers for each event
-- * 'Monoid' — @mempty@ is a no-op handler
newtype EventHandler m event = EventHandler {handleEvent :: event -> m ()}
instance Contravariant (EventHandler m) where
contramap f (EventHandler h) = EventHandler (h . f)
instance (Applicative m) => Semigroup (EventHandler m event) where
EventHandler h1 <> EventHandler h2 = EventHandler $ \e -> h1 e *> h2 e
instance (Applicative m) => Monoid (EventHandler m event) where
mempty = EventHandler $ \_ -> pure ()
-- | Apply an 'EventHandler' to a list of events in order.
handleEvents :: (Monad m) => EventHandler m event -> [event] -> m ()
handleEvents (EventHandler h) = mapM_ h
-- | Filter events before they reach the handler. Events for which the
-- function returns 'Nothing' are silently dropped.
eventHandlerMapMaybe ::
(Applicative m) =>
(eventB -> Maybe eventA) ->
EventHandler m eventA ->
EventHandler m eventB
eventHandlerMapMaybe f (EventHandler h) = EventHandler $ \e -> for_ (f e) h
-- | Wrap an 'EventHandler' with a 'Codec' so it can consume events
-- of the encoded type. Throws 'DecodeError' if decoding fails.
-- Use 'lenientCodecEventHandler' to silently skip failures.
codecEventHandler ::
Codec event encoded ->
EventHandler m event ->
EventHandler m encoded
codecEventHandler codec (EventHandler h) = EventHandler $ \e ->
case codec.decode e of
Just a -> h a
Nothing -> throw $ DecodeError "codecEventHandler" "Failed to decode event"
-- | Like 'codecEventHandler' but silently drops events that fail to
-- decode.
--
-- Recommended for production use when using sum-type codecs, as it
-- allows event handlers to gracefully handle events they don't recognize.
lenientCodecEventHandler ::
(Applicative m) =>
Codec event encoded ->
EventHandler m event ->
EventHandler m encoded
lenientCodecEventHandler codec = eventHandlerMapMaybe codec.decode
-- | Adapt an 'EventHandler' using a 'TypeEmbedding'. Events that do not
-- belong to the embedded subset are silently dropped.
embeddedEventHandler ::
(Applicative m) =>
TypeEmbedding event adapted ->
EventHandler m event ->
EventHandler m adapted
embeddedEventHandler emb = eventHandlerMapMaybe emb.extract