packages feed

etc-0.3.2.0: src/System/Etc/Internal/Resolver/Cli/Common.hs

{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
module System.Etc.Internal.Resolver.Cli.Common where

import qualified Prelude as P

import           RIO
import qualified RIO.ByteString.Lazy as BL (toStrict)
import qualified RIO.Set             as Set
import qualified RIO.Text            as Text
import qualified RIO.Vector          as Vector

import qualified Data.Aeson          as JSON
import qualified Data.Aeson.Internal as JSON (IResult (..), iparse)
import qualified Data.Text.IO        as Text
import qualified Options.Applicative as Opt

import System.Exit

import qualified System.Etc.Internal.Spec.Types as Spec
import           System.Etc.Internal.Types

--------------------------------------------------------------------------------

{-| A wrapper for sub-routines that return an error message; this was
    created for error report purposes.
-}
newtype GetErrorMessage
  = GetErrorMessage {
    -- | Unwrap IO action with error message
    getErrorMessage :: IO Text
  }

instance Show GetErrorMessage where
  show _ = "<<error message>>"

{-| Error when evaluating the ConfigSpec or when executing the OptParser

-}
data CliConfigError
  -- | The type of the Command Key is invalid
  = InvalidCliCommandKey Text
  -- | Trying to use command for an entry without setting commands section
  | CommandsKeyNotDefined
  -- | Trying to use a command that is not defined in commands section
  | UnknownCommandKey Text
  -- | The command setting is missing on a Command Cli
  | CommandKeyMissing
  -- | There is a command setting on a plain Cli
  | CommandKeyOnPlainCli
  -- | The internal OptParser API failed
  | CliEvalExited ExitCode GetErrorMessage
  deriving (Show)

instance Exception CliConfigError

--------------------------------------------------------------------------------

specToCliSwitchFieldMod specSettings =
  maybe Opt.idm (Opt.long . Text.unpack) (Spec.optLong specSettings)
    `mappend` maybe Opt.idm Opt.short                shortOption
    `mappend` maybe Opt.idm (Opt.help . Text.unpack) (Spec.optHelp specSettings)
 where
  shortOption = do
    shortStr <- Spec.optShort specSettings
    fst <$> Text.uncons shortStr

specToCliVarFieldMod specSettings = specToCliSwitchFieldMod specSettings
  `mappend` maybe Opt.idm (Opt.metavar . Text.unpack) (Spec.optMetavar specSettings)


commandToKey :: (MonadThrow m, JSON.ToJSON cmd) => cmd -> m [Text]
commandToKey cmd = case JSON.toJSON cmd of
  JSON.String commandStr -> return [commandStr]
  JSON.Array  jsonList   -> jsonList & Vector.toList & mapM commandToKey & (concat <$>)
  _ ->
    cmd
      & JSON.encode
      & BL.toStrict
      & Text.decodeUtf8'
      & either tshow id
      & InvalidCliCommandKey
      & throwM

settingsToJsonCli :: Bool -> Spec.CliEntryMetadata -> Opt.Parser (Maybe (Value JSON.Value))
settingsToJsonCli sensitive specSettings =
  let requiredCombinator =
        if Spec.optRequired specSettings then (Just <$>) else Opt.optional
  in
    requiredCombinator $ case specSettings of
      Spec.Opt{} -> case Spec.optValueType specSettings of
        Spec.StringOpt -> boolToValue sensitive . JSON.String . Text.pack <$> Opt.strOption
          (specToCliVarFieldMod specSettings)

        Spec.NumberOpt -> boolToValue sensitive . JSON.Number . fromInteger <$> Opt.option
          Opt.auto
          (specToCliVarFieldMod specSettings)

        Spec.SwitchOpt -> boolToValue sensitive . JSON.Bool <$> Opt.switch
          (specToCliSwitchFieldMod specSettings)

      Spec.Arg{} -> case Spec.argValueType specSettings of
        Spec.StringArg ->
          boolToValue sensitive . JSON.String . Text.pack <$> Opt.strArgument
            (specSettings & Spec.argMetavar & maybe Opt.idm (Opt.metavar . Text.unpack))
        Spec.NumberArg ->
          boolToValue sensitive . JSON.Number . fromInteger <$> Opt.argument
            Opt.auto
            (specSettings & Spec.argMetavar & maybe Opt.idm (Opt.metavar . Text.unpack))

parseCommandJsonValue :: (MonadThrow m, JSON.FromJSON a) => JSON.Value -> m a
parseCommandJsonValue commandValue = case JSON.iparse JSON.parseJSON commandValue of
  JSON.IError _path err -> throwM (InvalidCliCommandKey $ Text.pack err)

  JSON.ISuccess result  -> return result

jsonToConfigValue :: Maybe (Value JSON.Value) -> ConfigValue
jsonToConfigValue specEntryDefVal =
  ConfigValue $ Set.fromList $ maybe [] ((: []) . Cli) specEntryDefVal

handleCliResult :: Either SomeException a -> IO a
handleCliResult result = case result of
  Right config -> return config

  Left  err    -> case fromException err of
    Just (CliEvalExited ExitSuccess (GetErrorMessage getMsg)) -> do
      getMsg >>= Text.putStrLn
      exitSuccess

    Just (CliEvalExited exitCode (GetErrorMessage getMsg)) -> do
      getMsg >>= Text.hPutStrLn stderr
      exitWith exitCode

    _ -> throwIO err

programResultToResolverResult :: MonadThrow m => Text -> Opt.ParserResult a -> m a
programResultToResolverResult progName programResult = case programResult of
  Opt.Success result -> return result

  Opt.Failure failure ->
    let (outputMsg, exitCode) = Opt.renderFailure failure $ Text.unpack progName
    in  throwM $ CliEvalExited exitCode (GetErrorMessage $ return (Text.pack outputMsg))

  Opt.CompletionInvoked compl ->
    let getMsg = Text.pack <$> Opt.execCompletion compl (Text.unpack progName)
    in  throwM $ CliEvalExited ExitSuccess (GetErrorMessage getMsg)