keiro-dsl-0.4.0.1: 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 (
BindingObligationKind (..),
BindingObligation (..),
BindingHole (..),
bindingObligations,
bindingHoles,
renderBindingObligations,
) where
import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.:?), (.=))
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 (HaskellSource (..), Name, Spec (..), WireEnum (..))
import Keiro.Dsl.TypeGraph
data BindingObligationKind
= BindingValue
| FixtureValue
| InitialValue
deriving stock (Eq, Ord, Show)
data BindingObligation = BindingObligation
{ obligationMappedName :: !Name
, obligationPackage :: !Text
, obligationModule :: !Text
, obligationSymbol :: !Text
, obligationKind :: !BindingObligationKind
, obligationSignature :: !Text
, obligationUseSites :: ![Text]
, obligationBindingVersion :: !(Maybe Text)
}
deriving stock (Eq, Ord, Show)
data BindingHole = BindingHole
{ holeMappedName :: !Name
, holeModule :: !Text
, holeSymbol :: !Text
, holeKind :: !BindingObligationKind
, holePath :: !(Maybe Text)
, holeSignature :: !Text
}
deriving stock (Eq, Ord, Show)
instance ToJSON BindingObligation where
toJSON obligation =
object
[ "schema" .= (1 :: Int)
, "mappedName" .= obligationMappedName obligation
, "package" .= obligationPackage obligation
, "module" .= obligationModule obligation
, "symbol" .= obligationSymbol obligation
, "kind" .= renderKind (obligationKind obligation)
, "signature" .= obligationSignature obligation
, "useSites" .= obligationUseSites obligation
, "bindingVersion" .= obligationBindingVersion 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"
instance ToJSON BindingHole where
toJSON hole =
object
[ "schema" .= (1 :: Int)
, "mappedName" .= holeMappedName hole
, "module" .= holeModule hole
, "symbol" .= holeSymbol hole
, "kind" .= renderKind (holeKind hole)
, "path" .= holePath hole
, "signature" .= holeSignature 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 TypeGraphError) [BindingObligation]
bindingObligations spec = do
graph <- resolveTypeGraph spec
pure . sortOn obligationSortKey . concat $
[ obligationsFor graph declaration
| ResolvedStructural declaration _ <- Map.elems (tgDeclarations graph)
]
bindingHoles :: Spec -> Either (NonEmpty TypeGraphError) [BindingHole]
bindingHoles spec = do
graph <- resolveTypeGraph spec
obligations <- bindingObligations spec
pure . sortOn holeSortKey . concat $
[ holesFor graph declaration shape obligations
| ResolvedStructural declaration shape <- Map.elems (tgDeclarations graph)
]
holesFor :: TypeGraph -> StructuralDecl -> ResolvedMappedShape -> [BindingObligation] -> [BindingHole]
holesFor _graph declaration shape obligations = bindingEntries <> auxiliaryEntries
where
own = filter ((== sdName declaration) . obligationMappedName) obligations
binding = onlyKind BindingValue
bindingEntries = case binding of
Nothing -> []
Just obligation -> map (bindingHole obligation) (shapeHolePaths shape)
auxiliaryEntries =
[ BindingHole
{ holeMappedName = obligationMappedName obligation
, holeModule = obligationModule obligation
, holeSymbol = obligationSymbol obligation
, holeKind = obligationKind obligation
, holePath = Nothing
, holeSignature = obligationSignature obligation
}
| obligation <- own
, obligationKind obligation /= BindingValue
]
onlyKind wanted = case filter ((== wanted) . obligationKind) own of
entry : _ -> Just entry
[] -> Nothing
bindingHole obligation (path, expectedType) =
BindingHole
{ holeMappedName = obligationMappedName obligation
, holeModule = obligationModule obligation
, holeSymbol = obligationSymbol obligation
, holeKind = BindingValue
, holePath = Just path
, holeSignature = obligationSymbol obligation <> "." <> path <> " :: " <> expectedType
}
shapeHolePaths :: ResolvedMappedShape -> [(Text, Text)]
shapeHolePaths =
foldMappedShape
MappedShapeAlgebra
{ onRecord = \_ _ fields -> [(rwfHaskell field, renderExprType (rwfType field)) | field <- fields]
, onEnum = \entries -> [(weCtor entry, "constructor case") | entry <- entries]
, onUnion = \_ arms ->
[ (rwaCtor arm, maybe "constructor case" renderExprType (rwaPayload arm))
| arm <- arms
]
}
renderExprType :: ResolvedTypeExpr -> Text
renderExprType =
foldTypeExpr
TypeExprAlgebra
{ onText = "Text"
, onInt = "Int"
, 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 = sdHaskell declaration
consumerType = hsModule source <> "." <> hsType source
shapeType = sdName declaration <> "Shape"
paths = map renderUsePath (usePaths graph (sdName declaration))
registerPaths =
[ renderUsePath path
| path@UsePath{upRoot = RootRegister{}} <- usePaths graph (sdName declaration)
]
bindingEntry =
obligationFor
declaration
(sdBinding declaration)
BindingValue
("StructuralBinding " <> consumerType <> " " <> shapeType)
paths
(Just (unBindingVersion (sdBindingVersion declaration)))
fixtureEntry =
obligationFor
declaration
(sdFixtures declaration)
FixtureValue
("FixtureCases " <> consumerType)
paths
Nothing
initialEntries = case (registerPaths, sdInitial declaration) of
([], _) -> []
(_, Nothing) -> []
(_, Just initialValue) ->
[ obligationFor declaration initialValue InitialValue consumerType registerPaths Nothing
]
obligationFor :: StructuralDecl -> QualifiedValueName -> BindingObligationKind -> Text -> [Text] -> Maybe Text -> BindingObligation
obligationFor declaration qualified kindValue signature paths version =
BindingObligation
{ obligationMappedName = sdName declaration
, obligationPackage = hsPackage (sdHaskell declaration)
, obligationModule = ownerModule
, obligationSymbol = symbol
, obligationKind = kindValue
, obligationSignature = symbol <> " :: " <> signature
, obligationUseSites = paths
, obligationBindingVersion = version
}
where
(ownerModule, symbol) = splitQualified (unQualifiedValueName qualified)
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@(first : _) =
(" " <> obligationModule first <> " (package " <> obligationPackage first <> ")")
: concatMap renderEntry entries
renderEntry obligation =
[ " " <> obligationSignature obligation
, " reason: " <> renderKind (obligationKind obligation) <> " — structural mapped type " <> obligationMappedName obligation <> renderPaths (obligationUseSites obligation)
]
<> maybe [] (\version -> [" provenance: binding-version " <> quoted version]) (obligationBindingVersion 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 =
( obligationPackage obligation
, obligationModule obligation
, obligationMappedName obligation
, obligationKind obligation
, obligationSymbol obligation
)
ownerKey :: BindingObligation -> (Text, Text)
ownerKey obligation = (obligationPackage obligation, obligationModule obligation)
holeSortKey :: BindingHole -> (Text, Name, BindingObligationKind, Maybe Text, Text)
holeSortKey hole =
(holeModule hole, holeMappedName hole, holeKind hole, holePath hole, holeSymbol 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)