keiro-dsl-0.6.0.0: test/conformance-nominal-scalars/Main.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE TypeApplications #-}
module Main (main) where
import Control.Monad (forM_, unless)
import Data.Aeson (Value (..), object, toJSON, (.=))
import Data.Aeson.Key (Key)
import qualified Data.Aeson.KeyMap as KeyMap
import qualified Data.KindID as KindID
import qualified Data.List.NonEmpty as NE
import Data.Proxy (Proxy (..))
import qualified Generated.NominalScalars.Nominal.Shape.OrderStatus as Representation
import Generated.NominalScalars.NominalLedger.Codec
import Generated.NominalScalars.NominalLedger.Domain
import qualified Generated.NominalScalars.NominalProjections as Projections
import Keiki.Core (Index, evalPred, fieldWitnessAgrees, lit, regProj, (!), (.==), (.>=))
import Keiki.Shape (CanonicalTypeName (..))
import Keiro.Codec (EventType (..), eventType)
import Keiro.Codec.Nominal
import qualified Keiro.EventStream as EventStream
import Keiro.Snapshot.Codec (defaultStateCodec)
import qualified NominalConformance.Bindings as Bindings
import NominalConformance.Domain
import Numeric.Natural (Natural)
import System.Environment (lookupEnv)
import System.Exit (exitFailure)
main :: IO ()
main = do
mutation <- lookupEnv "KEIRO_NOMINAL_MUTATION"
let checks =
bindingLawChecks
<> [ ("expected wire parity: all nominal categories", expectedWireParity)
, ("enum representation covers every constructor and wire spelling exactly once", enumCoverage)
, ("event codec exact JSON bytes", exactEventJson)
, ("event codec round-trip", eventRoundTrip)
, ("pre-adoption valid ord payload still decodes", validHistoricalPayloadDecodes)
, ("wrong-prefix ID rejection", wrongPrefixRejected)
, ("malformed ID rejection", malformedIdRejected)
, ("unknown enum rejection", unknownEnumRejected)
, ("snapshot cache round-trip", snapshotRoundTrip)
, ("canonical nominal identities", canonicalIdentities)
, ("scalar projection witness agreement", projectionAgreement)
, ("scalar equality support", equalityChecks)
, ("Int Natural and Time ordering support", orderingChecks)
, ("forward execution equals decoded replay", forwardReplayAgreement)
]
<> mutationChecks mutation
forM_ checks $ \(label, passed) ->
putStrLn ((if passed then "PASS " else "FAIL ") <> label)
unless (all snd checks) exitFailure
mutationChecks :: Maybe String -> [(String, Bool)]
mutationChecks Nothing = []
mutationChecks (Just "enum-transpose") = [("mutation gate: transposed enum representation preserves expected wire", transposedEnumWireParity)]
mutationChecks (Just "scalar-wire") = [("mutation gate: changed scalar expected wire remains exact", changedScalarWireParity)]
mutationChecks (Just "id-one-direction") = [("mutation gate: one-direction ID suffix preserves domain law", oneDirectionIdLaw)]
mutationChecks (Just other) = [("unknown mutation: " <> other, False)]
transposedEnumWireParity :: Bool
transposedEnumWireParity =
all
( \fixture ->
String (Representation.orderStatusRepresentationText (nominalToRepresentation transposed (nominalFixtureDomain fixture)))
== nominalFixtureWire fixture
)
(NE.toList (nominalFixtureCases Bindings.orderStatusFixtures))
where
transposed =
NominalBinding
{ nominalToRepresentation = \case
AwaitingApproval -> Representation.Submitted
Accepted -> Representation.Draft
, nominalFromRepresentation = \case
Representation.Draft -> Accepted
Representation.Submitted -> AwaitingApproval
}
changedScalarWireParity :: Bool
changedScalarWireParity =
toJSON (nominalToRepresentation Bindings.riskScoreBinding (RiskScore 7)) == toJSON (8 :: Int)
oneDirectionIdLaw :: Bool
oneDirectionIdLaw = case KindID.parseText @"ord" "ord_00041061050r3gg28a1c60t3ge" of
Left _ -> False
Right changed ->
nominalDomainRoundTrip
Bindings.orderIdBinding{nominalFromRepresentation = const (OrderId changed)}
Bindings.sampleOrderId
bindingLawChecks :: [(String, Bool)]
bindingLawChecks =
lawChecks "OrderId" Bindings.orderIdBinding Bindings.orderIdFixtures
<> lawChecks "OrderStatus" Bindings.orderStatusBinding Bindings.orderStatusFixtures
<> lawChecks "AccountNumber" Bindings.accountNumberBinding Bindings.accountNumberFixtures
<> lawChecks "RiskScore" Bindings.riskScoreBinding Bindings.riskScoreFixtures
<> lawChecks "SequenceNumber" Bindings.sequenceNumberBinding Bindings.sequenceNumberFixtures
<> lawChecks "FeatureFlag" Bindings.featureFlagBinding Bindings.featureFlagFixtures
<> lawChecks "ObservedAt" Bindings.observedAtBinding Bindings.observedAtFixtures
lawChecks :: (Eq domain, Eq representation) => String -> NominalBinding domain representation -> NominalFixtureCases domain -> [(String, Bool)]
lawChecks label binding fixtures =
[ ("binding domain law: " <> label, all (nominalDomainRoundTrip binding . nominalFixtureDomain) values)
,
( "binding representation law: " <> label
, all
(\fixture -> nominalRepresentationRoundTrip binding (nominalToRepresentation binding (nominalFixtureDomain fixture)))
values
)
]
where
values = NE.toList (nominalFixtureCases fixtures)
sampleEvent :: NominalLedgerEvent
sampleEvent =
NominalsRecorded
NominalsRecordedData
{ orderId = Bindings.sampleOrderId
, status = Accepted
, accountNumber = AccountNumber "acct-007"
, riskScore = RiskScore 7
, sequenceNumber = SequenceNumber 11
, featureFlag = FeatureFlag True
, observedAt = ObservedAt Bindings.sampleTime
}
expectedEventJson :: Value
expectedEventJson =
object
[ "kind" .= ("NominalsRecorded" :: String)
, "orderId" .= Bindings.validOrderIdText
, "status" .= ("submitted" :: String)
, "accountNumber" .= ("acct-007" :: String)
, "riskScore" .= (7 :: Int)
, "sequenceNumber" .= (11 :: Natural)
, "featureFlag" .= True
, "observedAt" .= Bindings.sampleTime
]
exactEventJson :: Bool
exactEventJson = encodeNominalLedgerEvent sampleEvent == expectedEventJson
eventRoundTrip :: Bool
eventRoundTrip =
parseNominalLedgerEvent (eventType nominalLedgerCodec sampleEvent) (encodeNominalLedgerEvent sampleEvent)
== Right sampleEvent
validHistoricalPayloadDecodes :: Bool
validHistoricalPayloadDecodes =
parseNominalLedgerEvent (EventType "NominalsRecorded") expectedEventJson == Right sampleEvent
wrongPrefixRejected :: Bool
wrongPrefixRejected = rejects (replace "orderId" (String "usr_00041061050r3gg28a1c60t3gf") expectedEventJson)
malformedIdRejected :: Bool
malformedIdRejected = rejects (replace "orderId" (String "not-a-typeid") expectedEventJson)
unknownEnumRejected :: Bool
unknownEnumRejected = rejects (replace "status" (String "retired") expectedEventJson)
rejects :: Value -> Bool
rejects value = case parseNominalLedgerEvent (EventType "NominalsRecorded") value of
Left _ -> True
Right _ -> False
replace :: Key -> Value -> Value -> Value
replace key value (Object fields) = Object (KeyMap.insert key value fields)
replace _ _ other = other
expectedWireParity :: Bool
expectedWireParity =
and
[ wire Bindings.orderIdFixtures (String . KindID.toText . nominalToRepresentation Bindings.orderIdBinding)
, wire Bindings.orderStatusFixtures (String . Representation.orderStatusRepresentationText . nominalToRepresentation Bindings.orderStatusBinding)
, wire Bindings.accountNumberFixtures (toJSON . nominalToRepresentation Bindings.accountNumberBinding)
, wire Bindings.riskScoreFixtures (toJSON . nominalToRepresentation Bindings.riskScoreBinding)
, wire Bindings.sequenceNumberFixtures (toJSON . nominalToRepresentation Bindings.sequenceNumberBinding)
, wire Bindings.featureFlagFixtures (toJSON . nominalToRepresentation Bindings.featureFlagBinding)
, wire Bindings.observedAtFixtures (toJSON . nominalToRepresentation Bindings.observedAtBinding)
]
where
wire fixtures encodeRepresentation =
all
(\fixture -> encodeRepresentation (nominalFixtureDomain fixture) == nominalFixtureWire fixture)
(NE.toList (nominalFixtureCases fixtures))
enumCoverage :: Bool
enumCoverage =
representations == [Representation.Draft, Representation.Submitted]
&& wires == [String "draft", String "submitted"]
where
fixtures = NE.toList (nominalFixtureCases Bindings.orderStatusFixtures)
representations = map (nominalToRepresentation Bindings.orderStatusBinding . nominalFixtureDomain) fixtures
wires = map nominalFixtureWire fixtures
snapshotRoundTrip :: Bool
snapshotRoundTrip = case EventStream.decode codec encoded of
Left _ -> False
Right (vertex, registers) ->
vertex == NominalLedgerEmpty
&& registers ! #orderId == Bindings.initialOrderId
&& registers ! #status == Bindings.initialOrderStatus
&& registers ! #accountNumber == Bindings.initialAccountNumber
&& registers ! #riskScore == Bindings.initialRiskScore
&& registers ! #sequenceNumber == Bindings.initialSequenceNumber
&& registers ! #featureFlag == Bindings.initialFeatureFlag
&& registers ! #observedAt == Bindings.initialObservedAt
&& EventStream.encode codec (vertex, registers) == encoded
where
codec = defaultStateCodec @NominalLedgerRegs @NominalLedgerVertex 1
encoded = EventStream.encode codec (NominalLedgerEmpty, initialNominalLedgerRegs)
canonicalIdentities :: Bool
canonicalIdentities =
and
[ canonicalTypeName (Proxy @OrderId) == "nominal.OrderId.v1"
, canonicalTypeName (Proxy @OrderStatus) == "nominal.OrderStatus.v1"
, canonicalTypeName (Proxy @AccountNumber) == "nominal.AccountNumber.v1"
, canonicalTypeName (Proxy @RiskScore) == "nominal.RiskScore.v1"
, canonicalTypeName (Proxy @SequenceNumber) == "nominal.SequenceNumber.v1"
, canonicalTypeName (Proxy @FeatureFlag) == "nominal.FeatureFlag.v1"
, canonicalTypeName (Proxy @ObservedAt) == "nominal.ObservedAt.v1"
]
projectionAgreement :: Bool
projectionAgreement =
and
[ fieldWitnessAgrees Projections.accountNumberWitness (nominalToRepresentation Bindings.accountNumberBinding) Bindings.initialAccountNumber
, fieldWitnessAgrees Projections.riskScoreWitness (nominalToRepresentation Bindings.riskScoreBinding) Bindings.initialRiskScore
, fieldWitnessAgrees Projections.sequenceNumberWitness (nominalToRepresentation Bindings.sequenceNumberBinding) Bindings.initialSequenceNumber
, fieldWitnessAgrees Projections.featureFlagWitness (nominalToRepresentation Bindings.featureFlagBinding) Bindings.initialFeatureFlag
, fieldWitnessAgrees Projections.observedAtWitness (nominalToRepresentation Bindings.observedAtBinding) Bindings.initialObservedAt
]
equalityChecks :: Bool
equalityChecks =
and
[ evalPred (regProj Projections.accountNumberWitness accountIx .== lit "acct-initial") initialNominalLedgerRegs ()
, evalPred (regProj Projections.riskScoreWitness riskIx .== lit 0) initialNominalLedgerRegs ()
, evalPred (regProj Projections.sequenceNumberWitness sequenceIx .== lit 0) initialNominalLedgerRegs ()
, evalPred (regProj Projections.featureFlagWitness featureIx .== lit False) initialNominalLedgerRegs ()
, evalPred (regProj Projections.observedAtWitness observedIx .== lit Bindings.sampleTime) initialNominalLedgerRegs ()
]
orderingChecks :: Bool
orderingChecks =
and
[ evalPred (regProj Projections.riskScoreWitness riskIx .>= lit 0) initialNominalLedgerRegs ()
, evalPred (regProj Projections.sequenceNumberWitness sequenceIx .>= lit 0) initialNominalLedgerRegs ()
, evalPred (regProj Projections.observedAtWitness observedIx .>= lit Bindings.sampleTime) initialNominalLedgerRegs ()
]
accountIx :: Index NominalLedgerRegs AccountNumber
accountIx = #accountNumber
riskIx :: Index NominalLedgerRegs RiskScore
riskIx = #riskScore
sequenceIx :: Index NominalLedgerRegs SequenceNumber
sequenceIx = #sequenceNumber
featureIx :: Index NominalLedgerRegs FeatureFlag
featureIx = #featureFlag
observedIx :: Index NominalLedgerRegs ObservedAt
observedIx = #observedAt
forwardReplayAgreement :: Bool
forwardReplayAgreement = case parseNominalLedgerEvent (EventType "NominalsRecorded") (encodeNominalLedgerEvent sampleEvent) of
Left _ -> False
Right decoded -> applyEvent decoded == applyEvent sampleEvent
where
applyEvent (NominalsRecorded payload) =
( payload.orderId
, payload.status
, payload.accountNumber
, payload.riskScore
, payload.sequenceNumber
, payload.featureFlag
, payload.observedAt
)