packages feed

keiro-0.17.0.0: test/ReactionExample.hs

module ReactionExample
  ( ExampleInput (..),
    Severity (..),
    ExampleSagaCommand (..),
    ExampleSagaEvent (..),
    ExampleTargetCommand (..),
    ExampleTargetEvent (..),
    ExampleSagaStream,
    ExampleTargetStream,
    exampleReactionManager,
    exampleTargetEventStream,
    exampleReminderTimerId,
    exampleEscalationTimerId,
  )
where

import Data.Aeson (FromJSON, ToJSON, object, withObject, (.:))
import Data.Aeson qualified as Aeson
import Data.Aeson.Types (Parser, parseEither)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Proxy (Proxy (..))
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Time (UTCTime, addUTCTime)
import Data.UUID qualified as UUID
import GHC.Generics (Generic)
import Keiki.Core
  ( Edge (..),
    HsPred,
    InCtor,
    RegFile (..),
    SymTransducer (..),
    Update (..),
    WireCtor,
    inpCtor,
    matchInCtor,
    oNil,
    pack,
    (*:),
  )
import Keiki.Core qualified as Keiki
import Keiki.Shape (CanonicalStateShape)
import Keiro.Codec (Codec (..))
import Keiro.Command (DomainCommandHandler (..), SilentDomainDecision (..))
import Keiro.EventStream (EventStream (..), SnapshotPolicy (..))
import Keiro.EventStream.Validate (ValidatedEventStream, mkEventStreamOrThrow)
import Keiro.ProcessManager (PMCommand (..))
import Keiro.ProcessManager.Reaction qualified as Reaction
import Keiro.Stream (stream, streamName)
import Keiro.Timer (TimerId (..), TimerRequest (..))
import Kiroku.Store.Types (EventType (..))

data Severity = Routine | Urgent
  deriving stock (Generic, Eq, Show)
  deriving anyclass (FromJSON, ToJSON)

data ExampleInput
  = IncidentReported !Text !Severity
  | IncidentAcknowledged !Text
  deriving stock (Generic, Eq, Show)

data ExampleSagaCommand
  = RecordIncident !Severity
  | RecordAcknowledgement
  deriving stock (Generic, Eq, Show)

data ExampleSagaEvent
  = IncidentRecorded !Severity
  | AcknowledgementRecorded
  deriving stock (Generic, Eq, Show)

data ExampleSagaState = Tracking
  deriving stock (Generic, Eq, Show, Enum, Bounded, Ord)
  deriving anyclass (FromJSON, ToJSON)

instance CanonicalStateShape ExampleSagaState

data ExampleTargetCommand
  = SendAlert !Text
  | ApplyLateTimeout
  deriving stock (Generic, Eq, Show)

data ExampleTargetEvent
  = AlertSent !Text
  | LateTimeoutApplied
  deriving stock (Generic, Eq, Show)

data ExampleTargetState = AwaitingAlert | AlertComplete
  deriving stock (Generic, Eq, Show, Enum, Bounded, Ord)
  deriving anyclass (FromJSON, ToJSON)

instance CanonicalStateShape ExampleTargetState

type SeverityFields = '[ '("severity", Severity)]

type CorrelationFields = '[ '("correlationId", Text)]

type ExampleSagaStream = EventStream (HsPred '[] ExampleSagaCommand) '[] ExampleSagaState ExampleSagaCommand ExampleSagaEvent

type ExampleTargetStream = EventStream (HsPred '[] ExampleTargetCommand) '[] ExampleTargetState ExampleTargetCommand ExampleTargetEvent

exampleReactionManager ::
  UTCTime ->
  Reaction.ReactiveProcessManager
    ExampleInput
    (HsPred '[] ExampleSagaCommand)
    '[]
    ExampleSagaState
    ExampleSagaCommand
    ExampleSagaEvent
    (HsPred '[] ExampleTargetCommand)
    '[]
    ExampleTargetState
    ExampleTargetCommand
    ExampleTargetEvent
    Text
    Text
exampleReactionManager injectedNow =
  Reaction.ReactiveProcessManager
    { name = "incident-reaction-example",
      correlate = \case
        IncidentReported correlationId _ -> correlationId
        IncidentAcknowledged correlationId -> correlationId,
      sagaHandler = exampleSagaHandler,
      streamFor = \correlationId -> stream ("incident-reaction-saga:" <> correlationId),
      targetEventStream = exampleTargetEventStream,
      targetProjections = const [],
      react = \case
        IncidentReported _ Routine -> Reaction.NoAdvance []
        IncidentReported correlationId Urgent ->
          Reaction.AdvanceReaction
            { command = RecordIncident Urgent,
              followUps =
                [ Reaction.FollowSchedule Reaction.Once (reminderTimer correlationId),
                  Reaction.FollowSchedule Reaction.Once (escalationTimer correlationId)
                ],
              onAccepted =
                [ Reaction.FollowDispatch
                    (PMCommand (stream ("incident-reaction-target:" <> correlationId)) (SendAlert correlationId))
                ]
            }
        IncidentAcknowledged _ ->
          Reaction.AdvanceReaction
            { command = RecordAcknowledgement,
              followUps =
                [ Reaction.FollowCancel exampleReminderTimerId,
                  Reaction.FollowCancel exampleEscalationTimerId
                ],
              onAccepted = []
            }
    }
  where
    reminderTimer correlationId =
      TimerRequest
        { timerId = exampleReminderTimerId,
          processManagerName = "incident-reaction-example-reminder",
          correlationId,
          fireAt = addUTCTime 300 injectedNow,
          payload = object ["kind" Aeson..= ("reminder" :: Text), "correlationId" Aeson..= correlationId]
        }
    escalationTimer correlationId =
      TimerRequest
        { timerId = exampleEscalationTimerId,
          processManagerName = "incident-reaction-example-escalation",
          correlationId,
          fireAt = addUTCTime 900 injectedNow,
          payload = object ["kind" Aeson..= ("escalation" :: Text), "correlationId" Aeson..= correlationId]
        }

exampleReminderTimerId :: TimerId
exampleReminderTimerId = TimerId (UUID.fromWords 0x27900001 0 0 1)

exampleEscalationTimerId :: TimerId
exampleEscalationTimerId = TimerId (UUID.fromWords 0x27900002 0 0 2)

exampleSagaHandler :: DomainCommandHandler (HsPred '[] ExampleSagaCommand) '[] ExampleSagaState ExampleSagaCommand ExampleSagaEvent Text Text
exampleSagaHandler =
  DomainCommandHandler
    { eventStream = exampleSagaEventStream,
      classifySilent = \_ -> SilentNoOp "already acknowledged"
    }

exampleSagaEventStream :: ValidatedEventStream (HsPred '[] ExampleSagaCommand) '[] ExampleSagaState ExampleSagaCommand ExampleSagaEvent
exampleSagaEventStream = mkEventStreamOrThrow "reaction-example-saga" exampleSagaEventStreamDef

exampleSagaEventStreamDef :: ExampleSagaStream
exampleSagaEventStreamDef =
  EventStream
    { transducer = exampleSagaTransducer,
      initialState = Tracking,
      initialRegisters = RNil,
      eventCodec = exampleSagaCodec,
      resolveStreamName = streamName,
      snapshotPolicy = Never,
      stateCodec = Nothing
    }

exampleSagaTransducer :: SymTransducer (HsPred '[] ExampleSagaCommand) '[] ExampleSagaState ExampleSagaCommand ExampleSagaEvent
exampleSagaTransducer =
  SymTransducer
    { edgesOut = \Tracking ->
        [ Edge
            { guard = matchInCtor recordIncidentCtor,
              update = UKeep,
              output = [pack recordIncidentCtor incidentRecordedCtor (inpCtor recordIncidentCtor #severity *: oNil)],
              target = Tracking,
              mode = Keiki.Live
            },
          Edge
            { guard = matchInCtor recordAcknowledgementCtor,
              update = UKeep,
              output = [pack recordAcknowledgementCtor acknowledgementRecordedCtor oNil],
              target = Tracking,
              mode = Keiki.Live
            }
        ],
      initial = Tracking,
      initialRegs = RNil,
      isFinal = const False
    }

exampleTargetEventStream :: ValidatedEventStream (HsPred '[] ExampleTargetCommand) '[] ExampleTargetState ExampleTargetCommand ExampleTargetEvent
exampleTargetEventStream = mkEventStreamOrThrow "reaction-example-target" exampleTargetEventStreamDef

exampleTargetEventStreamDef :: ExampleTargetStream
exampleTargetEventStreamDef =
  EventStream
    { transducer = exampleTargetTransducer,
      initialState = AwaitingAlert,
      initialRegisters = RNil,
      eventCodec = exampleTargetCodec,
      resolveStreamName = streamName,
      snapshotPolicy = Never,
      stateCodec = Nothing
    }

exampleTargetTransducer :: SymTransducer (HsPred '[] ExampleTargetCommand) '[] ExampleTargetState ExampleTargetCommand ExampleTargetEvent
exampleTargetTransducer =
  SymTransducer
    { edgesOut = \case
        AwaitingAlert ->
          [ Edge
              { guard = matchInCtor sendAlertCtor,
                update = UKeep,
                output = [pack sendAlertCtor alertSentCtor (inpCtor sendAlertCtor #correlationId *: oNil)],
                target = AlertComplete,
                mode = Keiki.Live
              },
            Edge
              { guard = matchInCtor applyLateTimeoutCtor,
                update = UKeep,
                output = [pack applyLateTimeoutCtor lateTimeoutAppliedCtor oNil],
                target = AlertComplete,
                mode = Keiki.Live
              }
          ]
        AlertComplete ->
          [ Edge
              { guard = matchInCtor applyLateTimeoutCtor,
                update = UKeep,
                output = [],
                target = AlertComplete,
                mode = Keiki.Live
              }
          ],
      initial = AwaitingAlert,
      initialRegs = RNil,
      isFinal = const False
    }

recordIncidentCtor :: InCtor ExampleSagaCommand SeverityFields
recordIncidentCtor =
  Keiki.unavailableInCtor
    "RecordIncident"
    (\case RecordIncident severity -> Just (RCons Proxy severity RNil); RecordAcknowledgement -> Nothing)
    (\case RCons _ severity RNil -> RecordIncident severity)

recordAcknowledgementCtor :: InCtor ExampleSagaCommand '[]
recordAcknowledgementCtor =
  Keiki.unavailableInCtor
    "RecordAcknowledgement"
    (\case RecordAcknowledgement -> Just RNil; RecordIncident {} -> Nothing)
    (\RNil -> RecordAcknowledgement)

incidentRecordedCtor :: WireCtor ExampleSagaEvent (Severity, ())
incidentRecordedCtor =
  Keiki.unavailableWireCtor
    "IncidentRecorded"
    (\case IncidentRecorded severity -> Just (severity, ()); AcknowledgementRecorded -> Nothing)
    (\(severity, ()) -> IncidentRecorded severity)

acknowledgementRecordedCtor :: WireCtor ExampleSagaEvent ()
acknowledgementRecordedCtor =
  Keiki.unavailableWireCtor
    "AcknowledgementRecorded"
    (\case AcknowledgementRecorded -> Just (); IncidentRecorded {} -> Nothing)
    (const AcknowledgementRecorded)

sendAlertCtor :: InCtor ExampleTargetCommand CorrelationFields
sendAlertCtor =
  Keiki.unavailableInCtor
    "SendAlert"
    (\case SendAlert correlationId -> Just (RCons Proxy correlationId RNil); ApplyLateTimeout -> Nothing)
    (\case RCons _ correlationId RNil -> SendAlert correlationId)

applyLateTimeoutCtor :: InCtor ExampleTargetCommand '[]
applyLateTimeoutCtor =
  Keiki.unavailableInCtor
    "ApplyLateTimeout"
    (\case ApplyLateTimeout -> Just RNil; SendAlert {} -> Nothing)
    (\RNil -> ApplyLateTimeout)

alertSentCtor :: WireCtor ExampleTargetEvent (Text, ())
alertSentCtor =
  Keiki.unavailableWireCtor
    "AlertSent"
    (\case AlertSent correlationId -> Just (correlationId, ()); LateTimeoutApplied -> Nothing)
    (\(correlationId, ()) -> AlertSent correlationId)

lateTimeoutAppliedCtor :: WireCtor ExampleTargetEvent ()
lateTimeoutAppliedCtor =
  Keiki.unavailableWireCtor
    "LateTimeoutApplied"
    (\case LateTimeoutApplied -> Just (); AlertSent {} -> Nothing)
    (const LateTimeoutApplied)

exampleSagaCodec :: Codec ExampleSagaEvent
exampleSagaCodec =
  Codec
    { eventTypes = EventType "IncidentRecorded" :| [EventType "AcknowledgementRecorded"],
      eventType = \case
        IncidentRecorded {} -> EventType "IncidentRecorded"
        AcknowledgementRecorded -> EventType "AcknowledgementRecorded",
      schemaVersion = 1,
      encode = \case
        IncidentRecorded severity -> object ["severity" Aeson..= severity]
        AcknowledgementRecorded -> object [],
      decode = \eventType value -> parseCodec eventType value $ \objectValue -> case eventType of
        EventType "IncidentRecorded" -> IncidentRecorded <$> objectValue .: "severity"
        EventType "AcknowledgementRecorded" -> pure AcknowledgementRecorded
        _ -> fail "unknown example saga event",
      upcasters = []
    }

exampleTargetCodec :: Codec ExampleTargetEvent
exampleTargetCodec =
  Codec
    { eventTypes = EventType "AlertSent" :| [EventType "LateTimeoutApplied"],
      eventType = \case
        AlertSent {} -> EventType "AlertSent"
        LateTimeoutApplied -> EventType "LateTimeoutApplied",
      schemaVersion = 1,
      encode = \case
        AlertSent correlationId -> object ["correlationId" Aeson..= correlationId]
        LateTimeoutApplied -> object [],
      decode = \eventType value -> parseCodec eventType value $ \objectValue -> case eventType of
        EventType "AlertSent" -> AlertSent <$> objectValue .: "correlationId"
        EventType "LateTimeoutApplied" -> pure LateTimeoutApplied
        _ -> fail "unknown example target event",
      upcasters = []
    }

parseCodec :: EventType -> Aeson.Value -> (Aeson.Object -> Parser event) -> Either Text event
parseCodec eventType value parser =
  case parseEither (withObject (Text.unpack (coerceEventType eventType)) parser) value of
    Left message -> Left (Text.pack message)
    Right decoded -> Right decoded

coerceEventType :: EventType -> Text
coerceEventType (EventType eventType) = eventType