keiro-dsl-0.6.0.0: src/Keiro/Dsl/MappedConsumer.hs
-- | One checked projection of mapped declarations for every scaffold
-- integration surface. Keeping dependency requirements and persisted identities
-- together prevents the manifest, preflight report, and scaffold record from
-- silently disagreeing.
module Keiro.Dsl.MappedConsumer
( ConsumerPlan (..),
MappingIdentity (..),
consumerPlan,
)
where
import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.=))
import Data.List (nub, sort)
import Data.List.NonEmpty qualified as NE
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import Keiro.Dsl.Grammar (HaskellSource (..), Spec)
import Keiro.Dsl.NominalType
import Keiro.Dsl.TypeGraph
data ConsumerPlan = ConsumerPlan
{ consumerPackages :: ![Text],
consumerModules :: ![Text],
consumerMappings :: ![MappingIdentity]
}
deriving stock (Eq, Show)
data MappingIdentity
= StructuralMapping
{ mappingSpecName :: !Text,
mappingCanonicalType :: !Text,
mappingPackage :: !Text,
mappingModule :: !Text,
mappingType :: !Text,
mappingBindingSymbol :: !Text,
mappingBindingVersion :: !Text
}
| OpaqueMapping
{ mappingSpecName :: !Text,
mappingPackage :: !Text,
mappingModule :: !Text,
mappingType :: !Text,
mappingCodecIdentity :: !Text,
mappingCodecVersion :: !Text
}
| NominalMapping
{ mappingSpecName :: !Text,
mappingNominalCategory :: !Text,
mappingNominalRepresentation :: !Text,
mappingCanonicalType :: !Text,
mappingPackage :: !Text,
mappingModule :: !Text,
mappingType :: !Text,
mappingBindingSymbol :: !Text,
mappingBindingVersion :: !Text,
mappingFixtureSymbol :: !Text,
mappingInitialSymbol :: !(Maybe Text)
}
deriving stock (Eq, Show)
instance ToJSON MappingIdentity where
toJSON StructuralMapping {mappingSpecName, mappingCanonicalType, mappingPackage, mappingModule, mappingType, mappingBindingSymbol, mappingBindingVersion} =
object
[ "schema" .= (1 :: Int),
"mode" .= ("structural" :: Text),
"specName" .= mappingSpecName,
"canonicalType" .= mappingCanonicalType,
"package" .= mappingPackage,
"module" .= mappingModule,
"type" .= mappingType,
"bindingSymbol" .= mappingBindingSymbol,
"bindingVersion" .= mappingBindingVersion
]
toJSON OpaqueMapping {mappingSpecName, mappingPackage, mappingModule, mappingType, mappingCodecIdentity, mappingCodecVersion} =
object
[ "schema" .= (1 :: Int),
"mode" .= ("opaque" :: Text),
"specName" .= mappingSpecName,
"package" .= mappingPackage,
"module" .= mappingModule,
"type" .= mappingType,
"codecIdentity" .= mappingCodecIdentity,
"codecVersion" .= mappingCodecVersion
]
toJSON NominalMapping {mappingSpecName, mappingNominalCategory, mappingNominalRepresentation, mappingCanonicalType, mappingPackage, mappingModule, mappingType, mappingBindingSymbol, mappingBindingVersion, mappingFixtureSymbol, mappingInitialSymbol} =
object
[ "schema" .= (1 :: Int),
"mode" .= ("nominal" :: Text),
"specName" .= mappingSpecName,
"category" .= mappingNominalCategory,
"representation" .= mappingNominalRepresentation,
"canonicalType" .= mappingCanonicalType,
"package" .= mappingPackage,
"module" .= mappingModule,
"type" .= mappingType,
"bindingSymbol" .= mappingBindingSymbol,
"bindingVersion" .= mappingBindingVersion,
"fixtureSymbol" .= mappingFixtureSymbol,
"initialSymbol" .= mappingInitialSymbol
]
instance FromJSON MappingIdentity where
parseJSON = withObject "keiro-dsl mapping identity" $ \value -> do
schema <- value .: "schema"
if schema /= (1 :: Int)
then fail "unsupported mapping identity schema"
else do
mode <- value .: "mode"
case (mode :: Text) of
"structural" ->
StructuralMapping
<$> value .: "specName"
<*> value .: "canonicalType"
<*> value .: "package"
<*> value .: "module"
<*> value .: "type"
<*> value .: "bindingSymbol"
<*> value .: "bindingVersion"
"opaque" ->
OpaqueMapping
<$> value .: "specName"
<*> value .: "package"
<*> value .: "module"
<*> value .: "type"
<*> value .: "codecIdentity"
<*> value .: "codecVersion"
"nominal" ->
NominalMapping
<$> value .: "specName"
<*> value .: "category"
<*> value .: "representation"
<*> value .: "canonicalType"
<*> value .: "package"
<*> value .: "module"
<*> value .: "type"
<*> value .: "bindingSymbol"
<*> value .: "bindingVersion"
<*> value .: "fixtureSymbol"
<*> value .: "initialSymbol"
_ -> fail "unknown mapping identity mode"
consumerPlan :: Spec -> ConsumerPlan
consumerPlan spec = case (resolveTypeGraph spec, resolveNominalTypes spec) of
(Right graph, Right nominalRegistry) ->
ConsumerPlan
{ consumerPackages = uniqueSorted ([hsPackage (mappedSource declaration) | declaration <- declarations] <> map nominalPackage nominalBindings),
consumerModules = uniqueSorted (concatMap mappedModules declarations <> concatMap nominalModules nominalBindings),
consumerMappings = sortMappings (map mappingIdentity declarations <> map nominalMappingIdentity nominalBindings)
}
where
declarations = Map.elems (tgDeclarations graph)
nominalBindings =
[ (nominal, binding)
| nominal <- Map.elems (nominalTypes nominalRegistry),
ConsumerNominal binding <- [resolvedNominalOwnership nominal]
]
_ -> ConsumerPlan [] [] []
mappedSource :: ResolvedMappedDecl -> HaskellSource
mappedSource (ResolvedStructural declaration _) = sdHaskell declaration
mappedSource (ResolvedOpaque declaration) = odHaskell declaration
mappedModules :: ResolvedMappedDecl -> [Text]
mappedModules (ResolvedStructural declaration _) =
hsModule (sdHaskell declaration)
: qualifiedModule (sdBinding declaration)
: qualifiedModule (sdFixtures declaration)
: maybe [] (pure . qualifiedModule) (sdInitial declaration)
mappedModules (ResolvedOpaque declaration) =
hsModule (odHaskell declaration)
: qualifiedModule (odFixtures declaration)
: maybe [] (pure . qualifiedModule) (odInitial declaration)
mappingIdentity :: ResolvedMappedDecl -> MappingIdentity
mappingIdentity (ResolvedStructural declaration _) =
StructuralMapping
{ mappingSpecName = sdName declaration,
mappingCanonicalType = unCanonicalTypeId (sdCanonical declaration),
mappingPackage = hsPackage (sdHaskell declaration),
mappingModule = hsModule (sdHaskell declaration),
mappingType = hsType (sdHaskell declaration),
mappingBindingSymbol = unQualifiedValueName (sdBinding declaration),
mappingBindingVersion = unBindingVersion (sdBindingVersion declaration)
}
mappingIdentity (ResolvedOpaque declaration) =
OpaqueMapping
{ mappingSpecName = odName declaration,
mappingPackage = hsPackage (odHaskell declaration),
mappingModule = hsModule (odHaskell declaration),
mappingType = hsType (odHaskell declaration),
mappingCodecIdentity = unCodecIdentity (odCodecIdentity declaration),
mappingCodecVersion = unCodecVersion (odCodecVersion declaration)
}
nominalMappingIdentity :: (ResolvedNominalType, ConsumerNominalBinding) -> MappingIdentity
nominalMappingIdentity (nominal, binding) =
NominalMapping
{ mappingSpecName = resolvedNominalName nominal,
mappingNominalCategory = nominalCategory nominal,
mappingNominalRepresentation = nominalRepresentationIdentity nominal,
mappingCanonicalType = unCanonicalTypeId (consumerNominalCanonical binding),
mappingPackage = hsPackage source,
mappingModule = hsModule source,
mappingType = hsType source,
mappingBindingSymbol = unQualifiedValueName (consumerNominalBinding binding),
mappingBindingVersion = unBindingVersion (consumerNominalBindingVersion binding),
mappingFixtureSymbol = unQualifiedValueName (consumerNominalFixtures binding),
mappingInitialSymbol = unQualifiedValueName <$> consumerNominalInitial binding
}
where
source = consumerNominalHaskell binding
nominalPackage :: (ResolvedNominalType, ConsumerNominalBinding) -> Text
nominalPackage (_, binding) = hsPackage (consumerNominalHaskell binding)
nominalModules :: (ResolvedNominalType, ConsumerNominalBinding) -> [Text]
nominalModules (_, binding) =
hsModule (consumerNominalHaskell binding)
: qualifiedModule (consumerNominalBinding binding)
: qualifiedModule (consumerNominalFixtures binding)
: maybe [] (pure . qualifiedModule) (consumerNominalInitial binding)
nominalCategory :: ResolvedNominalType -> Text
nominalCategory nominal = case resolvedNominalRepresentation nominal of
IdRepresentation {} -> "id"
EnumRepresentation {} -> "enum"
ScalarRepresentation {} -> "scalar"
nominalRepresentationIdentity :: ResolvedNominalType -> Text
nominalRepresentationIdentity nominal = case resolvedNominalRepresentation nominal of
IdRepresentation prefix -> "KindID:" <> prefix
EnumRepresentation constructors ->
"enum:" <> T.intercalate "," [constructor <> "=" <> wire | (constructor, wire) <- NE.toList constructors]
ScalarRepresentation representation -> case representation of
NominalText -> "Text"
NominalInt -> "Int"
NominalNatural -> "Natural"
NominalBool -> "Bool"
NominalTime -> "Time"
qualifiedModule :: QualifiedValueName -> Text
qualifiedModule qualified = T.dropEnd 1 (fst (T.breakOnEnd "." (unQualifiedValueName qualified)))
sortMappings :: [MappingIdentity] -> [MappingIdentity]
sortMappings = sortOnName
where
sortOnName [] = []
sortOnName mappings =
[ mapping
| name <- sort (map mappingSpecName mappings),
mapping <- mappings,
mappingSpecName mapping == name
]
uniqueSorted :: [Text] -> [Text]
uniqueSorted = sort . nub