keiro-dsl-0.12.0.0: src/Keiro/Dsl/ServiceHarness.hs
-- | One generated facade that normalizes every node-level harness in a checked
-- service into executable checks and review-owned facts.
module Keiro.Dsl.ServiceHarness
( DuplicateServiceFactKey (..),
serviceConformanceModuleName,
serviceConformanceFactKeys,
serviceConformanceFactValues,
serviceHarnessModule,
)
where
import Data.List (group, mapAccumL, sort, sortOn)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import Keiro.Dsl.Grammar
import Keiro.Dsl.Harness (processHarnessFactValues, routerHarnessFactValuesForService, workflowHarnessFactValues)
import Keiro.Dsl.LanguageVersion (LanguageFeature (MappedConsumerSurfaceSyntax), languageSupportsFeature)
import Keiro.Dsl.Scaffold (Context, ModuleKind (Generated), ScaffoldModule (..), contextGeneratedPrefix, genPrefixFor, generatedBanner, pascal)
import Keiro.Dsl.SemanticContract (CheckedService, checkedLanguageContract, checkedSpec, checkedTypeGraph, effectiveContractLanguageVersion)
import Keiro.Dsl.SemanticImpact (mappedSurfaceFactValues, semanticImpactForService)
import Keiro.Dsl.StructuralConformance (hasStructuralConformance, structuralConformanceModuleName)
import Keiro.Dsl.Validate (nodeIdentity)
-- | A fully qualified process, router, or workflow fact key that would occur
-- more than once in the generated facade.
newtype DuplicateServiceFactKey = DuplicateServiceFactKey
{ duplicateServiceFactKey :: Text
}
deriving stock (Eq, Ord, Show)
-- | The stable context-level facade module imported by the generated package.
serviceConformanceModuleName :: Context -> Text
serviceConformanceModuleName ctx = contextGeneratedPrefix ctx <> ".Conformance"
-- | Every normalized expectation key in stable node-identity order.
serviceConformanceFactKeys :: CheckedService -> [Text]
serviceConformanceFactKeys service =
map fst (serviceConformanceFactValues service)
-- | The create-once expectation baseline for all facts-producing nodes.
serviceConformanceFactValues :: CheckedService -> [(Text, Text)]
serviceConformanceFactValues service =
surfaceFactValues service <> concatMap (valuesForNode service) (serviceHarnessNodes service)
-- | Emit exactly one facade, including an empty facade for a service with no
-- harness-producing nodes. Duplicate normalized expectation keys are refused
-- before a scaffold write set exists.
serviceHarnessModule :: Context -> CheckedService -> Either [DuplicateServiceFactKey] ScaffoldModule
serviceHarnessModule ctx service = case duplicateKeys of
[] -> Right facade
keys -> Left (map DuplicateServiceFactKey keys)
where
duplicateKeys =
[ key
| key : duplicate : _ <- group (sort (serviceConformanceFactKeys service)),
key == duplicate
]
moduleName = serviceConformanceModuleName ctx
facade =
ScaffoldModule
{ modulePath = T.unpack (T.replace "." "/" moduleName <> ".hs"),
moduleText = renderServiceHarness ctx service,
kind = Generated,
origin = "context " <> specContext (checkedSpec service) <> " service conformance facade"
}
renderServiceHarness :: Context -> CheckedService -> Text
renderServiceHarness ctx service =
T.unlines $
[ generatedBanner,
"module " <> serviceConformanceModuleName ctx,
" ( runServiceConformanceChecks",
" , serviceConformanceFacts",
" ) where"
]
<> importLines
<> [""]
<> renderChecks hasStructural checkSources
<> [""]
<> renderFacts (surfaceFactValues service) factSources
where
hasStructural = hasStructuralConformance service
aliased = aliasNodes hasStructural (serviceHarnessNodes service)
importLines
| null imports = []
| otherwise = "" : imports
imports =
["import " <> structuralConformanceModuleName ctx <> " qualified as StructuralConformance" | hasStructural]
<> map (renderImport ctx) aliased
checkSources = [(node, alias) | (node, alias) <- aliased, producesChecks node]
factSources = [(node, alias) | (node, alias) <- aliased, producesFacts node]
renderImport :: Context -> (Node, Text) -> Text
renderImport ctx (node, alias) =
"import " <> harnessModuleName ctx node <> " qualified as " <> alias
aliasNodes :: Bool -> [Node] -> [(Node, Text)]
aliasNodes reservesStructural nodes = snd (mapAccumL assign initialCounts nodes)
where
initialCounts = Map.fromList [("StructuralConformance", 1 :: Int) | reservesStructural]
assign counts node =
let base = aliasForNode node
occurrence = Map.findWithDefault 0 base counts + 1
alias = base <> if occurrence == (1 :: Int) then "" else T.pack (show occurrence)
in (Map.insert base occurrence counts, (node, alias))
aliasForNode :: Node -> Text
aliasForNode node =
let (_, nodeName, _) = nodeIdentity node
in pascal nodeName
harnessModuleName :: Context -> Node -> Text
harnessModuleName ctx = \case
NAggregate aggregate -> genPrefixFor ctx (aggName aggregate) <> ".Harness"
NProcess process -> genPrefixFor ctx (procId process) <> ".ProcessHarness"
NRouter router -> genPrefixFor ctx (rtId router) <> ".RouterHarness"
NReadModel readModel -> genPrefixFor ctx (pascal (rmName readModel)) <> ".ReadModelHarness"
NWorkflow workflow -> genPrefixFor ctx (wfId workflow) <> ".WorkflowFacts"
node -> error ("service harness requested a module for unsupported node " <> show (nodeIdentity node))
renderChecks :: Bool -> [(Node, Text)] -> [Text]
renderChecks False [] =
[ "runServiceConformanceChecks :: IO [(String, Bool)]",
"runServiceConformanceChecks = pure []"
]
renderChecks hasStructural sources =
[ "runServiceConformanceChecks :: IO [(String, Bool)]",
"runServiceConformanceChecks =",
" pure ("
]
<> renderConcatenation (structuralExpressions <> map checkExpression sources)
<> [" )"]
where
structuralExpressions =
[ "[(\"structural/\" <> fact, passed) | (fact, passed) <- StructuralConformance.structuralConformanceAssertions]"
| hasStructural
]
checkExpression :: (Node, Text) -> Text
checkExpression (node, alias) = case node of
NAggregate aggregate ->
"[(\"aggregate/" <> aggName aggregate <> "/\" <> fact, passed) | (fact, passed) <- " <> alias <> ".harnessAssertions]"
NReadModel readModel ->
"[(\"readmodel/" <> rmName readModel <> "/\" <> fact, passed) | (fact, passed) <- " <> alias <> ".readModelFactResults]"
_ -> error "checkExpression called for a fact-only node"
renderFacts :: [(Text, Text)] -> [(Node, Text)] -> [Text]
renderFacts [] [] =
[ "serviceConformanceFacts :: [(String, String)]",
"serviceConformanceFacts = []"
]
renderFacts surfaceFacts sources =
[ "serviceConformanceFacts :: [(String, String)]",
"serviceConformanceFacts ="
]
<> renderConcatenation (surfaceExpression surfaceFacts <> map factExpression sources)
surfaceExpression :: [(Text, Text)] -> [Text]
surfaceExpression [] = []
surfaceExpression values =
[ "[ " <> T.intercalate "\n , " ["(" <> quoted key <> ", " <> quoted value <> ")" | (key, value) <- values] <> "\n ]"
]
where
quoted = T.pack . show . T.unpack
factExpression :: (Node, Text) -> Text
factExpression (node, alias) =
"[(\"" <> kindName <> "/" <> nodeName <> "/\" <> fact, value) | (fact, value) <- " <> alias <> "." <> valueName <> "]"
where
(kindName, nodeName, _) = nodeIdentity node
valueName = case node of
NProcess {} -> "processHarnessValues"
NRouter {} -> "routerHarnessValues"
NWorkflow {} -> "workflowFactValues"
_ -> error "factExpression called for a check-only node"
renderConcatenation :: [Text] -> [Text]
renderConcatenation expressions = case expressions of
[] -> []
first : rest -> (" " <> first) : [" <> " <> expression | expression <- rest]
serviceHarnessNodes :: CheckedService -> [Node]
serviceHarnessNodes =
sortOn sortKey . filter (\node -> producesChecks node || producesFacts node) . specNodes . checkedSpec
where
sortKey node = let (kindName, nodeName, _) = nodeIdentity node in (kindName, nodeName)
producesChecks :: Node -> Bool
producesChecks NAggregate {} = True
producesChecks NReadModel {} = True
producesChecks _ = False
producesFacts :: Node -> Bool
producesFacts NProcess {} = True
producesFacts NRouter {} = True
producesFacts NWorkflow {} = True
producesFacts _ = False
valuesForNode :: CheckedService -> Node -> [(Text, Text)]
valuesForNode service node =
[(kindName <> "/" <> nodeName <> "/" <> factName, value) | (factName, value) <- factValues]
where
(kindName, nodeName, _) = nodeIdentity node
factValues = case node of
NProcess process -> processHarnessFactValues process
NRouter router -> routerHarnessFactValuesForService service router
NWorkflow workflow -> workflowHarnessFactValues workflow
_ -> []
surfaceFactValues :: CheckedService -> [(Text, Text)]
surfaceFactValues service
| not
( languageSupportsFeature
(effectiveContractLanguageVersion (checkedLanguageContract service))
MappedConsumerSurfaceSyntax
) =
[]
| otherwise = case checkedTypeGraph service of
Left _ -> []
Right graph -> mappedSurfaceFactValues (semanticImpactForService service graph)