packages feed

effectful-cli-0.2.6.0: src/Data/CLI/Config.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}

-- | Ready-made @show@/@get@/@set@ commands over the application's TOML config file.
module Data.CLI.Config (
  ConfigFile (..),
  ConfigCommand (..),
) where

import Data.CLI.Class (Command (..))
import Data.CLI.Impl (noArgs, unboundEnumError)
import Data.Error.CLI (CLIError (InputError))
import Data.Kind (Type)
import Data.Text (Text)
import Data.Text qualified as T
import Effectful.CLI.Config (getConfigKey, readConfigText, setConfigKey)
import Effectful.CLI.Exception (rethrowIO)
import Effectful.CLI.IO (printOutputLn)
import Effectful.Reader.Static (asks)
import GHC.Enum (boundedEnumFrom, boundedEnumFromThen)
import Options.Applicative (Parser, metavar, strArgument)
import Toml.Schema (FromValue)

-- | Application config @c@ that knows its config file and the type it decodes to.
--
-- @since 0.2.6.0
class FromValue (ConfigOf c) => ConfigFile c where
  -- | Type the config file decodes to; 'ConfigSet' refuses to write anything else.
  type ConfigOf c :: Type

  -- | Path of the config file.
  configFilePath :: c -> FilePath

-- | Config file commands for config @c@ and application error @e@. IO failures
-- are raised as 'InputError'.
--
-- @since 0.2.6.0
data ConfigCommand c e
  = -- | Print the config file.
    ConfigShow
  | -- | Print a dotted key's value.
    ConfigGet Text
  | -- | Set a dotted key to a TOML literal, or a string if not one.
    ConfigSet Text Text
  deriving (Eq, Show)

instance Bounded (ConfigCommand c e) where
  minBound = ConfigShow
  maxBound = ConfigSet mempty mempty

instance Enum (ConfigCommand c e) where
  fromEnum ConfigShow = 0
  fromEnum (ConfigGet _) = 1
  fromEnum (ConfigSet _ _) = 2

  toEnum 0 = minBound
  toEnum 1 = ConfigGet mempty
  toEnum 2 = maxBound
  toEnum _ = unboundEnumError "ConfigCommand"

  enumFrom = boundedEnumFrom
  enumFromThen = boundedEnumFromThen

instance ConfigFile c => Command c e (ConfigCommand c e) where
  cmdname ConfigShow = "show"
  cmdname (ConfigGet _) = "get"
  cmdname (ConfigSet _ _) = "set"

  doc ConfigShow = "Print the config file"
  doc (ConfigGet _) = "Print a dotted key's value (e.g. section.key)"
  doc (ConfigSet _ _) = "Set a dotted key to a TOML literal, or a string if not one"

  parser ConfigShow = noArgs ConfigShow
  parser (ConfigGet _) = ConfigGet <$> keyArg
  parser (ConfigSet _ _) = ConfigSet <$> keyArg <*> strArgument (metavar "VALUE")

  runner cmd = do
    path <- asks @c configFilePath
    rethrowIO (InputError . ("config file: " <>) . T.show) $ case cmd of
      ConfigShow -> printOutputLn . T.stripEnd =<< readConfigText path
      ConfigGet k -> printOutputLn =<< getConfigKey path k
      ConfigSet k v -> setConfigKey @(ConfigOf c) path k v

keyArg :: Parser Text
keyArg = strArgument (metavar "KEY")