packages feed

keiro-dsl-0.7.0.0: src/Keiro/Dsl/EventOutput.hs

{-# OPTIONS_GHC -Werror=incomplete-patterns #-}

-- | Checked ownership for values emitted by aggregate transitions.
--
-- The source grammar preserves whether an event copied a command with
-- @fields(Command)@ or declared an explicit field list.  This module turns that
-- provenance into the one semantic model consumed by validation, fingerprints,
-- scaffolding, and behavior coverage.  No downstream consumer may infer
-- ownership merely from coincidentally equal field names.
module Keiro.Dsl.EventOutput
  ( CheckedFieldCopy (..),
    OutputObligationKey (..),
    EventOutputMapping (..),
    EventOutputError (..),
    eventOutputMapping,
    eventOutputCanonical,
  )
where

import Data.List (find)
import Data.Text (Text)
import Data.Text qualified as T
import Keiro.Dsl.AggregateType
import Keiro.Dsl.Grammar
import Keiro.Dsl.PrettyPrint (renderExpr)

-- | One field in a checked identity copy.  The selector and current wire name
-- are retained separately so a future aggregate-field alias can change the wire
-- identity without changing how generated Haskell reads the command payload.
data CheckedFieldCopy = CheckedFieldCopy
  { outputSelector :: !Name,
    outputWireName :: !Text,
    outputFieldType :: !ResolvedAggregateType
  }
  deriving stock (Eq, Ord, Show)

-- | Stable, semantic name for one consumer-owned event-output obligation.
newtype OutputObligationKey = OutputObligationKey {unOutputObligationKey :: Text}
  deriving stock (Eq, Ord, Show)

-- | Exclusive ownership of one event term emitted by one transition.
data EventOutputMapping
  = GeneratedCommandIdentity
      { outputSourceCommand :: !Name,
        outputFields :: ![CheckedFieldCopy]
      }
  | HandOwnedEventOutput
      { outputObligation :: !OutputObligationKey
      }
  deriving stock (Eq, Ord, Show)

data EventOutputError
  = OutputEventMissing !Name
  | OutputSourceCommandMissing !Name
  | OutputCommandMismatch
      { declaredSourceCommand :: !Name,
        consumingTransitionCommand :: !Name,
        emittedEventName :: !Name
      }
  | OutputFieldTypeInvalid !AggregateTypeError
  | OutputFieldTypeMismatch !Name !ResolvedAggregateType !ResolvedAggregateType
  deriving stock (Eq, Show)

-- | Resolve one transition/event pair.  @fields(Command)@ is generated-owned
-- only for a transition consuming that exact command.  Explicit event fields
-- remain hand-owned even when their names happen to match command fields.
eventOutputMapping :: Spec -> Aggregate -> Transition -> Int -> Name -> Either EventOutputError EventOutputMapping
eventOutputMapping spec aggregate transition emitIndex eventName = do
  event <- maybe (Left (OutputEventMissing eventName)) Right (find ((== eventName) . evName) (aggEvents aggregate))
  case (tImplementation transition, evBody event) of
    (LegacyHoleImplementation, _) -> pure handOwned
    (_, EventFields _) -> pure handOwned
    (_, EventFromCommand sourceCommand)
      | sourceCommand /= tCommand transition ->
          Left
            OutputCommandMismatch
              { declaredSourceCommand = sourceCommand,
                consumingTransitionCommand = tCommand transition,
                emittedEventName = eventName
              }
      | otherwise -> do
          command <- maybe (Left (OutputSourceCommandMissing sourceCommand)) Right (find ((== sourceCommand) . cmdName) (aggCommands aggregate))
          fields <- traverse checkedCopy (cmdFields command)
          pure
            GeneratedCommandIdentity
              { outputSourceCommand = sourceCommand,
                outputFields = fields
              }
  where
    symbols = aggregateSymbols spec
    handOwned =
      HandOwnedEventOutput
        { outputObligation =
            OutputObligationKey
              ( T.intercalate
                  "/"
                  [ "event-output-v1",
                    aggName aggregate,
                    transitionModeName (tMode transition),
                    tSource transition,
                    tCommand transition,
                    maybe "unguarded" renderExpr (tGuard transition),
                    T.intercalate ";" [register <> ":=" <> renderExpr expression | (register, expression) <- tWrites transition],
                    T.intercalate "," (tEmits transition),
                    tGoto transition,
                    T.pack (show emitIndex),
                    eventName
                  ]
              )
        }
    checkedCopy field = do
      commandType <- mapLeft OutputFieldTypeInvalid (inferAggregateFieldType symbols aggregate CommandFieldUse field)
      eventType <- mapLeft OutputFieldTypeInvalid (inferAggregateFieldType symbols aggregate EventFieldUse field)
      if commandType == eventType
        then
          pure
            CheckedFieldCopy
              { outputSelector = aggregateFieldName field,
                outputWireName = aggregateFieldName field,
                outputFieldType = commandType
              }
        else Left (OutputFieldTypeMismatch (aggregateFieldName field) commandType eventType)

-- | Canonical ownership text used by fold and behavior fingerprints.
eventOutputCanonical :: EventOutputMapping -> Text
eventOutputCanonical mapping = case mapping of
  GeneratedCommandIdentity command fields ->
    "generated-command-identity:"
      <> command
      <> "["
      <> T.intercalate
        ","
        [ outputSelector field
            <> "="
            <> outputWireName field
            <> ":"
            <> aggregateCanonicalName (outputFieldType field)
        | field <- fields
        ]
      <> "]"
  HandOwnedEventOutput obligation -> "hand-owned:" <> unOutputObligationKey obligation

mapLeft :: (left -> other) -> Either left right -> Either other right
mapLeft f = either (Left . f) Right

transitionModeName :: TransitionMode -> Text
transitionModeName TmLive = "live"
transitionModeName TmReplayOnly = "replay-only"