packages feed

keiro-dsl-0.12.0.0: src/Keiro/Dsl/ReadModelQueryContract.hs

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

-- | Durable identity for the two compile-time query positions of a read model.
-- These rows deliberately exclude SQL columns, codecs, and projection runtime
-- identity: they describe only the generated Haskell API and its mapped closure.
module Keiro.Dsl.ReadModelQueryContract
  ( QueryContractPosition (..),
    QueryContractIdentity (..),
    QueryContractDrift (..),
    queryContractIdentities,
    queryContractIdentitiesForService,
    queryContractIdentityKey,
    queryContractDrift,
  )
where

import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.=))
import Data.List (sortOn)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Map.Strict qualified as Map
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Text (Text)
import Keiro.Dsl.Grammar
import Keiro.Dsl.PrettyPrint (renderTypeExpr)
import Keiro.Dsl.SemanticContract (CheckedService, checkedSpec, checkedTypeGraph, legacyCheckedService)
import Keiro.Dsl.TypeGraph

data QueryContractPosition
  = QueryInputConsumer
  | QueryResultConsumer
  deriving stock (Eq, Ord, Show)

data QueryContractIdentity = QueryContractIdentity
  { qciReadModel :: !Name,
    qciPosition :: !QueryContractPosition,
    qciTypeExpression :: !Text,
    qciMappedDependencies :: ![Name]
  }
  deriving stock (Eq, Ord, Show)

data QueryContractDrift = QueryContractDrift
  { qcdKey :: !(Name, QueryContractPosition),
    qcdPrevious :: !(Maybe QueryContractIdentity),
    qcdCurrent :: !(Maybe QueryContractIdentity)
  }
  deriving stock (Eq, Show)

instance ToJSON QueryContractPosition where
  toJSON QueryInputConsumer = toJSON ("input" :: Text)
  toJSON QueryResultConsumer = toJSON ("result" :: Text)

instance FromJSON QueryContractPosition where
  parseJSON value = do
    label <- parseJSON value
    case (label :: Text) of
      "input" -> pure QueryInputConsumer
      "result" -> pure QueryResultConsumer
      other -> fail ("unknown read-model query contract position: " <> show other)

instance ToJSON QueryContractIdentity where
  toJSON identity =
    object
      [ "readModel" .= qciReadModel identity,
        "position" .= qciPosition identity,
        "typeExpression" .= qciTypeExpression identity,
        "mappedDependencies" .= qciMappedDependencies identity
      ]

instance FromJSON QueryContractIdentity where
  parseJSON = withObject "QueryContractIdentity" $ \fields ->
    QueryContractIdentity
      <$> fields .: "readModel"
      <*> fields .: "position"
      <*> fields .: "typeExpression"
      <*> fields .: "mappedDependencies"

queryContractIdentityKey :: QueryContractIdentity -> (Name, QueryContractPosition)
queryContractIdentityKey identity = (qciReadModel identity, qciPosition identity)

queryContractDrift :: [QueryContractIdentity] -> [QueryContractIdentity] -> [QueryContractDrift]
queryContractDrift current previous =
  [ QueryContractDrift key oldValue newValue
  | key <- Set.toAscList (Map.keysSet oldByKey <> Map.keysSet newByKey),
    let oldValue = Map.lookup key oldByKey,
    let newValue = Map.lookup key newByKey,
    oldValue /= newValue
  ]
  where
    oldByKey = Map.fromList [(queryContractIdentityKey identity, identity) | identity <- previous]
    newByKey = Map.fromList [(queryContractIdentityKey identity, identity) | identity <- current]

queryContractIdentities :: Spec -> Either (NonEmpty TypeGraphError) [QueryContractIdentity]
queryContractIdentities = queryContractIdentitiesForService . legacyCheckedService

queryContractIdentitiesForService :: CheckedService -> Either (NonEmpty TypeGraphError) [QueryContractIdentity]
queryContractIdentitiesForService service = do
  graph <- checkedTypeGraph service
  fmap (sortOn queryContractIdentityKey . concat) (traverse (identitiesFor graph) readModels)
  where
    spec = checkedSpec service
    readModels = [readModel | NReadModel readModel <- specNodes spec]

identitiesFor :: TypeGraph -> ReadModelNode -> Either (NonEmpty TypeGraphError) [QueryContractIdentity]
identitiesFor _ ReadModelNode {queryTypes = Nothing} = Right []
identitiesFor graph readModel@ReadModelNode {queryTypes = Just queryPair} = do
  inputExpression <- resolve QueryInputConsumer (inputLoc queryPair) (input queryPair)
  resultExpression <- resolve QueryResultConsumer (resultLoc queryPair) (result queryPair)
  pure
    [ identity QueryInputConsumer (input queryPair) inputExpression,
      identity QueryResultConsumer (result queryPair) resultExpression
    ]
  where
    resolve position location expression =
      case resolveTypeExpression graph owner location expression of
        Left failure -> Left (failure :| [])
        Right resolved -> Right resolved
      where
        owner = "readmodel '" <> rmName readModel <> "' query " <> positionLabel position
    identity position sourceExpression resolved =
      QueryContractIdentity
        { qciReadModel = rmName readModel,
          qciPosition = position,
          qciTypeExpression = renderTypeExpr sourceExpression,
          qciMappedDependencies = Set.toAscList (Set.map unMappedKey (mappedClosure graph resolved))
        }

mappedClosure :: TypeGraph -> ResolvedTypeExpr -> Set MappedKey
mappedClosure graph =
  foldTypeExpr
    TypeExprAlgebra
      { onText = Set.empty,
        onInt = Set.empty,
        onInteger = Set.empty,
        onBool = Set.empty,
        onNatural = Set.empty,
        onTime = Set.empty,
        onJson = Set.empty,
        onOptional = id,
        onList = id,
        onMap = id,
        onRef = \key -> Set.insert key (Map.findWithDefault Set.empty key (tgReachability graph))
      }

positionLabel :: QueryContractPosition -> Text
positionLabel QueryInputConsumer = "input"
positionLabel QueryResultConsumer = "result"