packages feed

keiro-dsl-0.18.0.0: test/conformance-structural-text-sets/Main.hs

module Main (main) where

import Conformance.StructuralTextSets.Bindings (textLabelsFixtures)
import Conformance.StructuralTextSets.Domain
import Conformance.StructuralTextSets.Historical (historicalTextLabelsCodec)
import Control.Monad (forM_, unless)
import Data.Aeson (Value (..), parseJSON)
import Data.Aeson qualified as Aeson
import Data.Aeson.Key qualified as Key
import Data.Aeson.KeyMap qualified as KeyMap
import Data.Aeson.Types (parseEither)
import Data.Either (isLeft)
import Data.List.NonEmpty qualified as NonEmpty
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import Generated.StructuralTextSets.LabelJobs.Queue (LabelJob (..), encodeLabelJob, parseLabelJob)
import Generated.StructuralTextSets.LabelLookup.QueryContract (LabelLookupQueryInput, LabelLookupQueryResult)
import Generated.StructuralTextSets.LabelStore.Codec
import Generated.StructuralTextSets.LabelStore.Domain
import Generated.StructuralTextSets.LabelStore.Harness (harnessAssertions)
import Generated.StructuralTextSets.LabelStore.Transducer (labelStoreTransducer)
import Generated.StructuralTextSets.StructuralConformance (structuralConformanceAssertions)
import Keiki.Core (applyEventsEither, (!))
import Keiro.Codec (EventType (..), eventType)
import Keiro.Codec.Structural qualified
import Keiro.Dsl.CodecCompare (CompareObservation (..), DecodeOutcome (..), FixtureVerdict (..), HistoricalCodec (..), classifyObservation)
import Keiro.Test.ReplayCompatibility (NormalizationFailure (..), checkNormalizationLaw)
import System.Exit (exitFailure)

main :: IO ()
main = do
  let assertions =
        [("structural/" <> label, passed) | (label, passed) <- structuralConformanceAssertions]
          <> harnessAssertions
          <> [ ("bare Set Text writes sorted unique arrays", canonicalTextLabelsWire),
               ("duplicate and permuted arrays normalize before binding", nonCanonicalInputsNormalize),
               ("normalization preserves actual transducer replay state", normalizationReplayLaw),
               ("replay-only history normalizes before reaching the transducer", replayOnlyNormalization),
               ("a first event that loses set information is rejected", headInformationLossRejected),
               ("a list-valued live mutation fails the replay law", listMutationRejected),
               ("a strict duplicate reader requires versioned compatibility work", strictDuplicateReaderRejected),
               ("historical Aeson Set writer and reader remain compatible", historicalCodecParity),
               ("invalid set elements fail at their array position", invalidElementRejected),
               ("root optional set fields treat omission and null alike", rootOptionalAbsenceEquivalence),
               ("non-optional set fields still reject omission", nonOptionalRootStrict),
               ("nested list and map sets round-trip", nestedRoundTrip),
               ("queue payloads preserve structural text sets", queueRoundTrip),
               ("query contracts preserve structural text-set domain types", queryAgreement),
               ("application-owned workflow codecs normalize through the same set domain", workflowCodecRoundTrip)
             ]
  forM_ assertions $ \(label, passed) ->
    putStrLn ((if passed then "PASS  " else "FAIL  ") <> label)
  unless (all snd assertions) exitFailure

canonicalTextLabelsWire :: Bool
canonicalTextLabelsWire =
  encodeTextLabelsMapped value == Aeson.toJSON (["A", "a", "a\x0308", "ä", "\xE000", "\x10000"] :: [Text])
  where
    value = TextLabels (Set.fromList ["\x10000", "\xE000", "ä", "a\x0308", "a", "A"])

nonCanonicalInputsNormalize :: Bool
nonCanonicalInputsNormalize =
  decodeTextLabelsMapped nonCanonical == Right expected
    && decodeTextLabelsMapped canonical == Right expected
    && fmap encodeTextLabelsMapped (decodeTextLabelsMapped nonCanonical) == Right canonical
  where
    nonCanonical = Aeson.toJSON (["b", "a", "a"] :: [Text])
    canonical = Aeson.toJSON (["a", "b"] :: [Text])
    expected = TextLabels (Set.fromList ["a", "b"])

normalizationReplayLaw :: Bool
normalizationReplayLaw =
  null
    ( checkNormalizationLaw
        decodeTextLabelsMapped
        encodeTextLabelsMapped
        replayRawSetChain
        []
        (Aeson.toJSON (["b", "a", "a"] :: [Text]))
        (Aeson.toJSON (["a", "b"] :: [Text]))
        []
    )

-- The replay function receives the raw set array, inserts it into both events
-- of the generated transition, crosses the generated event parser, and then
-- applies the real generated transducer. Its observation includes control state
-- and the durable register, not merely the decoded set.
replayRawSetChain :: [Value] -> Either Text (LabelStoreVertex, TextLabels)
replayRawSetChain wires = do
  events <- concat <$> traverse rawTransition wires
  (vertex, registers) <- firstText (applyEventsEither labelStoreTransducer (LabelStoreEmpty, initialLabelStoreRegs) events)
  pure (vertex, registers ! #current)
  where
    rawTransition wire = do
      let labels = TextLabels Set.empty
          optional = MaybeTextLabels Nothing
          envelope = sampleEnvelope
          stored = LabelsStored (LabelsStoredData labels optional envelope)
          audited = LabelsAudited (LabelsAuditedData labels optional envelope)
      storedEvent <- parseLabelStoreEvent (EventType "LabelsStored") (replaceObjectField "labels" wire (encodeLabelStoreEvent stored))
      auditedEvent <- parseLabelStoreEvent (EventType "LabelsAudited") (replaceObjectField "labels" wire (encodeLabelStoreEvent audited))
      pure [storedEvent, auditedEvent]

replayOnlyNormalization :: Bool
replayOnlyNormalization =
  case parseLabelStoreEvent (EventType "LegacyLabelsImported") rawEvent of
    Left _ -> False
    Right parsedEvent ->
      case applyEventsEither labelStoreTransducer (LabelStoreEmpty, initialLabelStoreRegs) [parsedEvent] of
        Left _ -> False
        Right (vertex, registers) ->
          vertex == LabelStoreStored
            && registers ! #current == TextLabels (Set.fromList ["a", "b"])
  where
    emptyLabels = TextLabels Set.empty
    sampleEvent = LegacyLabelsImported (LegacyLabelsImportedData emptyLabels (MaybeTextLabels Nothing) sampleEnvelope)
    rawEvent = replaceObjectField "labels" (Aeson.toJSON (["b", "a", "a"] :: [Text])) (encodeLabelStoreEvent sampleEvent)

headInformationLossRejected :: Bool
headInformationLossRejected =
  isLeft (parseLabelStoreEvent (EventType "LabelsStored") (deleteObjectField "labels" (encodeLabelStoreEvent event)))
  where
    event = LabelsStored (LabelsStoredData (TextLabels (Set.fromList ["a", "b"])) (MaybeTextLabels Nothing) sampleEnvelope)

listMutationRejected :: Bool
listMutationRejected =
  let failures =
        checkNormalizationLaw
          decodeTextList
          encodeDeduplicatedList
          replayTextList
          []
          (Aeson.toJSON (["b", "a", "a"] :: [Text]))
          (Aeson.toJSON (["a", "b"] :: [Text]))
          []
   in DomainRoundTripFailed `elem` failures
        && NormalizedReplayDiverged `elem` failures
  where
    decodeTextList :: Value -> Either Text [Text]
    decodeTextList = firstText . parseEither parseJSON
    encodeDeduplicatedList :: [Text] -> Value
    encodeDeduplicatedList = Aeson.toJSON . Set.toAscList . Set.fromList
    replayTextList :: [Value] -> Either Text [Text]
    replayTextList values = case traverse decodeTextList values of
      Left problem -> Left problem
      Right [] -> Left "empty mutation trace"
      Right decoded -> Right (last decoded)

strictDuplicateReaderRejected :: Bool
strictDuplicateReaderRejected =
  case
      classifyObservation
        ( DecodeObservation
            "duplicate-text-set.json"
            (Aeson.toJSON (["b", "a", "a"] :: [Text]))
            (DecodedShape (Aeson.toJSON (["a", "b"] :: [Text])))
            (DecodeFailed "candidate reader rejected a duplicate accepted by v1")
        )
    of
    Right (RequiresVersionWork _) -> True
    _ -> False

historicalCodecParity :: Bool
historicalCodecParity =
  all
    ( \(label, value) ->
        classifyObservation (EncodeObservation label (historicalTextLabelsCodec.encode value) (encodeTextLabelsMapped value)) == Right JsonParity
    )
    (NonEmpty.toList (Keiro.Codec.Structural.fixtureCases textLabelsFixtures))
    && historicalTextLabelsCodec.decode (Aeson.toJSON (["b", "a", "a"] :: [Text]))
      == Right (TextLabels (Set.fromList ["a", "b"]))

invalidElementRejected :: Bool
invalidElementRejected =
  case decodeTextLabelsMapped (Aeson.toJSON ([String "a", Number 1] :: [Value])) of
    Left problem -> "[1]" `T.isInfixOf` problem
    Right _ -> False

-- | Plan 295 found both spellings of absence in retained history -- omitted keys
-- and explicit nulls -- which historical Aeson decoding had always read as
-- 'Nothing'. A root 'Optional' mapped set field must therefore accept an omitted
-- key and decode it exactly as an explicit null.
rootOptionalAbsenceEquivalence :: Bool
rootOptionalAbsenceEquivalence =
  parseLabelStoreEvent kind (deleteObjectField "optionalLabels" encoded) == Right event
    && parseLabelStoreEvent kind (replaceObjectField "optionalLabels" Null encoded) == Right event
  where
    event = LabelsStored (LabelsStoredData labels (MaybeTextLabels Nothing) sampleEnvelope)
    kind = eventType labelStoreCodec event
    encoded = encodeLabelStoreEvent event
    labels = TextLabels (Set.fromList ["a", "b"])

-- | The equivalence above is scoped to root 'Optional' mapped fields. A
-- non-optional mapped root carries no absent spelling, so omitting it stays a
-- decode failure rather than defaulting.
nonOptionalRootStrict :: Bool
nonOptionalRootStrict =
  isLeft (parseLabelStoreEvent kind (deleteObjectField "labels" encoded))
    && isLeft (parseLabelStoreEvent kind (deleteObjectField "envelope" encoded))
  where
    event = LabelsStored (LabelsStoredData labels (MaybeTextLabels Nothing) sampleEnvelope)
    kind = eventType labelStoreCodec event
    encoded = encodeLabelStoreEvent event
    labels = TextLabels (Set.fromList ["a", "b"])

nestedRoundTrip :: Bool
nestedRoundTrip = decodeLabelEnvelopeMapped (encodeLabelEnvelopeMapped sampleEnvelope) == Right sampleEnvelope

queueRoundTrip :: Bool
queueRoundTrip = parseLabelJob (encodeLabelJob payload) == Right payload
  where
    payload = LabelJob (TextLabels (Set.fromList ["a", "b"])) (MaybeTextLabels Nothing) sampleEnvelope

queryAgreement :: Bool
queryAgreement =
  queryInputIdentity (MaybeTextLabels (Just (Set.fromList ["a", "b"]))) == MaybeTextLabels (Just (Set.fromList ["a", "b"]))
    && queryResultIdentity sampleEnvelope == sampleEnvelope

queryInputIdentity :: LabelLookupQueryInput -> MaybeTextLabels
queryInputIdentity = id

queryResultIdentity :: LabelLookupQueryResult -> LabelEnvelope
queryResultIdentity = id

-- Workflows remain application-owned. This explicit example demonstrates the
-- required codec property without implying that the DSL owns workflow journals.
workflowCodecRoundTrip :: Bool
workflowCodecRoundTrip =
  parseEither parseJSON (Aeson.toJSON value) == Right value
    && parseEither parseJSON (Aeson.toJSON (["b", "a", "a"] :: [Text])) == Right value
  where
    value = TextLabels (Set.fromList ["a", "b"])

sampleEnvelope :: LabelEnvelope
sampleEnvelope =
  LabelEnvelope
    (Set.fromList ["b", "a"])
    (Just (Set.fromList ["z", "y"]))
    (MaybeTextLabels (Just (Set.fromList ["named", "optional"])))
    [Set.fromList ["second", "first"], Set.empty]
    (Map.fromList [("unicode", Set.fromList ["\x10000", "\xE000", "a\x0308", "ä"])])

firstText :: (Show problem) => Either problem value -> Either Text value
firstText = either (Left . T.pack . show) Right

deleteObjectField :: Key.Key -> Value -> Value
deleteObjectField key (Object value) = Object (KeyMap.delete key value)
deleteObjectField _ value = value

replaceObjectField :: Key.Key -> Value -> Value -> Value
replaceObjectField key inserted (Object value) = Object (KeyMap.insert key inserted value)
replaceObjectField _ _ value = value