keiro-dsl-0.15.0.0: src/Keiro/Dsl/ExplainBindings.hs
{-# OPTIONS_GHC -Werror=incomplete-patterns #-}
-- | Consumer-owned Haskell obligations implied by checked structural mapped
-- declarations. The same values drive create-once skeletons, scaffold-record
-- diffs, and the @check --explain-bindings@ report.
module Keiro.Dsl.ExplainBindings
( BindingResolutionError (..),
BindingObligationKind (..),
BindingObligation (..),
BindingHole (..),
bindingObligations,
bindingObligationsForService,
bindingHoles,
bindingHolesForService,
renderBindingObligations,
)
where
import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.:?), (.=))
import Data.Bifunctor (first)
import Data.List (groupBy, sortOn)
import Data.List.NonEmpty (NonEmpty)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import Keiro.Dsl.Grammar
import Keiro.Dsl.HaskellName qualified as HaskellName
import Keiro.Dsl.IdDomain (idDomainContractFor, idDomainVersion)
import Keiro.Dsl.NominalType
import Keiro.Dsl.SemanticContract (CheckedService, checkedLanguageContract, checkedSpec, checkedTypeGraph, legacyCheckedService)
import Keiro.Dsl.TypeGraph
data BindingResolutionError
= BindingTypeGraphError !TypeGraphError
| BindingNominalTypeError !NominalTypeError
deriving stock (Eq, Show)
data BindingObligationKind
= BindingValue
| FixtureValue
| InitialValue
deriving stock (Eq, Ord, Show)
data BindingObligation = BindingObligation
{ mappedName :: !Name,
package :: !Text,
moduleName :: !Text,
symbol :: !Text,
kind :: !BindingObligationKind,
signature :: !Text,
useSites :: ![Text],
bindingVersion :: !(Maybe Text),
canonicalType :: !(Maybe Text),
equalityContract :: !(Maybe Text),
idDomainContract :: !(Maybe Text),
category :: !Text
}
deriving stock (Eq, Ord, Show)
data BindingHole = BindingHole
{ mappedName :: !Name,
moduleName :: !Text,
symbol :: !Text,
kind :: !BindingObligationKind,
path :: !(Maybe Text),
signature :: !Text
}
deriving stock (Eq, Ord, Show)
instance ToJSON BindingObligation where
toJSON obligation =
object
[ "schema" .= (1 :: Int),
"mappedName" .= (.mappedName) obligation,
"package" .= (.package) obligation,
"module" .= (.moduleName) obligation,
"symbol" .= (.symbol) obligation,
"kind" .= renderKind ((.kind) obligation),
"signature" .= (.signature) obligation,
"useSites" .= (.useSites) obligation,
"bindingVersion" .= (.bindingVersion) obligation,
"canonicalType" .= (.canonicalType) obligation,
"equalityContract" .= (.equalityContract) obligation,
"idDomainContract" .= (.idDomainContract) obligation,
"category" .= (.category) obligation
]
instance FromJSON BindingObligation where
parseJSON = withObject "keiro-dsl binding obligation" $ \value -> do
schema <- value .: "schema"
if schema /= (1 :: Int)
then fail "unsupported binding obligation schema"
else do
kindText <- value .: "kind"
kindValue <- maybe (fail "unknown binding obligation kind") pure (parseKind kindText)
BindingObligation
<$> value .: "mappedName"
<*> value .: "package"
<*> value .: "module"
<*> value .: "symbol"
<*> pure kindValue
<*> value .: "signature"
<*> value .: "useSites"
<*> value .:? "bindingVersion"
<*> value .:? "canonicalType"
<*> value .:? "equalityContract"
<*> value .:? "idDomainContract"
<*> (value .:? "category" >>= pure . maybe "structural" id)
instance ToJSON BindingHole where
toJSON hole =
object
[ "schema" .= (1 :: Int),
"mappedName" .= (.mappedName) hole,
"module" .= (.moduleName) hole,
"symbol" .= (.symbol) hole,
"kind" .= renderKind ((.kind) hole),
"path" .= (.path) hole,
"signature" .= (.signature) hole
]
instance FromJSON BindingHole where
parseJSON = withObject "keiro-dsl binding hole" $ \value -> do
schema <- value .: "schema"
if schema /= (1 :: Int)
then fail "unsupported binding hole schema"
else do
kindText <- value .: "kind"
kindValue <- maybe (fail "unknown binding hole kind") pure (parseKind kindText)
BindingHole
<$> value .: "mappedName"
<*> value .: "module"
<*> value .: "symbol"
<*> pure kindValue
<*> value .:? "path"
<*> value .: "signature"
bindingObligations :: Spec -> Either (NonEmpty BindingResolutionError) [BindingObligation]
bindingObligations = bindingObligationsForService . legacyCheckedService
bindingObligationsForService :: CheckedService -> Either (NonEmpty BindingResolutionError) [BindingObligation]
bindingObligationsForService service = do
graph <- first (fmap BindingTypeGraphError) (checkedTypeGraph service)
nominalRegistry <- first (fmap BindingNominalTypeError) (resolveNominalTypes spec)
pure . sortOn obligationSortKey $
concat
[ obligationsFor graph declaration
| ResolvedStructural declaration _ <- Map.elems ((.declarations) graph)
]
<> concatMap (nominalObligationsFor service) (Map.elems (nominalTypes nominalRegistry))
where
spec = checkedSpec service
bindingHoles :: Spec -> Either (NonEmpty BindingResolutionError) [BindingHole]
bindingHoles = bindingHolesForService . legacyCheckedService
bindingHolesForService :: CheckedService -> Either (NonEmpty BindingResolutionError) [BindingHole]
bindingHolesForService service = do
graph <- first (fmap BindingTypeGraphError) (checkedTypeGraph service)
obligations <- bindingObligationsForService service
pure . sortOn holeSortKey $
concat
[ holesFor graph declaration shape obligations
| ResolvedStructural declaration shape <- Map.elems ((.declarations) graph)
]
<> [ BindingHole
{ mappedName = (.mappedName) obligation,
moduleName = (.moduleName) obligation,
symbol = (.symbol) obligation,
kind = (.kind) obligation,
path = Nothing,
signature = (.signature) obligation
}
| obligation <- obligations,
(.category) obligation /= "structural"
]
holesFor :: TypeGraph -> StructuralDecl -> ResolvedMappedShape -> [BindingObligation] -> [BindingHole]
holesFor _graph declaration shape obligations = bindingEntries <> auxiliaryEntries
where
own = filter ((== (.name) declaration) . (.mappedName)) obligations
binding = onlyKind BindingValue
bindingEntries = case binding of
Nothing -> []
Just obligation -> map (bindingHole obligation) (shapeHolePaths shape)
auxiliaryEntries =
[ BindingHole
{ mappedName = (.mappedName) obligation,
moduleName = (.moduleName) obligation,
symbol = (.symbol) obligation,
kind = (.kind) obligation,
path = Nothing,
signature = (.signature) obligation
}
| obligation <- own,
(.kind) obligation /= BindingValue
]
onlyKind wanted = case filter ((== wanted) . (.kind)) own of
entry : _ -> Just entry
[] -> Nothing
bindingHole obligation (path, expectedType) =
BindingHole
{ mappedName = (.mappedName) obligation,
moduleName = (.moduleName) obligation,
symbol = (.symbol) obligation,
kind = BindingValue,
path = Just path,
signature = (.symbol) obligation <> "." <> path <> " :: " <> expectedType
}
shapeHolePaths :: ResolvedMappedShape -> [(Text, Text)]
shapeHolePaths =
foldMappedShape
MappedShapeAlgebra
{ onRecord = \_ _ fields -> [((.haskell) field, renderExprType ((.valueType) field)) | field <- fields],
onEnum = \entries -> [((.ctor) entry, "constructor case") | entry <- entries],
onUnion = \_ arms ->
[ ((.ctor) arm, maybe "constructor case" renderExprType ((.payload) arm))
| arm <- arms
]
}
renderExprType :: ResolvedTypeExpr -> Text
renderExprType =
foldTypeExpr
TypeExprAlgebra
{ onText = "Text",
onInt = "Int",
onInteger = "Integer",
onBool = "Bool",
onNatural = "Natural",
onTime = "UTCTime",
onJson = "Value",
onOptional = \value -> "Maybe (" <> value <> ")",
onList = \value -> "[" <> value <> "]",
onMap = \value -> "Map Text (" <> value <> ")",
onRef = unMappedKey
}
obligationsFor :: TypeGraph -> StructuralDecl -> [BindingObligation]
obligationsFor graph declaration = bindingEntry : fixtureEntry : initialEntries
where
source = (.haskell) declaration
consumerType = (.moduleName) source <> "." <> (.valueType) source
shapeType = (.name) declaration <> "Shape"
paths = map renderUsePath (usePaths graph ((.name) declaration))
registerPaths =
[ renderUsePath path
| path@UsePath {root = RootRegister {}} <- usePaths graph ((.name) declaration)
]
bindingEntry =
obligationFor
declaration
((.binding) declaration)
BindingValue
("StructuralBinding " <> consumerType <> " " <> shapeType)
paths
(Just (unBindingVersion ((.bindingVersion) declaration)))
(Just (unCanonicalTypeId ((.canonical) declaration)))
fixtureEntry =
obligationFor
declaration
((.fixtures) declaration)
FixtureValue
("FixtureCases " <> consumerType)
paths
Nothing
(Just (unCanonicalTypeId ((.canonical) declaration)))
initialEntries = case (registerPaths, (.initial) declaration) of
([], _) -> []
(_, Nothing) -> []
(_, Just initialValue) ->
[ obligationFor declaration initialValue InitialValue consumerType registerPaths Nothing (Just (unCanonicalTypeId ((.canonical) declaration)))
]
obligationFor :: StructuralDecl -> QualifiedValueName -> BindingObligationKind -> Text -> [Text] -> Maybe Text -> Maybe Text -> BindingObligation
obligationFor declaration qualified kindValue signature paths version canonical =
BindingObligation
{ mappedName = (.name) declaration,
package = (.package) ((.haskell) declaration),
moduleName = ownerModule,
symbol = symbol,
kind = kindValue,
signature = symbol <> " :: " <> signature,
useSites = paths,
bindingVersion = version,
canonicalType = canonical,
equalityContract = Nothing,
idDomainContract = Nothing,
category = "structural"
}
where
(ownerModule, symbol) = splitQualified (unQualifiedValueName qualified)
nominalObligationsFor :: CheckedService -> ResolvedNominalType -> [BindingObligation]
nominalObligationsFor service nominal = case (.ownership) nominal of
GeneratedNominal -> []
ConsumerNominal binding -> bindingEntry : fixtureEntry : initialEntries
where
name = (.name) nominal
source = (.haskell) binding
consumerType = (.moduleName) source <> "." <> (.valueType) source
paths = useSites spec name
registerPaths = [path | path <- paths, " register " `T.isInfixOf` path]
category = case (.representation) nominal of
IdRepresentation {} -> "nominal-id"
EnumRepresentation {} -> "nominal-enum"
ScalarRepresentation {} -> "nominal-scalar"
representation = case (.representation) nominal of
IdRepresentation prefix -> "(KindID " <> quoted prefix <> ")"
EnumRepresentation {} -> nominalEnumRepresentationModule spec name <> "." <> name <> "Representation"
ScalarRepresentation NominalText -> "Text"
ScalarRepresentation NominalInt -> "Int"
ScalarRepresentation NominalNatural -> "Natural"
ScalarRepresentation NominalBool -> "Bool"
ScalarRepresentation NominalTime -> "UTCTime"
canonical = Just (unCanonicalTypeId ((.canonical) binding))
equalityContract = nominalEqualityIdentityForService (checkedLanguageContract service) nominal
idContract = case (.representation) nominal of
IdRepresentation prefix -> idDomainVersion <$> idDomainContractFor (checkedLanguageContract service) prefix
_ -> Nothing
bindingEntry = nominalObligation name binding category ((.binding) binding) BindingValue ("NominalBinding " <> consumerType <> " " <> representation) paths (Just (unBindingVersion ((.bindingVersion) binding))) canonical equalityContract idContract
fixtureEntry = nominalObligation name binding category ((.fixtures) binding) FixtureValue ("NominalFixtureCases " <> consumerType) paths Nothing canonical Nothing Nothing
initialEntries = case (registerPaths, (.initial) binding) of
([], _) -> []
(_, Nothing) -> []
(_, Just initialValue) -> [nominalObligation name binding category initialValue InitialValue consumerType registerPaths Nothing canonical Nothing Nothing]
where
spec = checkedSpec service
quoted value = T.pack (show value)
nominalObligation :: Name -> ConsumerNominalBinding -> Text -> QualifiedValueName -> BindingObligationKind -> Text -> [Text] -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Text -> BindingObligation
nominalObligation name binding category qualified kindValue signature paths version canonical equalityContract idDomainContract =
BindingObligation
{ mappedName = name,
package = (.package) ((.haskell) binding),
moduleName = ownerModule,
symbol = symbol,
kind = kindValue,
signature = symbol <> " :: " <> signature,
useSites = paths,
bindingVersion = version,
canonicalType = canonical,
equalityContract = equalityContract,
idDomainContract = idDomainContract,
category = category
}
where
(ownerModule, symbol) = splitQualified (unQualifiedValueName qualified)
useSites :: Spec -> Name -> [Text]
useSites spec target = concatMap aggregatePaths [aggregate | NAggregate aggregate <- (.nodes) spec]
where
aggregatePaths aggregate =
[ (.name) aggregate <> " command " <> (.name) command <> " ." <> (.name) field <> " : " <> target
| command <- (.commands) aggregate,
field <- (.fields) command,
fieldUses field
]
<> [ (.name) aggregate <> " event " <> (.name) event <> " ." <> (.name) field <> " : " <> target
| event <- (.events) aggregate,
field <- eventFields aggregate event,
fieldUses field
]
<> [ (.name) aggregate <> " register " <> (.name) register <> " : " <> target
| register <- (.regs) aggregate,
(.valueType) register == TRef target
]
eventFields aggregate event = case (.body) event of
EventFields fields -> fields
EventFromCommand commandName -> concat [(.fields) command | command <- (.commands) aggregate, (.name) command == commandName]
fieldUses field = (.valueType) field == Just (TRef target)
nominalEnumRepresentationModule :: Spec -> Name -> Text
nominalEnumRepresentationModule spec nominalName = case maybe GeneratedPrefix id ((.layout) spec) of
GeneratedPrefix -> root <> "Generated." <> contextModuleName <> ".Nominal.Shape." <> nominalName
CollocatedLeaf -> root <> contextModuleName <> ".Nominal.Shape." <> nominalName <> ".Generated"
where
root = maybe "" (<> ".") ((.moduleRoot) spec)
contextModuleName =
case HaskellName.deriveHaskellName HaskellName.LogicalWireWord site of
Right derived -> HaskellName.renderUpperCamelName ((.upperCamel) derived)
Left _ -> (.context) spec
site =
HaskellName.NameSite
{ HaskellName.kind = HaskellName.ContextModuleSite,
HaskellName.logicalName = (.context) spec,
HaskellName.owner = "binding-obligation-context",
HaskellName.line = 0
}
renderBindingObligations :: Text -> [BindingObligation] -> Text
renderBindingObligations context obligations = case obligations of
[] -> "no binding obligations for context " <> context
_ ->
T.unlines $
["binding obligations for context " <> context]
<> concatMap renderGroup grouped
where
grouped = groupBy sameOwner (sortOn obligationSortKey obligations)
sameOwner left right = ownerKey left == ownerKey right
renderGroup [] = []
renderGroup entries@(firstEntry : _) =
(" " <> (.moduleName) firstEntry <> " (package " <> (.package) firstEntry <> ")")
: concatMap renderEntry entries
renderEntry obligation =
[ " " <> (.signature) obligation,
" reason: " <> renderKind ((.kind) obligation) <> " — " <> (.category) obligation <> " type " <> (.mappedName) obligation <> renderPaths ((.useSites) obligation)
]
<> maybe [] (\version -> [" provenance: binding-version " <> quoted version]) ((.bindingVersion) obligation)
<> maybe [] (\canonical -> [" canonical-type: " <> quoted canonical]) ((.canonicalType) obligation)
<> maybe [] (\contract -> [" equality-contract: " <> quoted contract]) ((.equalityContract) obligation)
<> maybe [] (\contract -> [" id-domain-contract: " <> quoted contract]) ((.idDomainContract) obligation)
renderPaths [] = " (not currently used by an aggregate root)"
renderPaths paths = " (" <> T.intercalate "; " paths <> ")"
quoted value = T.pack (show value)
obligationSortKey :: BindingObligation -> (Text, Text, Text, BindingObligationKind, Text)
obligationSortKey obligation =
( (.package) obligation,
(.moduleName) obligation,
(.mappedName) obligation,
(.kind) obligation,
(.symbol) obligation
)
ownerKey :: BindingObligation -> (Text, Text)
ownerKey obligation = ((.package) obligation, (.moduleName) obligation)
holeSortKey :: BindingHole -> (Text, Name, BindingObligationKind, Maybe Text, Text)
holeSortKey hole =
((.moduleName) hole, (.mappedName) hole, (.kind) hole, (.path) hole, (.symbol) hole)
renderKind :: BindingObligationKind -> Text
renderKind BindingValue = "binding"
renderKind FixtureValue = "fixtures"
renderKind InitialValue = "initial-value"
parseKind :: Text -> Maybe BindingObligationKind
parseKind "binding" = Just BindingValue
parseKind "fixtures" = Just FixtureValue
parseKind "initial-value" = Just InitialValue
parseKind _ = Nothing
splitQualified :: Text -> (Text, Text)
splitQualified value =
let (prefix, name) = T.breakOnEnd "." value
in (T.dropEnd 1 prefix, name)