packages feed

stan-0.0.0.0: src/Stan/Cli.hs

{-# LANGUAGE ApplicativeDo #-}

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

CLI commands and options for @stan@.
-}

module Stan.Cli
    ( StanCommand (..)
    , StanArgs (..)
    , InspectionArgs (..)
    , TomlToCliArgs (..)
    , CliToTomlArgs (..)
    , runStanCli
    , stanParserPrefs
    , stanCliParser
    ) where

import Colourista (blue, bold, formatWith, reset, yellow)
import Colourista.Short (b)
import Data.Char (toUpper)
import Options.Applicative (CommandFields, Mod, Parser, ParserInfo (..), ParserPrefs, auto, columns,
                            command, commandGroup, customExecParser, flag, flag', fullDesc, help,
                            helpLongEquals, helper, hidden, hsubparser, info, infoOption, internal,
                            long, metavar, multiSuffix, option, prefs, progDesc, short,
                            showDefaultWith, showHelpOnEmpty, showHelpOnError, strArgument,
                            strOption, subparserInline, value)
import Options.Applicative.Help.Chunk (stringChunk)
import Trial (TaggedTrial, fiascoOnEmpty)
import Trial.OptparseApplicative (taggedTrialParser)

import Stan.Category (Category (..))
import Stan.Config (Check (..), CheckFilter (..), CheckType (..), ConfigP (..), PartialConfig,
                    Scope (..))
import Stan.Core.Id (Id (..))
import Stan.Info (prettyStanVersion, stanVersion)
import Stan.Inspection (Inspection)
import Stan.Observation (Observation)
import Stan.Report.Settings (ReportSettings (..), ToggleSolution (..), Verbosity (..))


-- | Commands used in Stan CLI.
data StanCommand
    = Stan !StanArgs  -- ^ Just @stan@ with its options.
    | StanInspection !InspectionArgs  -- ^ @stan inspection@.
    | StanTomlToCli !TomlToCliArgs  -- ^ @stan toml-to-cli@
    | StanCliToToml !CliToTomlArgs  -- ^ @stan cli-to-toml@
    | StanInspectionsToMd  -- ^ @stan inspections-to-md@

-- | Options used for the main @stan@ command.
data StanArgs = StanArgs
    { stanArgsHiedir               :: !FilePath  -- ^ Directory with HIE files
    , stanArgsCabalFilePath        :: ![FilePath]  -- ^ Path to @.cabal@ files.
    , stanArgsReportSettings       :: !ReportSettings  -- ^ Settings for report
    , stanArgsReport               :: !Bool  -- ^ Create @HTML@ report?
    , stanArgsUseDefaultConfigFile :: !(TaggedTrial Text Bool)  -- ^ Use default @.stan.toml@ file
    , stanArgsConfigFile           :: !(Maybe FilePath)  -- ^ Path to a custom configurations file.
    , stanArgsConfig               :: !PartialConfig
    }

-- | Options used for the @stan inspection@ command.
newtype InspectionArgs = InspectionArgs
    { inspectionArgsId :: Maybe (Id Inspection)
    }

-- | Options used for the @stan toml-to-cli@ command.
newtype TomlToCliArgs = TomlToCliArgs
    { tomlToCliArgsFilePath :: Maybe FilePath
    }

-- | Options used for the @stan cli-to-toml@ command.
data CliToTomlArgs = CliToTomlArgs
    { cliToTomlArgsFilePath :: !(Maybe FilePath)
    , cliToTomlArgsConfig   :: !PartialConfig
    }

-- | Run main parser of the @stan@ command line tool.
runStanCli :: IO StanCommand
runStanCli = customExecParser stanParserPrefs stanCliParser

-- | To turn on some special options.
stanParserPrefs :: ParserPrefs
stanParserPrefs = prefs $ mconcat
    [ helpLongEquals
    , showHelpOnEmpty
    , showHelpOnError
    , subparserInline
    , multiSuffix "s"
    , columns 100
    ]

stanCliParser :: ParserInfo StanCommand
stanCliParser = modifyHeader $ info (helper <*> versionP <*> stan) fullDesc

{- | Stan tool parser. It either uses the named commands or the main @stan@
command.
-}
stan :: Parser StanCommand
stan =  stanInspectionP
    <|> stanTomlToCliP
    <|> stanCliToTomlP
    <|> stanInspectionsToMd
    <|> stanP

-- | @stan@ command parser.
stanP :: Parser StanCommand
stanP = do
    stanArgsConfig <- configP
    stanArgsReport <- reportP
    stanArgsHiedir <- hiedirP
    stanArgsCabalFilePath <- cabalFilePathP
    stanArgsConfigFile <- configFileP
    stanArgsUseDefaultConfigFile <- useDefaultConfigFileP
    stanArgsReportSettings <- reportSettingsP
    pure $ Stan StanArgs{..}

-- | @stan inspection@ command parser.
stanInspectionP :: Parser StanCommand
stanInspectionP = hsubparser $
    command "inspection" (info inspectionP (progDesc "Show all Inspections"))
    <> commandVar "INSPECTION"
    <> help "Command to show all or specific inspection"
  where
    inspectionP :: Parser StanCommand
    inspectionP = do
        inspectionArgsId <- Id <<$>> optional ( strArgument
            (metavar "INSPECTION_ID" <> help "Show specific Inspection information"))
        pure $ StanInspection InspectionArgs{..}

stanTomlToCliP :: Parser StanCommand
stanTomlToCliP = hsubparser $ commandGroup "TOML Configurations"
    <> command "toml-to-cli"
        ( info tomlToCliP
            (progDesc "Convert TOML configuration file into stan CLI command")
        )
    <> commandVar "TOML-TO-CLI"
    <> help "Command to convert TOML configurations to CLI"
  where
    tomlToCliP :: Parser StanCommand
    tomlToCliP = do
        tomlToCliArgsFilePath <- configFileP
        pure $ StanTomlToCli TomlToCliArgs{..}

stanCliToTomlP :: Parser StanCommand
stanCliToTomlP = hsubparser $ commandGroup "TOML Configurations"
    <> command "cli-to-toml"
        ( info cliToTomlP
            (progDesc "Convert CLI arguments into stan TOML configuration")
        )
    <> commandVar "CLI-TO-TOML"
    <> help "Command to convert CLI configurations to TOML"
  where
    cliToTomlP :: Parser StanCommand
    cliToTomlP = do
        cliToTomlArgsFilePath <- configFileP
        cliToTomlArgsConfig   <- configP
        pure $ StanCliToToml CliToTomlArgs{..}

stanInspectionsToMd :: Parser StanCommand
stanInspectionsToMd = hsubparser
    $  command "inspections-to-md"
        (info (pure StanInspectionsToMd)
             (progDesc "Create md with all inspections info")
        )
    <> help "Create md with all inspections info"
    <> hidden <> internal

hiedirP :: Parser FilePath
hiedirP = strOption $ mconcat
    [ long "hiedir"
    , metavar "DIR_PATH"
    , value ".hie"
    , showDefaultWith (formatWith [blue, bold])
    , help "Relative path to the directory with HIE files"
    ]

cabalFilePathP :: Parser [FilePath]
cabalFilePathP = many $ strOption $ mconcat
    [ long "cabal-file-path"
    , metavar "FILE_PATH"
    , help "Relative path to the .cabal file (can specify many of this option)"
    ]

configFileP :: Parser (Maybe FilePath)
configFileP = optional $ strOption $ mconcat
    [ long "config-file"
    , metavar "FILE_PATH"
    , help "Relative path to the .toml configurations file"
    ]

useDefaultConfigFileP :: Parser (TaggedTrial Text Bool)
useDefaultConfigFileP = taggedTrialParser "no-default" $ flag' False $ mconcat
    [ long "no-default"
    , help "Ignore local .stan.toml configuration file"
    ]

reportP :: Parser Bool
reportP = do
    res <- optional $ hsubparser $
        command "report"
            (info pass (progDesc "Generate HTML Report"))
        <> commandGroup "Reporting"
        <> commandVar "REPORT"
        <> help "Command to generate an HTML Report"
    pure $ isJust res

reportSettingsP :: Parser ReportSettings
reportSettingsP = do
    reportSettingsVerbosity <- verbosityP
    reportSettingsSolutionVerbosity <- toggleSolutionP
    pure ReportSettings{..}

-- | The solution is shown by default and gets hidden when option is specified.
toggleSolutionP :: Parser ToggleSolution
toggleSolutionP = flag ShowSolution HideSolution $ mconcat
    [ long "hide-solution"
    , help "Hide verbose solution information for observations"
    ]

-- | The 'Observation' is shown juicy by default and gets shortened when option is specified.
verbosityP :: Parser Verbosity
verbosityP = flag Verbose NonVerbose $ mconcat
    [ long "short"
    , short 's'
    , help "Hide verbose output information for observations"
    ]

data ConfigCommand
    = CheckCommand !Check
    | RemoveCommand !Scope
    | IgnoreCommand !(Id Observation)

partitionCommands :: [ConfigCommand] -> ([Check], [Scope], [Id Observation])
partitionCommands [] = ([], [], [])
partitionCommands (cmd : rest) =
    let (check, remove, obs) = partitionCommands rest
    in case cmd of
        CheckCommand ch -> (ch:check, remove, obs)
        RemoveCommand r -> (check, r:remove, obs)
        IgnoreCommand o -> (check, remove, o:obs)

configP :: Parser PartialConfig
configP = do
    res <- many $
        cmd "check" "Specify list of checks" CheckCommand checkP
        <|> cmd "remove" "Specify scope to be removed" RemoveCommand scopeP
        <|> cmd "ignore" "Specify list of what needs to be ignored" IgnoreCommand (idP "Observations")
    pure $
        let (checks, removed, ignored) = partitionCommands res
        in ConfigP
            { configChecks  = fiascoOnEmpty "CLI" "checks" checks
            , configRemoved = fiascoOnEmpty "CLI" "remove" removed
            , configIgnored = fiascoOnEmpty "CLI" "ignore" ignored
            }
  where
    cmd :: String -> String -> (a -> ConfigCommand) -> Parser a -> Parser ConfigCommand
    cmd name h cc p = hsubparser
        ( command name
            (info (cc <$> p) (progDesc h))
        <> commandVar (map toUpper name)
        <> help ("Command to " <> h)
        <> commandGroup "CLI Configurations"
        )

-- | Parser of an 'Id'. Receives a string to specify in Help what kind of ID is this.
idP :: String -> Parser (Id a)
idP name = Id <$> strOption
    (long "id"
    <> metavar (map toUpper name <> "_ID")
    <> help (name <> " ID to be used"))

checkP :: Parser Check
checkP = do
    checkType <- checkTypeP
    checkFilter <- checkFilterP
    checkScope  <- scopeP
    pure Check{..}

checkTypeP :: Parser CheckType
checkTypeP =
    -- QUESTION: is it better than --type=Exclude or --type=Include
        flag' Include (long "include" <> help "Include check")
    <|> flag' Exclude (long "exclude" <> help "Exclude check")

checkFilterP :: Parser CheckFilter
checkFilterP =
        CheckInspection <$> idP "Inspection"
    <|> CheckSeverity <$> option auto
        -- TODO: how to specify all possible values here in help?
        (long "severity"
        <> metavar "SEVERITY"
        <> help "Inspection Severity to exclude or include")
    <|> CheckCategory . Category <$> strOption
        (long "category"
        <> metavar "CATEGORY"
        <> help "Inspection Category to exclude or include")
    <|> flag' CheckAll
        (long "filter-all"
        <> help "Exclude or include ALL inspections")

scopeP :: Parser Scope
scopeP =
        ScopeFile <$> strOption
        (long "file"
        <> metavar "FILE_PATH"
        <> help "File to exclude or include")
    <|> ScopeDirectory <$> strOption
        (long "directory"
        <> metavar "DIRECTORY_PATH"
        <> help "Directory to exclude or include")
    <|> flag' ScopeAll
        (long "scope-all"
        <> help "Apply check to all files")

-- | Show the version of the tool.
versionP :: Parser (a -> a)
versionP = infoOption (prettyStanVersion stanVersion)
    $ long "version"
    <> short 'v'
    <> help "Show Stan's version"
    <> hidden

-- to put custom header which doesn't cut all spaces
modifyHeader :: ParserInfo a -> ParserInfo a
modifyHeader p = p { infoHeader = stringChunk $ toString header }

header :: Text
header = unlines
    [ yellow
    , "     ______________    _   __"
    , "    / ___/_  __/   |  / | / /"
    , "    \\__ \\ / / / /| | /  |/ / "
    , "   ___/ // / / ___ |/ /|  /  "
    , "  /____//_/ /_/  |_/_/ |_/   "
    , reset
    , "  Haskell " <> b "ST" <> "atic " <> b "AN" <> "alyser"
    ]

commandVar :: String -> Mod CommandFields a
commandVar = metavar . b