packages feed

etc-0.0.0.0: src/System/Etc/Internal/Resolver/Cli/Command.hs

{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
module System.Etc.Internal.Resolver.Cli.Command (resolveCommandCli, resolveCommandCliPure) where

import Protolude

import Control.Monad.Catch (MonadThrow, throwM)
import Data.Hashable       (Hashable)
import Data.HashMap.Strict (HashMap)
import Data.Maybe          (fromMaybe)
import Data.Vector         (Vector)
import System.Environment  (getArgs, getProgName)

import qualified Data.Aeson          as JSON
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Text           as Text
import qualified Options.Applicative as Opt

import System.Etc.Internal.Resolver.Cli.Common
import System.Etc.Internal.Types

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

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

entrySpecToJsonCli
  :: (MonadThrow m)
    => Spec.CliEntrySpec cmd
    -> m (Vector cmd, Opt.Parser (Maybe JSON.Value))
entrySpecToJsonCli entrySpec =
  case entrySpec of
    Spec.CmdEntry commandJsonValue specSettings ->
      return ( commandJsonValue
             , settingsToJsonCli specSettings
             )

    Spec.PlainEntry {} ->
      throwM CommandKeyMissing

configValueSpecToCli
  :: (MonadThrow m, Eq cmd, Hashable cmd)
    => HashMap cmd (Opt.Parser ConfigValue)
    -> Text
    -> Spec.ConfigSources cmd
    -> m (HashMap cmd (Opt.Parser ConfigValue))
configValueSpecToCli acc0 specEntryKey sources =
  let
    updateAccConfigOptParser configValueParser accOptParser =
      (\configValue accSubConfig ->
        case accSubConfig of
          ConfigValue {} ->
            accSubConfig

          SubConfig subConfigMap ->
            subConfigMap
              & HashMap.alter (const $ Just configValue) specEntryKey
              & SubConfig)
        <$> configValueParser
        <*> accOptParser
  in
    case Spec.cliEntry sources of
      Nothing ->
        return acc0

      Just entrySpec -> do
        (commands, jsonOptParser) <-
          entrySpecToJsonCli entrySpec

        let
          configValueParser =
            jsonToConfigValue <$> jsonOptParser

        foldM (\acc command ->
                 acc
                   & HashMap.alter
                        (\mAccParser ->
                          mAccParser
                            & fromMaybe (pure $ SubConfig HashMap.empty)
                            & updateAccConfigOptParser configValueParser
                            & Just)
                        command
                   & return)
              acc0
              commands

subConfigSpecToCli
  :: (MonadThrow m, JSON.FromJSON cmd, JSON.ToJSON cmd, Eq cmd, Hashable cmd)
    => Text
    -> HashMap.HashMap Text (Spec.ConfigValue cmd)
    -> HashMap cmd (Opt.Parser ConfigValue)
    -> m (HashMap cmd (Opt.Parser ConfigValue))
subConfigSpecToCli specEntryKey subConfigSpec acc =
  let
    updateAccConfigOptParser subConfigParser accOptParser =
      (\subConfig accSubConfig ->
          case accSubConfig of
            ConfigValue {} ->
              accSubConfig

            SubConfig subConfigMap ->
              subConfigMap
                & HashMap.alter (const $ Just subConfig) specEntryKey
                & SubConfig)
        <$> subConfigParser
        <*> accOptParser

    addSubParserCommand command subConfigParser =
      HashMap.alter
        (\mAccOptParser ->
           case mAccOptParser of
             Nothing -> do
               commandText <- commandToKey command
               throwM $ UnknownCommandKey (Text.intercalate ", " commandText)

             Just accOptParser ->
               Just
               $ updateAccConfigOptParser subConfigParser accOptParser)
        command

  in do
     parserPerCommand <-
       foldM specToConfigValueCli
             HashMap.empty
             (HashMap.toList subConfigSpec)

     parserPerCommand
       & HashMap.foldrWithKey
                  addSubParserCommand
                  acc
       & return

specToConfigValueCli
  :: (MonadThrow m, JSON.FromJSON cmd, JSON.ToJSON cmd, Eq cmd, Hashable cmd)
    => HashMap cmd (Opt.Parser ConfigValue)
    -> ( Text, Spec.ConfigValue cmd )
    -> m (HashMap cmd (Opt.Parser ConfigValue))
specToConfigValueCli acc (specEntryKey, specConfigValue) =
  case specConfigValue of
    Spec.ConfigValue _ sources ->
      configValueSpecToCli
        acc
        specEntryKey
        sources

    Spec.SubConfig subConfigSpec ->
      subConfigSpecToCli
        specEntryKey
        subConfigSpec
        acc

configValueCliAccInit
  :: (MonadThrow m, JSON.FromJSON cmd, Eq cmd, Hashable cmd)
    => Spec.ConfigSpec cmd
    -> m (HashMap cmd (Opt.Parser ConfigValue))
configValueCliAccInit spec =
  let
    zeroParser =
      pure $ SubConfig HashMap.empty

    commandsSpec = do
      programSpec <- Spec.specCliProgramSpec spec
      Spec.cliCommands programSpec

  in
    case commandsSpec of
      Nothing ->
        throwM CommandsKeyNotDefined

      Just commands ->
        foldM (\acc (commandVal, _) -> do
                command <- parseCommandJsonValue (JSON.String commandVal)
                return $ HashMap.insert command zeroParser acc)
              HashMap.empty
              (HashMap.toList commands)

joinCommandParsers
  :: (MonadThrow m, JSON.ToJSON cmd, Eq cmd, Hashable cmd)
    => HashMap cmd (Opt.Parser ConfigValue)
    -> m (Opt.Parser (cmd, Config))
joinCommandParsers parserPerCommand =
  let
    joinParser acc (command, subConfigParser) =
      let
        parser =
          fmap (\subConfig -> (command, Config subConfig))
               subConfigParser
      in do
        commandTexts <- commandToKey command

        let
          commandParsers =
            map (\commandText ->
                    Opt.command (Text.unpack commandText)
                    (Opt.info (Opt.helper <*> parser) Opt.idm))
                commandTexts

        [acc]
          & (++ commandParsers)
          & mconcat
          & return
  in do
    mergedParsers <-
      foldM joinParser Opt.idm (HashMap.toList parserPerCommand)
    return (Opt.subparser mergedParsers)

specToConfigCli
  :: (MonadThrow m, JSON.FromJSON cmd, JSON.ToJSON cmd, Eq cmd, Hashable cmd)
    => Spec.ConfigSpec cmd
    -> m (Opt.Parser (cmd, Config))
specToConfigCli spec = do
  acc <- configValueCliAccInit spec
  parsers <-
    foldM specToConfigValueCli
          acc
          (HashMap.toList $ Spec.specConfigValues spec)

  joinCommandParsers parsers

{-|

Dynamically generate an OptParser CLI with sub-commands from the spec settings
declared on the @ConfigSpec@. This will process the OptParser from given input
rather than fetching it from the OS.

This will return the selected record parsed from the sub-command input and the
configuration map with keys defined for that sub-command.

-}
resolveCommandCliPure
  :: (MonadThrow m, JSON.FromJSON cmd, JSON.ToJSON cmd, Eq cmd, Hashable cmd)
    => Spec.ConfigSpec cmd  -- ^ Config Spec (normally parsed from json or yaml file)-- ^ The
    -> Text                 -- ^ Name of the program running the CLI
    -> [Text]               -- ^ Arglist for the program
    -> m (cmd, Config)      -- ^ Selected command and Configuration Map
resolveCommandCliPure configSpec progName args = do
  configParser <- specToConfigCli configSpec

  let
    programModFlags =
      case Spec.specCliProgramSpec configSpec of
           Just programSpec ->
             Opt.fullDesc
              `mappend` (programSpec
                        & Spec.cliProgramDesc
                        & Text.unpack
                        & Opt.progDesc)
              `mappend` (programSpec
                        & Spec.cliProgramHeader
                        & Text.unpack
                        & Opt.header)
           Nothing ->
             mempty

    programParser =
      Opt.info (Opt.helper <*> configParser)
               programModFlags

    programResult =
      args
        & map Text.unpack
        & Opt.execParserPure Opt.defaultPrefs programParser

  programResultToResolverResult progName programResult


{-|

Dynamically generate an OptParser CLI with sub-commands from the spec settings
declared on the @ConfigSpec@.

Once it generates the CLI and gathers the input, it will return the selected
record parsed from the sub-command input and the configuration map with keys
defined for that sub-command.

-}
resolveCommandCli
  :: (JSON.FromJSON cmd, JSON.ToJSON cmd, Eq cmd, Hashable cmd)
    => Spec.ConfigSpec cmd  -- ^ Config Spec (normally parsed from json or yaml file)
    -> IO (cmd, Config)     -- ^ Selected command and Configuration Map
resolveCommandCli configSpec = do
  progName <- Text.pack <$> getProgName
  args     <- map Text.pack <$> getArgs

  handleCliResult
    $ resolveCommandCliPure configSpec progName args