packages feed

summoner-2.2.0.0: src/Summoner/Config.hs

{-# LANGUAGE DataKinds            #-}
{-# LANGUAGE FlexibleContexts     #-}
{-# LANGUAGE FlexibleInstances    #-}
{-# LANGUAGE StandaloneDeriving   #-}
{-# LANGUAGE TypeFamilies         #-}
{-# LANGUAGE TypeOperators        #-}
{-# LANGUAGE UndecidableInstances #-}

{- |
Module                  : Summoner.Config
Copyright               : (c) 2017-2026 Kowainik
SPDX-License-Identifier : MPL-2.0
Maintainer              : Kowainik <xrom.xkov@gmail.com>
Stability               : Stable
Portability             : Portable

Summoner configurations.
-}

module Summoner.Config
       ( ConfigP (..)

       , PartialConfig
       , Config
       , configCodec
       , defaultConfig
       , guessConfigFromGit
       , finalise

       , loadFileConfig
       ) where

import Data.List (lookup)
import Generic.Data (gmappend, gmempty)
import Generic.Data.Orphans ()
import Shellmet (($?), ($|))
import Toml (Key, TomlBiMap, TomlCodec, (.=))
import Validation (Validation (..))

import Summoner.CustomPrelude (CustomPrelude (..), customPreludeT)
import Summoner.Decision (Decision (..))
import Summoner.Default (defaultEmail, defaultFullName, defaultLicenseName, defaultOwner)
import Summoner.GhcVer (GhcVer (..), parseGhcVer, showGhcVer)
import Summoner.License (LicenseName (..), parseLicenseName)
import Summoner.Source (Source, sourceCodec)

import qualified Toml


-- | The phase of the configurations.
data Phase
    = Partial
    | Final

{- | Type family to map 'Phase' to the corresponding field. This is a
Higher-Kinded Data approach specialised to custom enumeration.
-}
infixl 3 :-
type family phase :- field where
    'Partial :- field = Last field
    'Final   :- field = field

-- | Potentially incomplete configuration.
data ConfigP (p :: Phase) = ConfigP
    { cOwner      :: !(p :- Text)
    , cFullName   :: !(p :- Text)
    , cEmail      :: !(p :- Text)
    , cLicense    :: !(p :- LicenseName)
    , cGhcVer     :: !(p :- [GhcVer])
    , cCabal      :: !Decision
    , cStack      :: !Decision
    , cGitHub     :: !Decision
    , cGhActions  :: !Decision
    , cTravis     :: !Decision
    , cAppVey     :: !Decision
    , cPrivate    :: !Decision
    , cLib        :: !Decision
    , cExe        :: !Decision
    , cTest       :: !Decision
    , cBench      :: !Decision
    , cPrelude    :: !(Last CustomPrelude)
    , cExtensions :: ![Text]
    , cGhcOptions :: ![Text]  -- ^ GHC options to add to each stanza
    , cGitignore  :: ![Text]
    , cNoUpload   :: !Any  -- ^ Do not upload to the GitHub (even if enabled)
    , cFiles      :: !(Map FilePath Source)  -- ^ Custom files
    , cBranchName :: !(p :- Text)
    } deriving stock (Generic)

deriving stock instance
    ( Eq (p :- Text)
    , Eq (p :- LicenseName)
    , Eq (p :- [GhcVer])
    ) => Eq (ConfigP p)

deriving stock instance
    ( Show (p :- Text)
    , Show (p :- LicenseName)
    , Show (p :- [GhcVer])
    ) => Show (ConfigP p)

instance Semigroup PartialConfig where
    (<>) = gmappend

instance Monoid PartialConfig where
    mempty  = gmempty
    mappend = (<>)

-- | Incomplete configurations.
type PartialConfig = ConfigP 'Partial

-- | Complete configurations.
type Config = ConfigP 'Final

-- | Default 'Config' configurations.
defaultConfig :: PartialConfig
defaultConfig = ConfigP
    { cOwner        = Last (Just defaultOwner)
    , cFullName     = Last (Just defaultFullName)
    , cEmail        = Last (Just defaultEmail)
    , cLicense      = Last (Just defaultLicenseName)
    , cGhcVer       = Last (Just [])
    , cCabal        = Idk
    , cStack        = Idk
    , cGitHub       = Idk
    , cGhActions    = Idk
    , cTravis       = Idk
    , cAppVey       = Idk
    , cPrivate      = Idk
    , cLib          = Idk
    , cExe          = Idk
    , cTest         = Idk
    , cBench        = Idk
    , cPrelude      = Last Nothing
    , cExtensions   = []
    , cGhcOptions   = []
    , cGitignore    = []
    , cNoUpload     = Any False
    , cFiles        = mempty
    , cBranchName   = Last (Just "main")
    }

-- | Identifies how to read 'Config' data from the @.toml@ file.
configCodec :: TomlCodec PartialConfig
configCodec = ConfigP
    <$> Toml.last Toml.text "owner"         .= cOwner
    <*> Toml.last Toml.text "fullName"      .= cFullName
    <*> Toml.last Toml.text "email"         .= cEmail
    <*> Toml.last license   "license"       .= cLicense
    <*> Toml.last ghcVerArr "ghcVersions"   .= cGhcVer
    <*> decision            "cabal"         .= cCabal
    <*> decision            "stack"         .= cStack
    <*> decision            "github"        .= cGitHub
    <*> decision            "githubActions" .= cGhActions
    <*> decision            "travis"        .= cTravis
    <*> decision            "appveyor"      .= cAppVey
    <*> decision            "private"       .= cPrivate
    <*> decision            "lib"           .= cLib
    <*> decision            "exe"           .= cExe
    <*> decision            "test"          .= cTest
    <*> decision            "bench"         .= cBench
    <*> Toml.last preludeT  "prelude"       .= cPrelude
    <*> textArr             "extensions"    .= cExtensions
    <*> textArr             "ghc-options"   .= cGhcOptions
    <*> textArr             "gitignore"     .= cGitignore
    <*> Toml.any            "noUpload"      .= cNoUpload
    <*> filesCodec          "files"         .= cFiles
    <*> Toml.last Toml.text "branchName"    .= cBranchName
  where
    _GhcVer :: TomlBiMap GhcVer Toml.AnyValue
    _GhcVer = Toml._TextBy showGhcVer (maybeToRight "Wrong GHC version" . parseGhcVer)

    ghcVerArr :: Key -> TomlCodec [GhcVer]
    ghcVerArr = Toml.arrayOf _GhcVer

    license :: Key -> TomlCodec LicenseName
    license = Toml.textBy show (maybeToRight "Wrong license" . parseLicenseName)

    textArr :: Key -> TomlCodec [Text]
    textArr = Toml.dimap Just maybeToMonoid . Toml.dioptional . Toml.arrayOf Toml._Text

    decision :: Key -> TomlCodec Decision
    decision = Toml.dimap fromDecision toDecision . Toml.dioptional . Toml.bool

    decisionMaybe :: [(Decision, Maybe Bool)]
    decisionMaybe = [ (Idk, Nothing)
                    , (Yes, Just True)
                    , (Nop, Just False)
                    ]

    fromDecision :: Decision -> Maybe Bool
    fromDecision d = join $ lookup d decisionMaybe

    toDecision :: Maybe Bool -> Decision
    toDecision m = fromMaybe (error "Impossible") $ lookup m $ map swap decisionMaybe

    preludeT :: Key -> TomlCodec CustomPrelude
    preludeT = Toml.table customPreludeT

    filesCodec :: Key -> TomlCodec (Map FilePath Source)
    filesCodec = Toml.map (Toml.string "path") sourceCodec

{- | Try to retrieve user information from Git config.
Return the 'PartialConfig' with the corresponding filled fields if the
information is applicable.
-}
guessConfigFromGit :: IO PartialConfig
guessConfigFromGit = do
   gitOwner <- (Just <$> "git" $| ["config", "user.login"]) $? pure Nothing
   gitName  <- (Just <$> "git" $| ["config", "user.name"])  $? pure Nothing
   gitEmail <- (Just <$> "git" $| ["config", "user.email"]) $? pure Nothing
   gitBranch <- (Just <$> "git" $| ["config", "init.defaultBranch"]) $? pure Nothing
   pure $ defaultConfig
       { cOwner = Last gitOwner
       , cFullName = Last gitName
       , cEmail = Last gitEmail
       , cBranchName = Last gitBranch
       }

-- | Make sure that all the required configurations options were specified.
finalise :: PartialConfig -> Validation [Text] Config
finalise ConfigP{..} = ConfigP
    <$> fin  "owner"      cOwner
    <*> fin  "fullName"   cFullName
    <*> fin  "email"      cEmail
    <*> fin  "license"    cLicense
    <*> fin  "ghcVersions" cGhcVer
    <*> pure cCabal
    <*> pure cStack
    <*> pure cGitHub
    <*> pure cGhActions
    <*> pure cTravis
    <*> pure cAppVey
    <*> pure cPrivate
    <*> pure cLib
    <*> pure cExe
    <*> pure cTest
    <*> pure cBench
    <*> pure cPrelude
    <*> pure cExtensions
    <*> pure cGhcOptions
    <*> pure cGitignore
    <*> pure cNoUpload
    <*> pure cFiles
    <*> fin "branchName" cBranchName
  where
    fin :: Text -> Last a -> Validation [Text] a
    fin name = maybe (Failure ["Missing field: " <> name]) Success . getLast

-- | Read configuration from the given file and return it in data type.
loadFileConfig :: MonadIO m => FilePath -> m PartialConfig
loadFileConfig = Toml.decodeFile configCodec