packages feed

keiro-dsl-0.15.0.0: src/Keiro/Dsl/ConsumerTypePlan.hs

{-# OPTIONS_GHC -Werror=incomplete-patterns #-}

-- | Consumer-facing Haskell type requirements for one checked mapped
-- expression. This module deliberately plans no JSON, SQL, or runtime codec;
-- queue and read-model emitters add their own surface authority around it.
module Keiro.Dsl.ConsumerTypePlan
  ( HaskellTypeOccurrence (..),
    unHaskellTypeOccurrence,
    ImportRequirement (..),
    ConsumerTypePlan (..),
    ConsumerTypePlanError (..),
    planConsumerType,
    consumerTypeReferences,
    renderConsumerType,
  )
where

import Data.Map.Strict qualified as Map
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Text (Text)
import Keiro.Dsl.Grammar (HaskellSource (..))
import Keiro.Dsl.HaskellImport
import Keiro.Dsl.TypeGraph

newtype HaskellTypeOccurrence = HaskellTypeOccurrence
  { unHaskellTypeOccurrence :: Text
  }
  deriving stock (Eq, Ord, Show)

unHaskellTypeOccurrence :: HaskellTypeOccurrence -> Text
unHaskellTypeOccurrence (HaskellTypeOccurrence value) = value

-- | One deterministic import needed to render the planned type. Consumer
-- package provenance is retained so later Cabal planning does not need to
-- rediscover it from the raw specification.
data ImportRequirement = ImportRequirement
  { package :: !Text,
    moduleName :: !Text,
    occurrence :: !Text
  }
  deriving stock (Eq, Ord, Show)

data ConsumerTypePlan = ConsumerTypePlan
  { haskellType :: !HaskellTypeOccurrence,
    imports :: ![ImportRequirement],
    dependencies :: !(Set MappedKey)
  }
  deriving stock (Eq, Show)

data ConsumerTypePlanError
  = ConsumerTypePlanUnknownDeclaration !MappedKey
  | ConsumerTypePlanImportError !HaskellImportError
  deriving stock (Eq, Show)

planConsumerType :: TypeGraph -> ResolvedTypeExpr -> Either ConsumerTypePlanError ConsumerTypePlan
planConsumerType graph expression = do
  RenderedType {rendered, requirements, mappedDependencies} <- plan expression
  pure
    ConsumerTypePlan
      { haskellType = HaskellTypeOccurrence rendered,
        imports = Set.toAscList requirements,
        dependencies = mappedDependencies
      }
  where
    plan = \case
      RText -> atom "Text" [ImportRequirement "text" "Data.Text" "Text"]
      RInt -> atom "Int" []
      RInteger -> atom "Integer" []
      RBool -> atom "Bool" []
      RNatural -> atom "Natural" [ImportRequirement "base" "Numeric.Natural" "Natural"]
      RTime -> atom "UTCTime" [ImportRequirement "time" "Data.Time" "UTCTime"]
      RJson -> atom "Value" [ImportRequirement "aeson" "Data.Aeson" "Value"]
      ROptional value -> application "Maybe" [ImportRequirement "base" "Data.Maybe" "Maybe"] <$> plan value
      RList value -> listType <$> plan value
      RMap value -> mapType <$> plan value
      RRef key -> case Map.lookup key ((.declarations) graph) of
        Nothing -> Left (ConsumerTypePlanUnknownDeclaration key)
        Just declaration ->
          let source = mappedSource declaration
           in Right
                RenderedType
                  { rendered = (.valueType) source,
                    precedence = AtomicType,
                    requirements = Set.singleton (ImportRequirement ((.package) source) ((.moduleName) source) ((.valueType) source)),
                    mappedDependencies = Set.insert key (Map.findWithDefault Set.empty key ((.reachability) graph))
                  }

    atom rendered requiredImports =
      Right
        RenderedType
          { rendered,
            precedence = AtomicType,
            requirements = Set.fromList requiredImports,
            mappedDependencies = Set.empty
          }

    application constructor requiredImports value =
      replaceRenderedType
        (constructor <> " " <> argument value)
        ApplicationType
        (Set.fromList requiredImports <> value.requirements)
        value

    listType value = replaceRenderedType ("[" <> value.rendered <> "]") AtomicType value.requirements value

    mapType value =
      replaceRenderedType
        ("Map Text " <> argument value)
        ApplicationType
        ( Set.fromList
            [ ImportRequirement "containers" "Data.Map.Strict" "Map",
              ImportRequirement "text" "Data.Text" "Text"
            ]
            <> value.requirements
        )
        value

    argument value = case (.precedence) value of
      AtomicType -> (.rendered) value
      ApplicationType -> "(" <> (.rendered) value <> ")"

data TypePrecedence = AtomicType | ApplicationType

data RenderedType = RenderedType
  { rendered :: !Text,
    precedence :: !TypePrecedence,
    requirements :: !(Set ImportRequirement),
    mappedDependencies :: !(Set MappedKey)
  }

mappedSource :: ResolvedMappedDecl -> HaskellSource
mappedSource (ResolvedStructural declaration _) = (.haskell) declaration
mappedSource (ResolvedOpaque declaration) = (.haskell) declaration

consumerTypeReferences :: ConsumerTypePlan -> Set HaskellReference
consumerTypeReferences planned =
  Set.fromList
    [ HaskellReference moduleName occurrence TypeNamespace PreferUnqualified
    | ImportRequirement {package, moduleName, occurrence} <- (.imports) planned,
      package `Set.notMember` standardPackages
    ]
  where
    standardPackages = Set.fromList ["base", "aeson", "containers", "text", "time"]

-- | Render a planned type through the target module's complete deterministic
-- import plan. This is the collision-safe counterpart to 'haskellType', whose
-- unqualified text remains useful for diagnostics and dependency reports.
renderConsumerType :: HaskellImportPlan -> TypeGraph -> ResolvedTypeExpr -> Either ConsumerTypePlanError HaskellTypeOccurrence
renderConsumerType importPlan graph = fmap (HaskellTypeOccurrence . (.rendered)) . render
  where
    render = \case
      RText -> pure (plainAtom "Text")
      RInt -> pure (plainAtom "Int")
      RInteger -> pure (plainAtom "Integer")
      RBool -> pure (plainAtom "Bool")
      RNatural -> pure (plainAtom "Natural")
      RTime -> pure (plainAtom "UTCTime")
      RJson -> pure (plainAtom "Value")
      ROptional value -> application "Maybe" <$> render value
      RList value -> listType <$> render value
      RMap value -> do
        renderedValue <- render value
        pure $
          replaceRenderedType ("Map Text " <> argument renderedValue) ApplicationType renderedValue.requirements renderedValue
      RRef key -> case Map.lookup key ((.declarations) graph) of
        Nothing -> Left (ConsumerTypePlanUnknownDeclaration key)
        Just declaration ->
          let source = mappedSource declaration
           in atom ((.valueType) source) (reference ((.moduleName) source) ((.valueType) source))

    atom _ ref = plainAtom <$> plannedReference ref
    plainAtom value = RenderedType value AtomicType Set.empty Set.empty
    application constructor value = replaceRenderedType (constructor <> " " <> argument value) ApplicationType value.requirements value
    listType value = replaceRenderedType ("[" <> value.rendered <> "]") AtomicType value.requirements value
    argument value = case (.precedence) value of
      AtomicType -> (.rendered) value
      ApplicationType -> "(" <> (.rendered) value <> ")"
    reference moduleName occurrence = HaskellReference moduleName occurrence TypeNamespace PreferUnqualified
    plannedReference = either (Left . ConsumerTypePlanImportError) Right . renderPlannedReference importPlan

replaceRenderedType :: Text -> TypePrecedence -> Set ImportRequirement -> RenderedType -> RenderedType
replaceRenderedType rendered precedence requirements value =
  RenderedType
    { rendered,
      precedence,
      requirements,
      mappedDependencies = value.mappedDependencies
    }