packages feed

keiro-dsl-0.2.0.0: src/Keiro/Dsl/ReadModelShape.hs

{- | Deterministic identities derived from a first-class read-model declaration.
The validator, scaffolder, harness, and differ share these functions so captured
fixtures and generated runtime values cannot silently disagree.
-}
module Keiro.Dsl.ReadModelShape (
    canonicalShape,
    deriveShapeHash,
    registryNameFor,
    subscriptionNameFor,
) where

import Data.Bits (shiftR, xor, (.&.), (.|.))
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Word (Word64)
import Keiro.Dsl.Grammar (Name, ReadModelNode (..), RmColumn (..))
import Numeric (showHex)

-- | The ordered table-and-column identity hashed into 'deriveShapeHash'.
canonicalShape :: ReadModelNode -> Text
canonicalShape readModel =
    T.intercalate "|" (rmTable readModel : map columnSegment (rmColumns readModel))
  where
    columnSegment columnDecl =
        T.intercalate
            ":"
            [ rmcName columnDecl
            , rmcType columnDecl
            , if rmcRequired columnDecl then "req" else "null"
            ]

-- | A fixed-width FNV-1a-64 digest over the canonical shape's UTF-8 bytes.
deriveShapeHash :: ReadModelNode -> Text
deriveShapeHash readModel =
    "fnv1a:" <> T.justifyRight 16 '0' (T.pack (showHex digest ""))
  where
    digest = foldl' step offsetBasis (concatMap utf8Bytes (T.unpack (canonicalShape readModel)))
    step hash byte = (hash `xor` byte) * fnvPrime

-- | The runtime registry identity derived from context and notation name.
registryNameFor :: Name -> ReadModelNode -> Text
registryNameFor contextName readModel =
    contextName <> "-" <> T.replace "_" "-" (rmName readModel)

-- | The explicit subscription override or its deterministic default.
subscriptionNameFor :: Name -> ReadModelNode -> Text
subscriptionNameFor contextName readModel =
    fromMaybe (registryNameFor contextName readModel <> "-sub") (rmSubscription readModel)

offsetBasis :: Word64
offsetBasis = 0xcbf29ce484222325

fnvPrime :: Word64
fnvPrime = 0x100000001b3

{- | Encode one Unicode scalar value as UTF-8 bytes, represented as 'Word64'
values so the hash fold needs no bytestring package dependency.
-}
utf8Bytes :: Char -> [Word64]
utf8Bytes character
    | codePoint <= 0x7f = [byte codePoint]
    | codePoint <= 0x7ff =
        [ byte (0xc0 .|. (codePoint `shiftR` 6))
        , continuation codePoint
        ]
    | codePoint <= 0xffff =
        [ byte (0xe0 .|. (codePoint `shiftR` 12))
        , continuation (codePoint `shiftR` 6)
        , continuation codePoint
        ]
    | otherwise =
        [ byte (0xf0 .|. (codePoint `shiftR` 18))
        , continuation (codePoint `shiftR` 12)
        , continuation (codePoint `shiftR` 6)
        , continuation codePoint
        ]
  where
    codePoint = fromEnum character
    byte = fromIntegral
    continuation value = byte (0x80 .|. (value .&. 0x3f))