packages feed

opt-env-conf-0.13.0.0: src/OptEnvConf/Setting.hs

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -Wno-unused-pattern-binds #-}

module OptEnvConf.Setting
  ( Setting (..),
    EnvVarSetting (..),
    ConfigValSetting (..),

    -- * Builders
    help,
    metavar,
    argument,
    option,
    switch,
    reader,
    str,
    auto,
    long,
    short,
    env,
    unprefixedEnv,
    conf,
    confWith,
    confWith',
    unprefixedConf,
    unprefixedConfWith,
    unprefixedConfWith',
    name,
    value,
    valueWithShown,
    example,
    shownExample,
    hidden,
    completer,
    Builder (..),
    BuildInstruction (..),
    requiredCapability,
    readSecretCapability,

    -- * Internal
    showSettingABit,
    SettingHash (..),
    hashSetting,
    completeBuilder,
    mapMaybeBuilder,
    emptySetting,
    Metavar,
    Help,
    prefixEnvVarSetting,
    suffixEnvVarSetting,
    prefixConfigValSetting,
    suffixConfigValSettingKey,
  )
where

import Autodocodec
import Data.Hashable
import Data.List.NonEmpty (NonEmpty (..), (<|))
import qualified Data.List.NonEmpty as NE
import Data.Maybe
import Data.Set (Set)
import qualified Data.Set as Set
import qualified Data.Text as T
import OptEnvConf.Args (Dashed (..), renderDashed)
import OptEnvConf.Capability
import OptEnvConf.Casing
import OptEnvConf.Completer
import OptEnvConf.Reader
import Text.Show

type Metavar = String

type Help = String

-- | A setting for parsing and documenting a single value.
data Setting a = Setting
  { -- | Which dashed values are required for parsing
    --
    -- No dashed values means this is an argument.
    settingDasheds :: ![Dashed],
    -- | Which readers should be tried to parse a value from a string
    settingReaders :: ![Reader a],
    -- | Whether the readers should be used to parsed arguments
    settingTryArgument :: !Bool,
    -- | What value to parse when the switch exists.
    --
    -- Nothing means this is not a switch.
    settingSwitchValue :: !(Maybe a),
    -- | Whether the dasheds should be tried together with the readers as
    -- options.
    settingTryOption :: !Bool,
    -- | Which env vars can be read.
    settingEnvVars :: !(Maybe (NonEmpty EnvVarSetting)),
    -- | Which and how to parse config values
    settingConfigVals :: !(Maybe (NonEmpty (ConfigValSetting a))),
    -- | Default value, if none of the above find the setting.
    settingDefaultValue :: !(Maybe (a, String)),
    -- | Example values
    settingExamples :: ![String],
    -- | Whether to hide docs
    settingHidden :: !Bool,
    -- | Which metavar should be show in documentation
    settingMetavar :: !(Maybe Metavar),
    settingHelp :: !(Maybe String),
    settingCompleter :: !(Maybe Completer),
    settingRequiredCapabilities :: !(Set Capability)
  }

-- An 'Ord'-able Setting without giving 'Setting' an 'Eq' instance
newtype SettingHash = SettingHash Int
  deriving (Show, Eq, Ord)

-- We hash only the parts of the setting that have anything to do with how the
-- setting is parsed, not the parts that are for documentation.
hashSetting :: Setting a -> SettingHash
hashSetting Setting {..} =
  SettingHash
    ( 42
        `hashWithSalt` map renderDashed settingDasheds
        `hashWithSalt` settingTryArgument
        `hashWithSalt` length settingReaders
        `hashWithSalt` isJust settingSwitchValue
        `hashWithSalt` settingTryOption
        `hashWithSalt` settingEnvVars
        `hashWithSalt` (NE.map configValSettingPath <$> settingConfigVals)
        `hashWithSalt` (snd <$> settingDefaultValue)
    )

data EnvVarSetting = EnvVarSetting
  { envVarSettingVar :: !String,
    envVarSettingAllowPrefix :: !Bool
  }
  deriving (Show, Eq)

instance Hashable EnvVarSetting where
  hashWithSalt salt EnvVarSetting {..} =
    salt
      `hashWithSalt` envVarSettingVar
      `hashWithSalt` envVarSettingAllowPrefix

prefixEnvVarSetting :: String -> EnvVarSetting -> EnvVarSetting
prefixEnvVarSetting prefix e =
  if envVarSettingAllowPrefix e
    then e {envVarSettingVar = prefix <> envVarSettingVar e}
    else e

suffixEnvVarSetting :: String -> EnvVarSetting -> EnvVarSetting
suffixEnvVarSetting suffix e = e {envVarSettingVar = envVarSettingVar e <> suffix}

data ConfigValSetting a = forall void.
  ConfigValSetting
  { configValSettingPath :: !(NonEmpty String),
    configValSettingAllowPrefix :: !Bool,
    configValSettingCodec :: !(ValueCodec void (Maybe a))
  }

prefixConfigValSetting :: String -> ConfigValSetting a -> ConfigValSetting a
prefixConfigValSetting prefix c =
  if configValSettingAllowPrefix c
    then c {configValSettingPath = prefix NE.<| configValSettingPath c}
    else c

suffixConfigValSettingKey :: String -> ConfigValSetting a -> ConfigValSetting a
suffixConfigValSettingKey suffix c = c {configValSettingPath = suffixPath $ configValSettingPath c}
  where
    suffixPath :: NonEmpty String -> NonEmpty String
    suffixPath (f :| rest) = case NE.nonEmpty rest of
      Nothing -> (f <> suffix) :| []
      Just ne -> f NE.<| suffixPath ne

-- | A 'mempty' 'Setting' to build up a setting from.
emptySetting :: Setting a
emptySetting =
  Setting
    { settingDasheds = [],
      settingReaders = [],
      settingTryArgument = False,
      settingSwitchValue = Nothing,
      settingTryOption = False,
      settingEnvVars = Nothing,
      settingConfigVals = Nothing,
      settingMetavar = Nothing,
      settingHelp = Nothing,
      settingExamples = [],
      settingHidden = False,
      settingDefaultValue = Nothing,
      settingCompleter = Nothing,
      settingRequiredCapabilities = Set.empty
    }

-- | Show a 'Setting' as much as possible, for debugging
showSettingABit :: Setting a -> ShowS
showSettingABit Setting {..} =
  let Setting _ _ _ _ _ _ _ _ _ _ _ _ _ _ = undefined
   in showParen True $
        showString "Setting "
          . showsPrec 11 settingDasheds
          . showString " "
          . showListWith (\_ -> showString "_") settingReaders
          . showString " "
          . showsPrec 11 settingTryArgument
          . showString " "
          . showMaybeWith (\_ -> showString "_") settingSwitchValue
          . showString " "
          . showsPrec 11 settingTryOption
          . showString " "
          . showsPrec 11 settingEnvVars
          . showString " "
          . showMaybeWith (showNonEmptyWith showConfigValSettingABit) settingConfigVals
          . showString " "
          . showMaybeWith (\_ -> showString "_") settingDefaultValue
          . showString " "
          . showsPrec 11 settingExamples
          . showString " "
          . showsPrec 11 settingHidden
          . showString " "
          . showsPrec 11 settingMetavar
          . showString " "
          . showsPrec 11 settingHelp
          . showString " "
          . showMaybeWith (\_ -> showString "_") settingCompleter
          . showString " "
          . showsPrec 11 settingRequiredCapabilities

showConfigValSettingABit :: ConfigValSetting a -> ShowS
showConfigValSettingABit ConfigValSetting {..} =
  showString "ConfigValSetting "
    . showsPrec 11 configValSettingPath
    . showString " "
    . showString (showCodecABit configValSettingCodec)

showMaybeWith :: (a -> ShowS) -> Maybe a -> ShowS
showMaybeWith _ Nothing = showString "Nothing"
showMaybeWith func (Just a) = showParen True $ showString "Just " . func a

showNonEmptyWith :: (a -> ShowS) -> NonEmpty a -> ShowS
showNonEmptyWith func (a :| as) =
  showParen True $
    func a
      . showString " :| "
      . showListWith func as

-- | Builder for a 'Setting'
newtype Builder a = Builder {unBuilder :: [BuildInstruction a]}

data BuildInstruction a
  = BuildAddHelp !String
  | BuildSetMetavar !String
  | BuildTryArgument
  | BuildTryOption
  | BuildSetSwitchValue !a
  | BuildAddReader !(Reader a)
  | BuildAddLong !(NonEmpty Char)
  | BuildAddShort !Char
  | BuildAddEnv !EnvVarSetting
  | BuildAddConf !(ConfigValSetting a)
  | BuildSetDefault !a !String
  | BuildAddExample !String
  | BuildSetHidden
  | BuildSetCompleter !Completer
  | BuildAddRequiredCapability !Capability

applyBuildInstructions :: [BuildInstruction a] -> Setting a -> Setting a
applyBuildInstructions is s = foldr applyBuildInstruction s is

applyBuildInstruction :: BuildInstruction a -> Setting a -> Setting a
applyBuildInstruction bi s = case bi of
  BuildAddHelp h -> s {settingHelp = Just $ maybe h (<> h) (settingHelp s)}
  BuildSetMetavar mv -> s {settingMetavar = Just mv}
  BuildTryArgument -> s {settingTryArgument = True}
  BuildTryOption -> s {settingTryOption = True}
  BuildSetSwitchValue a -> s {settingSwitchValue = Just a}
  BuildAddReader r -> s {settingReaders = r : settingReaders s}
  BuildAddLong l -> s {settingDasheds = DashedLong l : settingDasheds s}
  BuildAddShort c -> s {settingDasheds = DashedShort c : settingDasheds s}
  BuildAddEnv v -> s {settingEnvVars = Just $ maybe (v :| []) (v <|) $ settingEnvVars s}
  BuildAddConf t -> s {settingConfigVals = Just $ maybe (t :| []) (t <|) $ settingConfigVals s}
  BuildSetDefault a shown -> s {settingDefaultValue = Just (a, shown)}
  BuildAddExample e -> s {settingExamples = e : settingExamples s}
  BuildSetHidden -> s {settingHidden = True}
  BuildSetCompleter c -> s {settingCompleter = Just c}
  BuildAddRequiredCapability c -> s {settingRequiredCapabilities = Set.insert c (settingRequiredCapabilities s)}

instance Semigroup (Builder f) where
  (<>) (Builder f1) (Builder f2) = Builder (f1 <> f2)

instance Monoid (Builder f) where
  mempty = Builder []
  mappend = (<>)

-- | Complete a 'Builder' into a 'Setting'
completeBuilder :: Builder a -> Setting a
completeBuilder b = applyBuildInstructions (unBuilder b) emptySetting

mapMaybeBuilder :: (BuildInstruction a -> Maybe (BuildInstruction b)) -> Builder a -> Builder b
mapMaybeBuilder func = Builder . mapMaybe func . unBuilder

-- | Document a setting
--
-- Multiple 'help's concatenate help on new lines.
help :: String -> Builder a
help s = Builder [BuildAddHelp s]

-- | Document an 'option' or 'env' var.
--
-- Multiple 'metavar's override eachother.
metavar :: String -> Builder a
metavar mv = Builder [BuildSetMetavar mv]

-- | Try to parse an argument.
--
-- You'll also need to add a 'reader'.
--
-- Multiple 'argument's are redundant.
argument :: Builder a
argument = Builder [BuildTryArgument]

-- | Try to parse an argument.
--
-- You'll also need to add a 'reader', at least one 'long' or 'short', and a
-- 'metavar'.
--
-- Multiple 'option's are redundant.
option :: Builder a
option = Builder [BuildTryOption]

-- | Try to parse a switch, activate the given value when succesful
--
-- You'll also need to add at least one 'long' or 'short'.
--
-- Multiple 'switch's override eachother.
switch :: a -> Builder a
switch v = Builder [BuildSetSwitchValue v]

-- | Declare how to parse an argument, option, or environment variable.
reader :: Reader a -> Builder a
reader r = Builder [BuildAddReader r]

-- | Try to parse this 'long' 'option' or 'switch'.
--
-- @long "foo"@ corresponds to @--foo@
--
-- Notes:
--     * Parsing options with an empty name in the 'long' is not supported.
--     * Parsing options with an '=' sign in the 'long' is not supported.
--
-- Multiple 'long's will be tried in order.
-- Empty 'long's will be ignored.
long :: String -> Builder a
long l = Builder [BuildAddLong ne | ne <- maybeToList (NE.nonEmpty l)]

-- | Try to parse this 'short' 'option' or 'switch'.
--
-- @short 'f'@ corresponds to @-f@
--
-- Notes:
--     * Parsing options with @short '-'@ is not supported.
--
-- Multiple 'short's will be tried in order.
short :: Char -> Builder a
short c = Builder [BuildAddShort c]

-- | Try to parse an environment variable.
--
-- You'll also need to add a 'reader' and a 'metavar'.
--
-- Multiple 'env's will be tried in order.
env :: String -> Builder a
env v = Builder [BuildAddEnv (EnvVarSetting v True)]

-- | Like 'env' but ignores any 'subEnv', 'subEnv_', or 'subAll'.
unprefixedEnv :: String -> Builder a
unprefixedEnv v = Builder [BuildAddEnv (EnvVarSetting v False)]

-- | Try to parse a configuration value at the given key.
--
-- Multiple 'conf's will be tried in order.
conf :: (HasCodec a) => String -> Builder a
conf k = confWith k codec

-- | Like 'conf' but with a custom 'Codec' for parsing the value.
confWith :: String -> ValueCodec void a -> Builder a
confWith k c = confWith' k (maybeCodec c)

-- | Like 'confWith' but allows interpreting 'Null' as a value other than "Not found".
confWith' :: String -> ValueCodec void (Maybe a) -> Builder a
confWith' k c =
  let t =
        ConfigValSetting
          { configValSettingPath = k :| [],
            configValSettingAllowPrefix = True,
            configValSettingCodec = c
          }
   in Builder [BuildAddConf t]

-- | Like 'conf' but ignores any 'subConf', 'subConf_', or 'subAll'.
unprefixedConf :: (HasCodec a) => String -> Builder a
unprefixedConf k = unprefixedConfWith k codec

-- | Like 'confWith' but ignores any 'subConf', 'subConf_', or 'subAll'.
unprefixedConfWith :: String -> ValueCodec void a -> Builder a
unprefixedConfWith k c = unprefixedConfWith' k (maybeCodec c)

-- | Like 'confWith'' but ignores any 'subConf', 'subConf_', or 'subAll'.
unprefixedConfWith' :: String -> ValueCodec void (Maybe a) -> Builder a
unprefixedConfWith' k c =
  let t =
        ConfigValSetting
          { configValSettingPath = k :| [],
            configValSettingAllowPrefix = False,
            configValSettingCodec = c
          }
   in Builder [BuildAddConf t]

-- | Short-hand function for 'option', 'long', 'env', and 'conf' at the same time.
--
-- Multiple 'name's will be tried in order.
name :: (HasCodec a) => String -> Builder a
name s =
  mconcat
    [ option,
      long (toArgCase s),
      env (toEnvCase s),
      conf (toConfigCase s)
    ]

-- | Set the default value
--
-- Multiple 'value's override eachother.
--
-- API Note: @default@ is not a valid identifier in Haskell.
-- I'd also have preferred @default@ instead.
value :: (Show a) => a -> Builder a
value = valueWithShown show

-- | Set the default value, along with version of it shown by a custom function.
valueWithShown :: (a -> String) -> a -> Builder a
valueWithShown show' a = Builder [BuildSetDefault a (show' a)]

-- | Provide an example value for documentation.
--
-- The example is provided as a literal string.
--
-- If you use 'reader' 'auto', you'll want to use 'shownExample' instead.
example :: String -> Builder a
example s = Builder [BuildAddExample s]

-- | Use 'Show' to show an 'example'.
--
-- This only makes sense if you use 'reader' 'auto'.
shownExample :: (Show a) => a -> Builder a
shownExample = example . show

-- | Don't show this setting in documentation
--
-- Multiple 'hidden's are redundant.
hidden :: Builder a
hidden = Builder [BuildSetHidden]

-- | Set the setting to tab-complete with the given completer
--
-- Multiple 'completer's are redundant.
completer :: Completer -> Builder a
completer c = Builder [BuildSetCompleter c]

-- | Annotate a setting with a required capability.
requiredCapability :: String -> Builder a
requiredCapability c = Builder [BuildAddRequiredCapability (Capability (T.pack c))]