packages feed

keiro-dsl-0.12.0.0: test/conformance-declarative-router/Main.hs

{-# LANGUAGE MultilineStrings #-}

module Main (main) where

import Conformance.DeclarativeRouter.Domain (TransferRouteInput (..))
import Control.Monad (unless)
import Data.Aeson (Value (Null))
import Data.ByteString (ByteString)
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Time (UTCTime (..), secondsToDiffTime)
import Data.Time.Calendar (Day (ModifiedJulianDay))
import Data.UUID qualified as UUID
import Data.Vector qualified as Vector
import Generated.TransferRouting.Hospital.Domain
  ( HospitalCommand (RouteAcceptedTransferNeed),
    RouteAcceptedTransferNeedData (..),
  )
import Generated.TransferRouting.Hospital.EventStream (hospitalCommandCategory, hospitalEventStream)
import Generated.TransferRouting.HospitalTransferRouter.Router
  ( hospitalTransferRouter,
    hospitalTransferRouterName,
    hospitalTransferRouterSelect,
    hospitalTransferRouterSelectionContract,
    hospitalTransferRouterSelectionFingerprint,
  )
import Generated.TransferRouting.HospitalTransferRouter.RouterHarness (routerHarnessValues)
import Generated.TransferRouting.StructuralConformance (structuralConformanceAssertions)
import Keiro.Command (defaultRunCommandOptions)
import Keiro.ProcessManager (PMCommand (..), PMCommandResult (..))
import Keiro.ReadModel (registerReadModel)
import Keiro.Router
  ( DeclarativeRouter (..),
    DeclarativeRouterResult (..),
    RouterResult (..),
    RouterSelectionFailure (..),
    mkRecipientLimit,
    normalizeRecipients,
    runDeclarativeRouterOnce,
  )
import Keiro.Stream (entityStream, streamName)
import Keiro.Test.Postgres (StoreRunner (..), withFreshResourceStore, withMigratedSuite)
import Kiroku.Store qualified as Store
import Kiroku.Store.Types
  ( EventId (..),
    EventType (..),
    GlobalPosition (..),
    RecordedEvent (..),
    StreamId (..),
    StreamName (..),
    StreamVersion (..),
  )
import Hasql.Transaction qualified as Tx

main :: IO ()
main =
  withMigratedSuite $ \fixture ->
    withFreshResourceStore fixture $ \(_storeHandle, StoreRunner runStore) -> do
      mapM_ (uncurry assert) structuralConformanceAssertions
      assert "generated selection fingerprint is observable" (lengthText hospitalTransferRouterSelectionFingerprint == 64)
      assert "generated harness owns selection" (("resolverOwnership", "generated-declarative") `elem` routerHarnessValues)

      expectRight =<< runStore (Store.runTransaction (Tx.sql initialRowsSql))
      _ <- expectRight =<< runStore (registerReadModel readModelName 1 readModelShape)

      let input = TransferRouteInput "transfer-7" "west"
      selected <- expectRight =<< runStore (hospitalTransferRouterSelect input)
      selectedCommands <- expectSelection selected
      assert
        "query deliberately returns unstable order with an exact duplicate"
        (commandStreamNames selectedCommands == map StreamName ["hospital-hospital-b", "hospital-hospital-a", "hospital-hospital-a"])

      recipientLimit <- expectSelection (mkRecipientLimit 64)
      normalized <- expectSelection (normalizeRecipients recipientLimit selectedCommands)
      assert
        "generated selection normalizes to sorted unique physical targets"
        (commandStreamNames normalized == map StreamName ["hospital-hospital-a", "hospital-hospital-b"])

      first <- expectRight =<< runStore (runDeclarativeRouterOnce defaultRunCommandOptions hospitalTransferRouter sourceEvent input)
      assert "first attempt appends A and B in normalized order" (appendedThenAppended first)

      expectRight =<< runStore (Store.runTransaction (Tx.sql driftRowsSql))
      second <- expectRight =<< runStore (runDeclarativeRouterOnce defaultRunCommandOptions hospitalTransferRouter sourceEvent input)
      assert "redelivery keeps B and appends C" (duplicateThenAppended second)

      mapM_ (assertOneEvent runStore) ["hospital-hospital-a", "hospital-hospital-b", "hospital-hospital-c"]

      let conflictTarget = entityStream hospitalCommandCategory "conflict"
          conflictingCommands =
            [ PMCommand conflictTarget (RouteAcceptedTransferNeed (RouteAcceptedTransferNeedData "transfer-7" "conflict")),
              PMCommand conflictTarget (RouteAcceptedTransferNeed (RouteAcceptedTransferNeedData "transfer-mutated" "conflict"))
            ]
      conflict <-
        expectRight
          =<< runStore
            ( runDeclarativeRouterOnce
                defaultRunCommandOptions
                DeclarativeRouter
                  { name = hospitalTransferRouterName,
                    key = const "transfer-7",
                    selectionContract = hospitalTransferRouterSelectionContract,
                    select = \_ -> pure (Right conflictingCommands),
                    targetEventStream = hospitalEventStream,
                    targetProjections = const []
                  }
                sourceEvent
                input
            )
      assert "conflicting commands fail before dispatch" (isConflict conflict)
      assertNoEvents runStore "hospital-conflict"

      putStrLn "declarative router conformance: PASS"

readModelName :: Text
readModelName = "transfer-routing-hospital-load"

readModelShape :: Text
readModelShape = "fnv1a:3c07a19c552c3547"

initialRowsSql :: ByteString
initialRowsSql =
  """
  CREATE TABLE public.hospital_load (
    hospital_id text NOT NULL,
    region text NOT NULL,
    available_beds integer NOT NULL,
    query_order integer NOT NULL
  );
  INSERT INTO public.hospital_load (hospital_id, region, available_beds, query_order) VALUES
    ('hospital-b', 'west', 1, 1),
    ('hospital-a', 'west', 2, 2),
    ('hospital-a', 'west', 2, 3),
    ('hospital-z', 'west', 0, 4),
    ('hospital-east', 'east', 5, 5);
  """

driftRowsSql :: ByteString
driftRowsSql =
  """
  TRUNCATE public.hospital_load;
  INSERT INTO public.hospital_load (hospital_id, region, available_beds, query_order) VALUES
    ('hospital-c', 'west', 3, 1),
    ('hospital-b', 'west', 1, 2);
  """

sourceEvent :: RecordedEvent
sourceEvent =
  RecordedEvent
    { eventId = EventId sourceUuid,
      eventType = EventType "AcceptedHospitalTransferNeed",
      streamVersion = StreamVersion 1,
      globalPosition = GlobalPosition 1,
      originalStreamId = StreamId 1,
      originalVersion = StreamVersion 1,
      payload = Null,
      metadata = Nothing,
      causationId = Nothing,
      correlationId = Nothing,
      createdAt = UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 0)
    }

sourceUuid :: UUID.UUID
sourceUuid = case UUID.fromString "123e4567-e89b-12d3-a456-426614174000" of
  Just value -> value
  Nothing -> error "invalid static source UUID"

commandStreamNames :: [PMCommand command] -> [StreamName]
commandStreamNames = map (\(PMCommand target _) -> streamName target)

appendedThenAppended :: DeclarativeRouterResult target -> Bool
appendedThenAppended = \case
  DeclarativeSelectionDispatched (RouterResult [PMCommandAppended {}, PMCommandAppended {}]) -> True
  _ -> False

duplicateThenAppended :: DeclarativeRouterResult target -> Bool
duplicateThenAppended = \case
  DeclarativeSelectionDispatched (RouterResult [PMCommandDuplicate {}, PMCommandAppended {}]) -> True
  _ -> False

isConflict :: DeclarativeRouterResult target -> Bool
isConflict = \case
  DeclarativeSelectionFailed SelectionConflictingCommands {} -> True
  _ -> False

assertOneEvent runStore streamNameValue = do
  events <- expectRight =<< runStore (Store.readStreamForward (StreamName streamNameValue) (StreamVersion 0) 10)
  assert ("one event in " <> show streamNameValue) (Vector.length events == 1)

assertNoEvents runStore streamNameValue = do
  events <- expectRight =<< runStore (Store.readStreamForward (StreamName streamNameValue) (StreamVersion 0) 10)
  assert ("no event in " <> show streamNameValue) (Vector.null events)

expectRight :: (Show problem) => Either problem value -> IO value
expectRight = either (error . show) pure

expectSelection :: (Show problem) => Either problem value -> IO value
expectSelection = expectRight

lengthText :: Text -> Int
lengthText = Text.length

assert :: String -> Bool -> IO ()
assert label condition = unless condition (error ("declarative router conformance failed: " <> label))