packages feed

keiro-dsl-0.4.0.1: 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.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import Keiro.Dsl.Grammar (HaskellSource (..), Spec)
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
        }
    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
            ]

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"
                    _ -> fail "unknown mapping identity mode"

consumerPlan :: Spec -> ConsumerPlan
consumerPlan spec = case resolveTypeGraph spec of
    Left _ -> ConsumerPlan [] [] []
    Right graph ->
        ConsumerPlan
            { consumerPackages = uniqueSorted [hsPackage (mappedSource declaration) | declaration <- declarations]
            , consumerModules = uniqueSorted (concatMap mappedModules declarations)
            , consumerMappings = sortMappings (map mappingIdentity declarations)
            }
      where
        declarations = Map.elems (tgDeclarations graph)

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)
        }

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