packages feed

stan-0.0.1.0: src/Stan/Toml.hs

{-# LANGUAGE ApplicativeDo #-}

{- |
Copyright: (c) 2020 Kowainik
SPDX-License-Identifier: MPL-2.0
Maintainer: Kowainik <xrom.xkov@gmail.com>

@tomland@ library integration. 'TomlCodec's for the 'Config' data type.
-}

module Stan.Toml
    ( getTomlConfig
      -- * Codecs
    , configCodec
      -- * Files
    , usedTomlFiles
    ) where

import Colourista (infoMessage)
import System.Directory (doesFileExist, getCurrentDirectory, getHomeDirectory)
import System.FilePath ((</>))
import Toml (AnyValue, BiMap (..), Key, TomlBiMap, TomlCodec, (.=))
import Trial (TaggedTrial, Trial (..), fiasco)
import Trial.Tomland (taggedTrialListCodec)

import Stan.Category (Category (..))
import Stan.Config (Check (..), CheckFilter (..), CheckType (..), ConfigP (..), PartialConfig,
                    Scope (..))
import Stan.Core.Id (Id (..))
import Stan.Inspection (Inspection (..))
import Stan.Observation (Observation (..))
import Stan.Severity (Severity (..))

import qualified Toml


{- | Based on the incoming settings returns the TOML configuration files that
were used to get the final config.
-}
usedTomlFiles :: Bool -> Maybe FilePath -> IO [FilePath]
usedTomlFiles useDefault mFile = do
    def <-
        if useDefault
        then do
            cur <- defaultCurConfigFile
            ifM (doesFileExist cur) (pure [cur]) $ do
                home <- defaultHomeConfigFile
                memptyIfNotExist home
        else pure []
    custom <- case mFile of
        Nothing -> pure []
        Just f  -> memptyIfNotExist f
    pure $ def <> custom
  where
    memptyIfNotExist :: FilePath -> IO [FilePath]
    memptyIfNotExist fp = ifM (doesFileExist fp) (pure [fp]) (pure [])

getTomlConfig :: Bool -> Bool -> Maybe FilePath -> IO PartialConfig
getTomlConfig isLoud useDefault mTomlFile = do
    def <-
        if useDefault
        then defaultCurConfigFile >>= readToml >>= \case
            Result _ r -> pure r
            resCur -> defaultHomeConfigFile >>= readToml >>= \ resHome ->
                pure $ inline $ resCur <> resHome
        else let e = fiasco "Selected NOT to use any default .stan.toml configuration files"
             in pure $ ConfigP e e e
    case mTomlFile of
        Just tomlFile -> (def <>) . inline <$> readToml tomlFile
        Nothing       -> pure def
  where
    readToml :: FilePath -> IO (Trial Text PartialConfig)
    readToml file = do
        isFile <- doesFileExist file
        if isFile
        then do
            when isLoud $ infoMessage $ "Reading Configurations from " <> toText file <> " ..."
            pure <$> Toml.decodeFile configCodec file
        else pure $ fiasco $ "TOML Configurations file doesn't exist: " <> toText file

    inline :: Trial Text PartialConfig -> PartialConfig
    inline = \case
        Fiasco f     -> let e = Fiasco f in ConfigP e e e
        Result _ res -> res

defaultTomlFile :: FilePath
defaultTomlFile = ".stan.toml"

defaultHomeConfigFile :: IO FilePath
defaultHomeConfigFile = (</> defaultTomlFile) <$> getHomeDirectory

defaultCurConfigFile :: IO FilePath
defaultCurConfigFile = (</> defaultTomlFile) <$> getCurrentDirectory

configCodec :: TomlCodec PartialConfig
configCodec = ConfigP
    <$> checksCodec  .= configChecks
    <*> removedCodec .= configRemoved
    <*> ignoredCodec .= configIgnored

removedCodec :: TomlCodec (TaggedTrial Text [Scope])
removedCodec = taggedTrialListCodec "remove" scopeCodec

ignoredCodec :: TomlCodec (TaggedTrial Text [Id Observation])
ignoredCodec = taggedTrialListCodec "ignore" idCodec

checksCodec :: TomlCodec (TaggedTrial Text [Check])
checksCodec = taggedTrialListCodec "check" checkCodec

checkCodec :: TomlCodec Check
checkCodec = Check
    <$> checkTypeCodec   .= checkType
    <*> checkFilterCodec .= checkFilter
    <*> scopeCodec       .= checkScope

checkTypeCodec :: TomlCodec CheckType
checkTypeCodec = Toml.enumBounded "type"

----------------------------------------------------------------------------
-- CheckFilter
----------------------------------------------------------------------------

checkInspection :: CheckFilter -> Maybe (Id Inspection)
checkInspection = \case
    CheckInspection idI -> Just idI
    _other -> Nothing

checkSeverity :: CheckFilter -> Maybe Severity
checkSeverity = \case
    CheckSeverity sev -> Just sev
    _other -> Nothing

checkCategory :: CheckFilter -> Maybe Category
checkCategory = \case
    CheckCategory category -> Just category
    _other -> Nothing

checkAll :: CheckFilter -> Maybe ()
checkAll = \case
    CheckAll -> Just ()
    _other -> Nothing

checkFilterCodec :: TomlCodec CheckFilter
checkFilterCodec =
        Toml.dimatch checkInspection CheckInspection  idCodec
    <|> Toml.dimatch checkSeverity   CheckSeverity    (Toml.enumBounded "severity")
    <|> Toml.dimatch checkCategory   CheckCategory    (Toml.diwrap (Toml.text "category"))
    <|> Toml.dimatch checkAll        (const CheckAll) (allCodec "filter")

idCodec :: TomlCodec (Id a)
idCodec = Toml.diwrap $ Toml.text "id"

----------------------------------------------------------------------------
-- CheckScope
----------------------------------------------------------------------------

scopeFile :: Scope -> Maybe FilePath
scopeFile = \case
    ScopeFile filePath -> Just filePath
    _other -> Nothing

scopeDir :: Scope -> Maybe FilePath
scopeDir = \case
    ScopeDirectory dir -> Just dir
    _other -> Nothing

scopeAll :: Scope -> Maybe ()
scopeAll = \case
    ScopeAll -> Just ()
    _other -> Nothing

scopeCodec :: TomlCodec Scope
scopeCodec =
        Toml.dimatch scopeFile ScopeFile        (Toml.string "file")
    <|> Toml.dimatch scopeDir  ScopeDirectory   (Toml.string "directory")
    <|> Toml.dimatch scopeAll  (const ScopeAll) (allCodec "scope")

----------------------------------------------------------------------------
-- Helpers
----------------------------------------------------------------------------

-- | Helper 'BiMap' for the hardcoded string @"all"@.
_All :: TomlBiMap () AnyValue
_All = _AllText >>> Toml._Text
  where
    _AllText :: TomlBiMap () Text
    _AllText = BiMap
        { forward  = \() -> Right "all"
        , backward = \case
            "all" -> Right ()
            t -> Left $ Toml.ArbitraryError $ "Expected Text value \"all\" but got: " <> t
        }

allCodec :: Key -> TomlCodec ()
allCodec = Toml.match _All