feedback-0.0.0.0: src/Feedback/Common/OptParse.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
module Feedback.Common.OptParse where
import Autodocodec
import Autodocodec.Yaml
import Control.Applicative
import Data.Map (Map)
import qualified Data.Map as M
import Data.Maybe
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Data.Version
import Data.Yaml (FromJSON, ToJSON)
import qualified Env
import GHC.Generics (Generic)
import Options.Applicative as OptParse
import qualified Options.Applicative.Help as OptParse (string)
import Path
import Path.IO
import Paths_feedback
data LoopSettings = LoopSettings
{ loopSettingRunSettings :: !RunSettings,
loopSettingFilterSettings :: !FilterSettings,
loopSettingOutputSettings :: !OutputSettings
}
deriving (Show, Eq, Generic)
combineToLoopSettings :: Flags -> Environment -> Maybe OutputConfiguration -> LoopConfiguration -> IO LoopSettings
combineToLoopSettings Flags {..} Environment {} mDefaultOutputConfig LoopConfiguration {..} = do
loopSettingRunSettings <- combineToRunSettings loopConfigRunConfiguration
let loopSettingFilterSettings = combineToFilterSettings loopConfigFilterConfiguration
let outputConfig = maybe loopConfigOutputConfiguration (<> loopConfigOutputConfiguration) mDefaultOutputConfig
let loopSettingOutputSettings = combineToOutputSettings flagOutputFlags outputConfig
pure LoopSettings {..}
data RunSettings = RunSettings
{ runSettingCommand :: !Command,
runSettingExtraEnv :: !(Map String String),
runSettingWorkingDir :: !(Maybe (Path Abs Dir))
}
deriving (Show, Eq, Generic)
combineToRunSettings :: RunConfiguration -> IO RunSettings
combineToRunSettings RunConfiguration {..} = do
let runSettingCommand = runConfigCommand
let runSettingExtraEnv = runConfigExtraEnv
runSettingWorkingDir <- mapM resolveDir' runConfigWorkingDir
pure RunSettings {..}
data OutputSettings = OutputSettings
{ outputSettingClear :: !Clear
}
deriving (Show, Eq, Generic)
data FilterSettings = FilterSettings
{ filterSettingGitingore :: !Bool,
filterSettingFind :: !(Maybe String)
}
deriving (Show, Eq, Generic)
combineToFilterSettings :: FilterConfiguration -> FilterSettings
combineToFilterSettings FilterConfiguration {..} =
let filterSettingGitingore = fromMaybe True filterConfigGitignore
filterSettingFind = filterConfigFind
in FilterSettings {..}
combineToOutputSettings :: OutputFlags -> OutputConfiguration -> OutputSettings
combineToOutputSettings OutputFlags {..} mConf =
let outputSettingClear = fromMaybe ClearScreen $ outputFlagClear <|> outputConfigClear mConf
in OutputSettings {..}
data Configuration = Configuration
{ configLoops :: !(Map String LoopConfiguration),
configOutputConfiguration :: !(Maybe OutputConfiguration)
}
deriving stock (Show, Eq, Generic)
deriving (FromJSON, ToJSON) via (Autodocodec Configuration)
instance HasCodec Configuration where
codec =
object "Configuration" $
Configuration
<$> optionalFieldWithOmittedDefault' "loops" M.empty .= configLoops
<*> optionalField "output" "default output configuration" .= configOutputConfiguration
data LoopConfiguration = LoopConfiguration
{ loopConfigDescription :: !(Maybe String),
loopConfigRunConfiguration :: !RunConfiguration,
loopConfigFilterConfiguration :: !FilterConfiguration,
loopConfigOutputConfiguration :: !OutputConfiguration
}
deriving stock (Show, Eq, Generic)
deriving (FromJSON, ToJSON) via (Autodocodec LoopConfiguration)
instance HasCodec LoopConfiguration where
codec =
named
"LoopConfiguration"
( dimapCodec f g $
eitherCodec (codec <?> "A bare command without any extra configuration") $
object "LoopConfiguration" loopConfigurationObjectCodec
)
<??> loopConfigDocs
where
loopConfigDocs =
[ "A LoopConfiguration specifies an entire feedback loop.",
"",
"It consists of three parts:",
"* Filter Configuration: Which files to watch",
"* Run Configuration: What to do when those files change",
"* Output Configuration: What to see"
]
f = \case
Left s -> makeLoopConfiguration (CommandArgs s)
Right loopConfig -> loopConfig
g loopConfig =
let runConfig = loopConfigRunConfiguration loopConfig
c = runConfigCommand runConfig
in case c of
CommandArgs cmd | loopConfig == makeLoopConfiguration c -> Left cmd
_ -> Right loopConfig
loopConfigurationObjectCodec :: JSONObjectCodec LoopConfiguration
loopConfigurationObjectCodec =
LoopConfiguration
<$> optionalField "description" "description of when to use this feedback loop" .= loopConfigDescription
<*> parseAlternative
(requiredField "run" "run configuration for this loop")
runConfigurationObjectCodec
.= loopConfigRunConfiguration
<*> parseAlternative
(requiredField "filter" "filter configuration for this loop")
filterConfigurationObjectCodec
.= loopConfigFilterConfiguration
<*> parseAlternative
(requiredField "output" "output configuration for this loop")
outputConfigurationObjectCodec
.= loopConfigOutputConfiguration
makeLoopConfiguration :: Command -> LoopConfiguration
makeLoopConfiguration c =
LoopConfiguration
{ loopConfigDescription = Nothing,
loopConfigRunConfiguration = makeRunConfiguration c,
loopConfigFilterConfiguration = emptyFilterConfiguration,
loopConfigOutputConfiguration = emptyOutputConfiguration
}
data RunConfiguration = RunConfiguration
{ runConfigCommand :: !Command,
runConfigExtraEnv :: !(Map String String),
runConfigWorkingDir :: !(Maybe FilePath)
}
deriving stock (Show, Eq, Generic)
deriving (FromJSON, ToJSON) via (Autodocodec RunConfiguration)
instance HasCodec RunConfiguration where
codec =
named "RunConfiguration" $
object "RunConfiguration" runConfigurationObjectCodec
runConfigurationObjectCodec :: JSONObjectCodec RunConfiguration
runConfigurationObjectCodec =
RunConfiguration
<$> commandObjectCodec .= runConfigCommand
<*> optionalFieldWithOmittedDefault "env" M.empty "extra environment variables to set" .= runConfigExtraEnv
<*> optionalField "working-dir" "where the process will be run" .= runConfigWorkingDir
makeRunConfiguration :: Command -> RunConfiguration
makeRunConfiguration c =
RunConfiguration
{ runConfigCommand = c,
runConfigExtraEnv = M.empty,
runConfigWorkingDir = Nothing
}
data FilterConfiguration = FilterConfiguration
{ filterConfigGitignore :: !(Maybe Bool),
filterConfigFind :: !(Maybe String)
}
deriving stock (Show, Eq, Generic)
deriving (FromJSON, ToJSON) via (Autodocodec FilterConfiguration)
instance HasCodec FilterConfiguration where
codec =
named
"FilterConfiguration"
( object "FilterConfiguration" filterConfigurationObjectCodec
)
<??> filterConfigurationDocs
where
filterConfigurationDocs =
[ "By default, standard filters are applied and,",
"if in a git repository, only files in the git repository are considered.",
"If either 'git' or 'find' configuration are specified, only those are used."
]
filterConfigurationObjectCodec :: JSONObjectCodec FilterConfiguration
filterConfigurationObjectCodec =
FilterConfiguration
<$> optionalField "git" "whether to ignore files that are not in the git repo\nConcretely, this uses `git ls-files` to find files that are in the repo, so files that have been added but are also ignored by .gitignore will still be watched." .= filterConfigGitignore
<*> optionalField "find" "arguments for the 'find' command to find files to be notified about" .= filterConfigFind
emptyFilterConfiguration :: FilterConfiguration
emptyFilterConfiguration =
FilterConfiguration
{ filterConfigGitignore = Nothing,
filterConfigFind = Nothing
}
data OutputConfiguration = OutputConfiguration
{ outputConfigClear :: !(Maybe Clear)
}
deriving stock (Show, Eq, Generic)
deriving (FromJSON, ToJSON) via (Autodocodec OutputConfiguration)
instance HasCodec OutputConfiguration where
codec =
named "OutputConfiguration" $
object "OutputConfiguration" outputConfigurationObjectCodec
outputConfigurationObjectCodec :: JSONObjectCodec OutputConfiguration
outputConfigurationObjectCodec =
OutputConfiguration
<$> optionalField "clear" "whether to clear the screen runs" .= outputConfigClear
instance Semigroup OutputConfiguration where
(<>) oc1 oc2 =
OutputConfiguration
{ outputConfigClear = outputConfigClear oc1 <|> outputConfigClear oc2
}
emptyOutputConfiguration :: OutputConfiguration
emptyOutputConfiguration =
OutputConfiguration
{ outputConfigClear = Nothing
}
getConfiguration :: Flags -> Environment -> IO (Maybe Configuration)
getConfiguration Flags {..} Environment {..} =
case flagConfigFile <|> envConfigFile of
Nothing -> defaultConfigFile >>= readYamlConfigFile
Just cf -> do
afp <- resolveFile' cf
readYamlConfigFile afp
defaultConfigFile :: IO (Path Abs File)
defaultConfigFile = do
here <- getCurrentDir
resolveFile here "feedback.yaml"
data Environment = Environment
{ envConfigFile :: !(Maybe FilePath)
}
deriving (Show, Eq, Generic)
getEnvironment :: IO Environment
getEnvironment = Env.parse (Env.header "Environment") environmentParser
environmentParser :: Env.Parser Env.Error Environment
environmentParser =
Env.prefixed "FEEDBACK_" $
Environment
<$> Env.var (fmap Just . Env.str) "CONFIG_FILE" (Env.def Nothing <> Env.help "Config file")
getFlags :: IO Flags
getFlags = customExecParser prefs_ flagsParser
prefs_ :: OptParse.ParserPrefs
prefs_ =
OptParse.defaultPrefs
{ OptParse.prefShowHelpOnError = True,
OptParse.prefShowHelpOnEmpty = True
}
flagsParser :: OptParse.ParserInfo Flags
flagsParser =
OptParse.info
(OptParse.helper <*> parseFlags)
( mconcat
[ OptParse.progDesc versionStr,
OptParse.fullDesc,
OptParse.footerDoc (Just $ OptParse.string footerStr)
]
)
where
versionStr =
"Version: " <> showVersion version
footerStr =
unlines
[ Env.helpDoc environmentParser,
"",
"Configuration file format:",
T.unpack (TE.decodeUtf8 (renderColouredSchemaViaCodec @Configuration))
]
data Flags = Flags
{ flagConfigFile :: !(Maybe FilePath),
flagCommand :: !String,
flagOutputFlags :: !OutputFlags,
flagDebug :: Bool
}
deriving (Show, Eq, Generic)
data OutputFlags = OutputFlags
{ outputFlagClear :: !(Maybe Clear)
}
deriving (Show, Eq, Generic)
parseFlags :: OptParse.Parser Flags
parseFlags =
Flags
<$> optional
( strOption
( mconcat
[ long "config-file",
help "Path to an altenative config file",
metavar "FILEPATH"
]
)
)
<*> parseCommandFlags
<*> parseOutputFlags
<*> switch (mconcat [long "debug", help "show debug information"])
parseCommandFlags :: OptParse.Parser String
parseCommandFlags =
let commandArg =
strArgument
( mconcat
[ help "The command to run",
metavar "COMMAND"
]
)
in unwords <$> many commandArg
parseOutputFlags :: OptParse.Parser OutputFlags
parseOutputFlags =
OutputFlags
<$> parseClearFlag
data Command
= CommandArgs !String
| CommandScript !String
deriving (Show, Eq, Generic)
commandObjectCodec :: JSONObjectCodec Command
commandObjectCodec =
dimapCodec f g $
eitherCodec
(requiredField "command" "the command to run on change")
(requiredField "script" "the script to run on change")
where
f = \case
Left c -> CommandArgs c
Right s -> CommandScript s
g = \case
CommandArgs c -> Left c
CommandScript s -> Right s
data Clear = ClearScreen | DoNotClearScreen
deriving (Show, Eq, Generic)
instance HasCodec Clear where
codec = dimapCodec f g codec
where
f True = ClearScreen
f False = DoNotClearScreen
g ClearScreen = True
g DoNotClearScreen = False
parseClearFlag :: OptParse.Parser (Maybe Clear)
parseClearFlag =
optional $
flag'
ClearScreen
( mconcat
[ long "clear",
help "clear the screen between feedback"
]
)
<|> flag'
DoNotClearScreen
( mconcat
[ long "no-clear",
help "do not clear the screen between feedback"
]
)