packages feed

etc 0.2.0.0 → 0.3.0.0

raw patch · 29 files changed

+1628/−2156 lines, 29 filesdep +riodep +typed-processdep +unliftiodep −bytestringdep −containersdep −directorydep ~aesondep ~ansi-wl-pprintdep ~basePVP ok

version bump matches the API change (PVP)

Dependencies added: rio, typed-process, unliftio

Dependencies removed: bytestring, containers, directory, exceptions, protolude, tasty-rerun, unordered-containers, vector

Dependency ranges changed: aeson, ansi-wl-pprint, base, edit-distance, hashable, optparse-applicative, tasty, tasty-hunit, text, yaml

API changes (from Hackage documentation)

+ System.Etc: Plain :: !a -> Value a
+ System.Etc: Sensitive :: !a -> Value a
+ System.Etc: [fromValue] :: Value a -> !a
+ System.Etc: data Value a
+ System.Etc.Internal.Spec.Types: [isSensitive] :: ConfigValue cmd -> !Bool
+ System.Etc.Internal.Types: Plain :: !a -> Value a
+ System.Etc.Internal.Types: Sensitive :: !a -> Value a
+ System.Etc.Internal.Types: [fromValue] :: Value a -> !a
+ System.Etc.Internal.Types: boolToValue :: Bool -> (a -> Value a)
+ System.Etc.Internal.Types: data Value a
+ System.Etc.Internal.Types: instance Data.Semigroup.Semigroup System.Etc.Internal.Types.Config
+ System.Etc.Internal.Types: instance Data.Semigroup.Semigroup System.Etc.Internal.Types.ConfigValue
+ System.Etc.Internal.Types: instance Data.String.IsString a => Data.String.IsString (System.Etc.Internal.Types.Value a)
+ System.Etc.Internal.Types: instance GHC.Base.Applicative System.Etc.Internal.Types.Value
+ System.Etc.Internal.Types: instance GHC.Base.Functor System.Etc.Internal.Types.Value
+ System.Etc.Internal.Types: instance GHC.Classes.Eq a => GHC.Classes.Eq (System.Etc.Internal.Types.Value a)
+ System.Etc.Internal.Types: instance GHC.Classes.Ord a => GHC.Classes.Ord (System.Etc.Internal.Types.Value a)
+ System.Etc.Internal.Types: instance GHC.Generics.Generic (System.Etc.Internal.Types.Value a)
+ System.Etc.Internal.Types: instance GHC.Show.Show a => GHC.Show.Show (System.Etc.Internal.Types.Value a)
- System.Etc: Cli :: Value -> ConfigSource
+ System.Etc: Cli :: !(Value Value) -> ConfigSource
- System.Etc: ConfigurationFileNotFound :: Text -> ConfigurationError
+ System.Etc: ConfigurationFileNotFound :: !Text -> ConfigurationError
- System.Etc: Default :: Value -> ConfigSource
+ System.Etc: Default :: !(Value Value) -> ConfigSource
- System.Etc: Env :: Text -> Value -> ConfigSource
+ System.Etc: Env :: !Text -> !(Value Value) -> ConfigSource
- System.Etc: File :: Int -> Text -> Value -> ConfigSource
+ System.Etc: File :: !Int -> !Text -> !(Value Value) -> ConfigSource
- System.Etc: InvalidConfigKeyPath :: [Text] -> ConfigurationError
+ System.Etc: InvalidConfigKeyPath :: ![Text] -> ConfigurationError
- System.Etc: InvalidConfiguration :: Text -> ConfigurationError
+ System.Etc: InvalidConfiguration :: !(Maybe Text) -> !Text -> ConfigurationError
- System.Etc: [configIndex] :: ConfigSource -> Int
+ System.Etc: [configIndex] :: ConfigSource -> !Int
- System.Etc: [envVar] :: ConfigSource -> Text
+ System.Etc: [envVar] :: ConfigSource -> !Text
- System.Etc: [filepath] :: ConfigSource -> Text
+ System.Etc: [filepath] :: ConfigSource -> !Text
- System.Etc: [value] :: ConfigSource -> Value
+ System.Etc: [value] :: ConfigSource -> !(Value Value)
- System.Etc: getAllConfigSources :: (IConfig config, MonadThrow m) => [Text] -> config -> m (Set ConfigSource)
+ System.Etc: getAllConfigSources :: (IConfig config, (MonadThrow m)) => [Text] -> config -> m (Set ConfigSource)
- System.Etc: getConfigValueWith :: (IConfig config, MonadThrow m) => (Value -> Parser result) -> [Text] -> config -> m result
+ System.Etc: getConfigValueWith :: (IConfig config, (MonadThrow m)) => (Value -> Parser result) -> [Text] -> config -> m result
- System.Etc: getSelectedConfigSource :: (IConfig config, MonadThrow m) => [Text] -> config -> m ConfigSource
+ System.Etc: getSelectedConfigSource :: (IConfig config, (MonadThrow m)) => [Text] -> config -> m ConfigSource
- System.Etc: parseConfigSpec :: (MonadCatch m) => Text -> m (ConfigSpec ())
+ System.Etc: parseConfigSpec :: (MonadThrow m) => Text -> m (ConfigSpec ())
- System.Etc.Internal.Spec.Types: Arg :: Maybe Text -> Bool -> CliArgValueType -> CliEntryMetadata
+ System.Etc.Internal.Spec.Types: Arg :: !(Maybe Text) -> !Bool -> !CliArgValueType -> CliEntryMetadata
- System.Etc.Internal.Spec.Types: CliCmdSpec :: Text -> Text -> CliCmdSpec
+ System.Etc.Internal.Spec.Types: CliCmdSpec :: !Text -> !Text -> CliCmdSpec
- System.Etc.Internal.Spec.Types: CliProgramSpec :: Text -> Text -> Maybe (HashMap Text CliCmdSpec) -> CliProgramSpec
+ System.Etc.Internal.Spec.Types: CliProgramSpec :: !Text -> !Text -> !(Maybe (HashMap Text CliCmdSpec)) -> CliProgramSpec
- System.Etc.Internal.Spec.Types: CmdEntry :: Vector cmd -> CliEntryMetadata -> CliEntrySpec cmd
+ System.Etc.Internal.Spec.Types: CmdEntry :: !(Vector cmd) -> !CliEntryMetadata -> CliEntrySpec cmd
- System.Etc.Internal.Spec.Types: ConfigSources :: Maybe Text -> Maybe (CliEntrySpec cmd) -> ConfigSources cmd
+ System.Etc.Internal.Spec.Types: ConfigSources :: !(Maybe Text) -> !(Maybe (CliEntrySpec cmd)) -> ConfigSources cmd
- System.Etc.Internal.Spec.Types: ConfigSpec :: [Text] -> Maybe CliProgramSpec -> HashMap Text (ConfigValue cmd) -> ConfigSpec cmd
+ System.Etc.Internal.Spec.Types: ConfigSpec :: ![Text] -> !(Maybe CliProgramSpec) -> !(HashMap Text (ConfigValue cmd)) -> ConfigSpec cmd
- System.Etc.Internal.Spec.Types: ConfigValue :: Maybe Value -> ConfigSources cmd -> ConfigValue cmd
+ System.Etc.Internal.Spec.Types: ConfigValue :: !(Maybe Value) -> !Bool -> !(ConfigSources cmd) -> ConfigValue cmd
- System.Etc.Internal.Spec.Types: ConfigurationFileNotFound :: Text -> ConfigurationError
+ System.Etc.Internal.Spec.Types: ConfigurationFileNotFound :: !Text -> ConfigurationError
- System.Etc.Internal.Spec.Types: InvalidConfigKeyPath :: [Text] -> ConfigurationError
+ System.Etc.Internal.Spec.Types: InvalidConfigKeyPath :: ![Text] -> ConfigurationError
- System.Etc.Internal.Spec.Types: InvalidConfiguration :: Text -> ConfigurationError
+ System.Etc.Internal.Spec.Types: InvalidConfiguration :: !(Maybe Text) -> !Text -> ConfigurationError
- System.Etc.Internal.Spec.Types: Opt :: Maybe Text -> Maybe Text -> Maybe Text -> Maybe Text -> Bool -> CliOptValueType -> CliEntryMetadata
+ System.Etc.Internal.Spec.Types: Opt :: !(Maybe Text) -> !(Maybe Text) -> !(Maybe Text) -> !(Maybe Text) -> !Bool -> !CliOptValueType -> CliEntryMetadata
- System.Etc.Internal.Spec.Types: PlainEntry :: CliEntryMetadata -> CliEntrySpec cmd
+ System.Etc.Internal.Spec.Types: PlainEntry :: !CliEntryMetadata -> CliEntrySpec cmd
- System.Etc.Internal.Spec.Types: SubConfig :: HashMap Text (ConfigValue cmd) -> ConfigValue cmd
+ System.Etc.Internal.Spec.Types: SubConfig :: !(HashMap Text (ConfigValue cmd)) -> ConfigValue cmd
- System.Etc.Internal.Spec.Types: [argMetavar] :: CliEntryMetadata -> Maybe Text
+ System.Etc.Internal.Spec.Types: [argMetavar] :: CliEntryMetadata -> !(Maybe Text)
- System.Etc.Internal.Spec.Types: [argValueType] :: CliEntryMetadata -> CliArgValueType
+ System.Etc.Internal.Spec.Types: [argValueType] :: CliEntryMetadata -> !CliArgValueType
- System.Etc.Internal.Spec.Types: [cliCmdDesc] :: CliCmdSpec -> Text
+ System.Etc.Internal.Spec.Types: [cliCmdDesc] :: CliCmdSpec -> !Text
- System.Etc.Internal.Spec.Types: [cliCmdHeader] :: CliCmdSpec -> Text
+ System.Etc.Internal.Spec.Types: [cliCmdHeader] :: CliCmdSpec -> !Text
- System.Etc.Internal.Spec.Types: [cliCommands] :: CliProgramSpec -> Maybe (HashMap Text CliCmdSpec)
+ System.Etc.Internal.Spec.Types: [cliCommands] :: CliProgramSpec -> !(Maybe (HashMap Text CliCmdSpec))
- System.Etc.Internal.Spec.Types: [cliEntryCmdValue] :: CliEntrySpec cmd -> Vector cmd
+ System.Etc.Internal.Spec.Types: [cliEntryCmdValue] :: CliEntrySpec cmd -> !(Vector cmd)
- System.Etc.Internal.Spec.Types: [cliEntryMetadata] :: CliEntrySpec cmd -> CliEntryMetadata
+ System.Etc.Internal.Spec.Types: [cliEntryMetadata] :: CliEntrySpec cmd -> !CliEntryMetadata
- System.Etc.Internal.Spec.Types: [cliEntry] :: ConfigSources cmd -> Maybe (CliEntrySpec cmd)
+ System.Etc.Internal.Spec.Types: [cliEntry] :: ConfigSources cmd -> !(Maybe (CliEntrySpec cmd))
- System.Etc.Internal.Spec.Types: [cliProgramDesc] :: CliProgramSpec -> Text
+ System.Etc.Internal.Spec.Types: [cliProgramDesc] :: CliProgramSpec -> !Text
- System.Etc.Internal.Spec.Types: [cliProgramHeader] :: CliProgramSpec -> Text
+ System.Etc.Internal.Spec.Types: [cliProgramHeader] :: CliProgramSpec -> !Text
- System.Etc.Internal.Spec.Types: [configSources] :: ConfigValue cmd -> ConfigSources cmd
+ System.Etc.Internal.Spec.Types: [configSources] :: ConfigValue cmd -> !(ConfigSources cmd)
- System.Etc.Internal.Spec.Types: [defaultValue] :: ConfigValue cmd -> Maybe Value
+ System.Etc.Internal.Spec.Types: [defaultValue] :: ConfigValue cmd -> !(Maybe Value)
- System.Etc.Internal.Spec.Types: [envVar] :: ConfigSources cmd -> Maybe Text
+ System.Etc.Internal.Spec.Types: [envVar] :: ConfigSources cmd -> !(Maybe Text)
- System.Etc.Internal.Spec.Types: [optHelp] :: CliEntryMetadata -> Maybe Text
+ System.Etc.Internal.Spec.Types: [optHelp] :: CliEntryMetadata -> !(Maybe Text)
- System.Etc.Internal.Spec.Types: [optLong] :: CliEntryMetadata -> Maybe Text
+ System.Etc.Internal.Spec.Types: [optLong] :: CliEntryMetadata -> !(Maybe Text)
- System.Etc.Internal.Spec.Types: [optMetavar] :: CliEntryMetadata -> Maybe Text
+ System.Etc.Internal.Spec.Types: [optMetavar] :: CliEntryMetadata -> !(Maybe Text)
- System.Etc.Internal.Spec.Types: [optRequired] :: CliEntryMetadata -> Bool
+ System.Etc.Internal.Spec.Types: [optRequired] :: CliEntryMetadata -> !Bool
- System.Etc.Internal.Spec.Types: [optShort] :: CliEntryMetadata -> Maybe Text
+ System.Etc.Internal.Spec.Types: [optShort] :: CliEntryMetadata -> !(Maybe Text)
- System.Etc.Internal.Spec.Types: [optValueType] :: CliEntryMetadata -> CliOptValueType
+ System.Etc.Internal.Spec.Types: [optValueType] :: CliEntryMetadata -> !CliOptValueType
- System.Etc.Internal.Spec.Types: [specCliProgramSpec] :: ConfigSpec cmd -> Maybe CliProgramSpec
+ System.Etc.Internal.Spec.Types: [specCliProgramSpec] :: ConfigSpec cmd -> !(Maybe CliProgramSpec)
- System.Etc.Internal.Spec.Types: [specConfigFilepaths] :: ConfigSpec cmd -> [Text]
+ System.Etc.Internal.Spec.Types: [specConfigFilepaths] :: ConfigSpec cmd -> ![Text]
- System.Etc.Internal.Spec.Types: [specConfigValues] :: ConfigSpec cmd -> HashMap Text (ConfigValue cmd)
+ System.Etc.Internal.Spec.Types: [specConfigValues] :: ConfigSpec cmd -> !(HashMap Text (ConfigValue cmd))
- System.Etc.Internal.Spec.Types: [subConfig] :: ConfigValue cmd -> HashMap Text (ConfigValue cmd)
+ System.Etc.Internal.Spec.Types: [subConfig] :: ConfigValue cmd -> !(HashMap Text (ConfigValue cmd))
- System.Etc.Internal.Types: Cli :: Value -> ConfigSource
+ System.Etc.Internal.Types: Cli :: !(Value Value) -> ConfigSource
- System.Etc.Internal.Types: ConfigValue :: Set ConfigSource -> ConfigValue
+ System.Etc.Internal.Types: ConfigValue :: !(Set ConfigSource) -> ConfigValue
- System.Etc.Internal.Types: Default :: Value -> ConfigSource
+ System.Etc.Internal.Types: Default :: !(Value Value) -> ConfigSource
- System.Etc.Internal.Types: Env :: Text -> Value -> ConfigSource
+ System.Etc.Internal.Types: Env :: !Text -> !(Value Value) -> ConfigSource
- System.Etc.Internal.Types: File :: Int -> Text -> Value -> ConfigSource
+ System.Etc.Internal.Types: File :: !Int -> !Text -> !(Value Value) -> ConfigSource
- System.Etc.Internal.Types: SubConfig :: HashMap Text ConfigValue -> ConfigValue
+ System.Etc.Internal.Types: SubConfig :: !(HashMap Text ConfigValue) -> ConfigValue
- System.Etc.Internal.Types: [configIndex] :: ConfigSource -> Int
+ System.Etc.Internal.Types: [configIndex] :: ConfigSource -> !Int
- System.Etc.Internal.Types: [configMap] :: ConfigValue -> HashMap Text ConfigValue
+ System.Etc.Internal.Types: [configMap] :: ConfigValue -> !(HashMap Text ConfigValue)
- System.Etc.Internal.Types: [configSource] :: ConfigValue -> Set ConfigSource
+ System.Etc.Internal.Types: [configSource] :: ConfigValue -> !(Set ConfigSource)
- System.Etc.Internal.Types: [envVar] :: ConfigSource -> Text
+ System.Etc.Internal.Types: [envVar] :: ConfigSource -> !Text
- System.Etc.Internal.Types: [filepath] :: ConfigSource -> Text
+ System.Etc.Internal.Types: [filepath] :: ConfigSource -> !Text
- System.Etc.Internal.Types: [value] :: ConfigSource -> Value
+ System.Etc.Internal.Types: [value] :: ConfigSource -> !(Value Value)
- System.Etc.Internal.Types: getAllConfigSources :: (IConfig config, MonadThrow m) => [Text] -> config -> m (Set ConfigSource)
+ System.Etc.Internal.Types: getAllConfigSources :: (IConfig config, (MonadThrow m)) => [Text] -> config -> m (Set ConfigSource)
- System.Etc.Internal.Types: getConfigValueWith :: (IConfig config, MonadThrow m) => (Value -> Parser result) -> [Text] -> config -> m result
+ System.Etc.Internal.Types: getConfigValueWith :: (IConfig config, (MonadThrow m)) => (Value -> Parser result) -> [Text] -> config -> m result
- System.Etc.Internal.Types: getSelectedConfigSource :: (IConfig config, MonadThrow m) => [Text] -> config -> m ConfigSource
+ System.Etc.Internal.Types: getSelectedConfigSource :: (IConfig config, (MonadThrow m)) => [Text] -> config -> m ConfigSource
- System.Etc.Spec: parseConfigSpec :: (MonadCatch m) => Text -> m (ConfigSpec ())
+ System.Etc.Spec: parseConfigSpec :: (MonadThrow m) => Text -> m (ConfigSpec ())

Files

CHANGELOG.md view
@@ -1,3 +1,18 @@+0.3.0.0+----+**BREAKING CHANGES**++* Bump resolver to lts-11+* Replace `protolude` in favor of `rio`+* Update `parseConfigSpec` to no longer attempt to parse JSON when `yaml` cabal+   flag is set+* Add `sensitive` setting to `etc/spec` entries+* Add `Value` type to deal with sensitive values+* Update examples with `sensitive` values+* Add optional key context to the `InvalidConfiguration` error (issue #12)+* Add config printer function that renders config map with/without colors (issue #15)+* Give precedence to values that do not contain inner `JSON.Null` values (issue #16)+ 0.2.0.0 ---- * Move `Config` API to typeclass `IConfig`
etc.cabal view
@@ -1,150 +1,139 @@--- This file has been generated from package.yaml by hpack version 0.17.0.------ see: https://github.com/sol/hpack--name:           etc-version:        0.2.0.0-synopsis:       Declarative configuration spec for Haskell projects-description:    Please see README.md-category:       Configuration, System-homepage:       https://github.com/roman/Haskell-etc-author:         Roman Gonzalez-maintainer:     romanandreg@gmail.com-copyright:      2017 Roman Gonzalez-tested-with:    GHC == 7.10.3, GHC == 8.0.1, GHC == 8.0.2-license:        MIT-license-file:   LICENSE-build-type:     Simple-cabal-version:  >= 1.10+name: etc+version: 0.3.0.0+cabal-version: >=1.10+build-type: Simple+license: MIT+license-file: LICENSE+copyright: 2017, 2018 Roman Gonzalez+maintainer: open-source@roman-gonzalez.info+homepage: https://github.com/roman/Haskell-etc+synopsis: Declarative configuration spec for Haskell projects+description:+    `etc` gathers configuration values from multiple sources (cli options, OS+    environment variables, files) using a declarative spec file that defines where+    these values are to be found and located in a configuration map.+category: Configuration, System+author: Roman Gonzalez+tested-with: GHC ==8.0.1 GHC ==8.0.2 GHC ==8.2.2 GHC ==8.4.2 data-files:     test/fixtures/config.foo     test/fixtures/config.json+    test/fixtures/config.null.json     test/fixtures/config.spec.yaml     test/fixtures/config.yaml     test/fixtures/config.yml extra-source-files:-  README.md-  CHANGELOG.md+    README.md+    CHANGELOG.md  source-repository head-  type: git-  location: https://github.com/roman/Haskell-etc+    type: git+    location: https://github.com/roman/Haskell-etc  flag extra-  description: Include extra utilities-  manual: False-  default: False+    description:+        Include extra utilities+    default: False  flag cli-  description: Include support for cli arguments-  manual: False-  default: False+    description:+        Include support for cli arguments+    default: False  flag yaml-  description: Include support to parse YAML files-  manual: False-  default: False+    description:+        Include support to parse YAML files+    default: False  library-  hs-source-dirs: src-  ghc-options: -Wall-  build-depends:-      base >=4.7 && <5-    , aeson >=0.11 && <1.3-    , bytestring >=0.10 && <0.11-    , containers >=0.5 && <0.6-    , text >=1.2 && <1.3-    , protolude >=0.1 && <0.2-    , unordered-containers >=0.2 && <0.3-    , directory >=1.2 && <1.4-    , exceptions >=0.8 && <0.9-    , hashable >=1.2 && <1.3-    , vector >=0.11 && <0.13---  exposed-modules:-      System.Etc-      System.Etc.Spec-      System.Etc.Internal.Config-      System.Etc.Internal.Spec.JSON-      System.Etc.Internal.Spec.Types-      System.Etc.Internal.Types-      System.Etc.Internal.Resolver.Default-      System.Etc.Internal.Resolver.File-      System.Etc.Internal.Resolver.Env--  other-modules:-      Paths_etc--  default-language: Haskell2010--  if flag(extra)-    cpp-options: -DWITH_EXTRA-    build-depends:-      ansi-wl-pprint >=0.6 && <0.7,-      edit-distance >=0.2 && <0.3--    exposed-modules:-      System.Etc.Internal.Extra.Printer-      System.Etc.Internal.Extra.EnvMisspell--  if flag(cli)-    cpp-options: -DWITH_CLI-    build-depends:-      optparse-applicative >=0.12 && <0.14+    +    if flag(extra)+        exposed-modules:+            System.Etc.Internal.Extra.Printer+            System.Etc.Internal.Extra.EnvMisspell+        build-depends:+            ansi-wl-pprint >=0.6,+            edit-distance >=0.2+        cpp-options: -DWITH_EXTRA+    +    if flag(cli)+        exposed-modules:+            System.Etc.Internal.Resolver.Cli+            System.Etc.Internal.Resolver.Cli.Common+            System.Etc.Internal.Resolver.Cli.Plain+            System.Etc.Internal.Resolver.Cli.Command+        build-depends:+            optparse-applicative >=0.12+        cpp-options: -DWITH_CLI+    +    if flag(yaml)+        exposed-modules:+            System.Etc.Internal.Spec.YAML+        build-depends:+            yaml >=0.8+        cpp-options: -DWITH_YAML     exposed-modules:-      System.Etc.Internal.Resolver.Cli-      System.Etc.Internal.Resolver.Cli.Common-      System.Etc.Internal.Resolver.Cli.Plain-      System.Etc.Internal.Resolver.Cli.Command--  if flag(yaml)-    cpp-options: -DWITH_YAML+        System.Etc+        System.Etc.Spec+        System.Etc.Internal.Config+        System.Etc.Internal.Spec.JSON+        System.Etc.Internal.Spec.Types+        System.Etc.Internal.Types+        System.Etc.Internal.Resolver.Default+        System.Etc.Internal.Resolver.File+        System.Etc.Internal.Resolver.Env     build-depends:-      yaml >=0.8 && <0.9-    exposed-modules:-      System.Etc.Internal.Spec.YAML--test-suite etc-testsuite-  type: exitcode-stdio-1.0-  main-is: TestSuite.hs-  hs-source-dirs:-      test-  ghc-options: -Wall-  build-depends:-      base >=4.7 && <5-    , aeson >=0.11 && <1.3-    , bytestring >=0.10 && <0.11-    , containers >=0.5 && <0.6-    , text >=1.2 && <1.3-    , protolude >=0.1 && <0.2-    , unordered-containers >=0.2 && <0.3-    , vector >=0.11 && <0.13-    , tasty >=0.11 && <0.12-    , tasty-hunit >=0.9 && <0.10-    , tasty-rerun >=1.1 && <1.2-    , etc+        base >=4.7 && <5,+        aeson >=0.11,+        hashable >=1.2,+        rio >=0.0.1.0,+        text >=0.0.1.0,+        typed-process >=0.1.1,+        unliftio >=0.1.1.0+    default-language: Haskell2010+    hs-source-dirs: src+    other-modules:+        Paths_etc+    ghc-options: -Wall -  if flag(cli)-    cpp-options: -DWITH_CLI-    build-depends:-      optparse-applicative >=0.12 && <0.14-  if flag(yaml)-    cpp-options: -DWITH_YAML-    build-depends:-      yaml >=0.8 && <0.9-  if flag(extra)-    cpp-options: -DWITH_EXTRA+test-suite  etc-testsuite+    +    if flag(cli)+        build-depends:+            optparse-applicative >=0.12+        cpp-options: -DWITH_CLI+        other-modules:+            System.Etc.Resolver.CliTest+            System.Etc.Resolver.Cli.PlainTest+            System.Etc.Resolver.Cli.CommandTest+    +    if flag(yaml)+        build-depends:+            yaml >=0.8+        cpp-options: -DWITH_YAML+    +    if flag(extra)+        build-depends:+            edit-distance >=0.2+        cpp-options: -DWITH_EXTRA+        other-modules:+            System.Etc.Extra.EnvMisspellTest+    type: exitcode-stdio-1.0+    main-is: TestSuite.hs     build-depends:-      edit-distance >=0.2 && <0.3-  other-modules:-      Paths_etc-      System.Etc.Resolver.CliTest-      System.Etc.Resolver.Cli.CommandTest-      System.Etc.Resolver.Cli.PlainTest-      System.Etc.Resolver.DefaultTest-      System.Etc.Resolver.EnvTest-      System.Etc.Resolver.FileTest-      System.Etc.Extra.EnvMisspellTest-      System.Etc.SpecTest-  default-language: Haskell2010+        base >=4.7,+        aeson >=0.11,+        rio >=0.0.1.0,+        tasty >=0.11,+        tasty-hunit >=0.9,+        etc -any+    default-language: Haskell2010+    hs-source-dirs: test+    other-modules:+        Paths_etc+        System.Etc.Resolver.DefaultTest+        System.Etc.Resolver.EnvTest+        System.Etc.Resolver.FileTest+        System.Etc.SpecTest+        System.Etc.ConfigTest+    ghc-options: -Wall
src/System/Etc.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE CPP               #-} {-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-} + {-|  -}@@ -11,6 +11,7 @@   -- $config     Config   , IConfig+  , Value(..)   , getConfigValue   , getConfigValueWith   , getSelectedConfigSource@@ -48,6 +49,7 @@   -- * Extra utilities   -- $extra   , renderConfig+  , renderConfigColor   , printPrettyConfig   , hPrintPrettyConfig @@ -62,7 +64,7 @@  import System.Etc.Internal.Resolver.Default (resolveDefault) import System.Etc.Internal.Types-    (Config, ConfigSource (..), ConfigValue, IConfig (..))+    (Config, ConfigSource (..), ConfigValue, IConfig (..), Value (..)) import System.Etc.Spec     (ConfigSpec, ConfigurationError (..), parseConfigSpec, readConfigSpec) @@ -82,7 +84,7 @@     , reportEnvMisspellingWarnings     ) import System.Etc.Internal.Extra.Printer-    (hPrintPrettyConfig, printPrettyConfig, renderConfig)+    (hPrintPrettyConfig, printPrettyConfig, renderConfig, renderConfigColor) #endif  import System.Etc.Internal.Config ()
src/System/Etc/Internal/Config.hs view
@@ -4,153 +4,104 @@ {-# LANGUAGE OverloadedStrings #-} module System.Etc.Internal.Config where -import Protolude--import Control.Monad.Catch (MonadThrow (..))+import           RIO+import qualified RIO.HashMap as HashMap+import qualified RIO.Set     as Set+import qualified RIO.Text    as Text  import qualified Data.Aeson          as JSON import qualified Data.Aeson.Internal as JSON (IResult (..), formatError, iparse) import qualified Data.Aeson.Types    as JSON (Parser)-import qualified Data.HashMap.Strict as HashMap-import qualified Data.Set            as Set-import qualified Data.Text           as Text  import System.Etc.Internal.Types  --------------------------------------------------------------------------------  configValueToJsonObject :: ConfigValue -> JSON.Value-configValueToJsonObject configValue =-  case configValue of-    ConfigValue sources ->-      case Set.maxView sources of-        Nothing ->-          undefined+configValueToJsonObject configValue = case configValue of+  ConfigValue sources -> case Set.maxView sources of+    Nothing          -> error "this should not happen" -        Just (source, _) ->-          value source+    Just (source, _) -> fromValue $ value source -    SubConfig configm ->-      configm+  SubConfig configm ->+    configm       & HashMap.foldrWithKey           (\key innerConfigValue acc ->-              HashMap.insert key (configValueToJsonObject innerConfigValue) acc)+            HashMap.insert key (configValueToJsonObject innerConfigValue) acc+          )           HashMap.empty       & JSON.Object  _getConfigValueWith-  :: MonadThrow m-  => (JSON.Value -> JSON.Parser result)-  -> [Text]-  -> Config-  -> m result+  :: MonadThrow m => (JSON.Value -> JSON.Parser result) -> [Text] -> Config -> m result _getConfigValueWith parser keys0 (Config configValue0) =   let-    loop keys configValue =-      case (keys, configValue) of-        ([], ConfigValue sources) ->-          case Set.maxView sources of-            Nothing ->-              throwM $ InvalidConfigKeyPath keys0+    loop keys configValue = case (keys, configValue) of+      ([], ConfigValue sources) -> case Set.maxView sources of+        Nothing          -> throwM $ InvalidConfigKeyPath keys0 -            Just (None, _) ->-              throwM $ InvalidConfigKeyPath keys0+        Just (None  , _) -> throwM $ InvalidConfigKeyPath keys0 -            Just (source, _) ->-              case JSON.iparse parser (value source) of-                JSON.IError path err ->-                  JSON.formatError path err-                  & Text.pack-                  & InvalidConfiguration-                  & throwM+        Just (source, _) -> case JSON.iparse parser (fromValue $ value source) of -                JSON.ISuccess result ->-                  return result+          JSON.IError path err ->+            let key = keys0 & reverse & Text.intercalate "."+            in  JSON.formatError path err+                & Text.pack+                & InvalidConfiguration (Just key)+                & throwM -        ([], innerConfigValue) ->-          case JSON.iparse parser (configValueToJsonObject innerConfigValue) of-            JSON.IError path err ->-              JSON.formatError path err-              & Text.pack-              & InvalidConfiguration-              & throwM+          JSON.ISuccess result -> return result -            JSON.ISuccess result ->-              return result+      ([], innerConfigValue) ->+        case JSON.iparse parser (configValueToJsonObject innerConfigValue) of+          JSON.IError path err ->+            let key = keys0 & reverse & Text.intercalate "."+            in  JSON.formatError path err+                & Text.pack+                & InvalidConfiguration (Just key)+                & throwM -        (k:keys1, SubConfig configm) ->-          case HashMap.lookup k configm of-            Nothing ->-              throwM $ InvalidConfigKeyPath keys0-            Just configValue1 ->-              loop keys1 configValue1+          JSON.ISuccess result -> return result -        _ ->-          throwM $ InvalidConfigKeyPath keys0-  in-    loop keys0 configValue0+      (k : keys1, SubConfig configm) -> case HashMap.lookup k configm of+        Nothing           -> throwM $ InvalidConfigKeyPath keys0+        Just configValue1 -> loop keys1 configValue1 -_getSelectedConfigSource-  :: (MonadThrow m)-  => [Text]-  -> Config-  -> m ConfigSource+      _ -> throwM $ InvalidConfigKeyPath keys0+  in  loop keys0 configValue0++_getSelectedConfigSource :: (MonadThrow m) => [Text] -> Config -> m ConfigSource _getSelectedConfigSource keys0 (Config configValue0) =-  let-    loop keys configValue =-      case (keys, configValue) of-        ([], ConfigValue sources) ->-          case Set.maxView sources of-            Nothing ->-              throwM $ InvalidConfigKeyPath keys0+  let loop keys configValue = case (keys, configValue) of+        ([], ConfigValue sources) -> case Set.maxView sources of+          Nothing          -> throwM $ InvalidConfigKeyPath keys0 -            Just (source, _) ->-              return source+          Just (source, _) -> return source -        (k:keys1, SubConfig configm) ->-          case HashMap.lookup k configm of-            Nothing ->-              throwM $ InvalidConfigKeyPath keys0-            Just configValue1 ->-              loop keys1 configValue1+        (k : keys1, SubConfig configm) -> case HashMap.lookup k configm of+          Nothing           -> throwM $ InvalidConfigKeyPath keys0+          Just configValue1 -> loop keys1 configValue1 -        _ ->-          throwM $ InvalidConfigKeyPath keys0-  in-    loop keys0 configValue0+        _ -> throwM $ InvalidConfigKeyPath keys0+  in  loop keys0 configValue0  -_getAllConfigSources-  :: (MonadThrow m)-  => [Text]-  -> Config-  -> m (Set ConfigSource)+_getAllConfigSources :: (MonadThrow m) => [Text] -> Config -> m (Set ConfigSource) _getAllConfigSources keys0 (Config configValue0) =-  let-    loop keys configValue =-      case (keys, configValue) of-        ([], ConfigValue sources) ->-          return sources+  let loop keys configValue = case (keys, configValue) of+        ([]       , ConfigValue sources) -> return sources -        (k:keys1, SubConfig configm) ->-          case HashMap.lookup k configm of-            Nothing ->-              throwM $ InvalidConfigKeyPath keys0-            Just configValue1 ->-              loop keys1 configValue1+        (k : keys1, SubConfig configm  ) -> case HashMap.lookup k configm of+          Nothing           -> throwM $ InvalidConfigKeyPath keys0+          Just configValue1 -> loop keys1 configValue1 -        _ ->-          throwM $ InvalidConfigKeyPath keys0-  in-    loop keys0 configValue0+        _ -> throwM $ InvalidConfigKeyPath keys0+  in  loop keys0 configValue0 -_getConfigValue-  :: (MonadThrow m, JSON.FromJSON result)-  => [Text]-  -> Config-  -> m result-_getConfigValue =-  _getConfigValueWith JSON.parseJSON+_getConfigValue :: (MonadThrow m, JSON.FromJSON result) => [Text] -> Config -> m result+_getConfigValue = _getConfigValueWith JSON.parseJSON   instance IConfig Config where
src/System/Etc/Internal/Extra/EnvMisspell.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE DeriveGeneric     #-}+{-# LANGUAGE FlexibleContexts  #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} module System.Etc.Internal.Extra.EnvMisspell (@@ -10,15 +11,14 @@   , reportEnvMisspellingWarnings   ) where -import Protolude hiding ((<$>), (<>))+import           RIO         hiding ((<$>), (<>))+import qualified RIO.HashMap as HashMap+import qualified RIO.Text    as Text+import qualified RIO.Vector  as Vector -import Data.Vector        (Vector) import System.Environment (getEnvironment) -import qualified Data.HashMap.Strict as HashMap-import qualified Data.Text           as Text-import qualified Data.Vector         as Vector-import qualified Text.EditDistance   as Distance+import qualified Text.EditDistance as Distance  import System.Etc.Internal.Spec.Types import Text.PrettyPrint.ANSI.Leijen@@ -32,74 +32,65 @@  lookupSpecEnvKeys :: ConfigSpec a -> Vector Text lookupSpecEnvKeys spec =-  let-    foldEnvSettings val acc =-      case val of-        ConfigValue _ sources ->+  let foldEnvSettings val acc = case val of+        ConfigValue _defVal _sensitive sources ->           maybe acc (`Vector.cons` acc) (envVar sources)-        SubConfig hsh ->-          HashMap.foldr foldEnvSettings acc hsh-  in-    foldEnvSettings (SubConfig $ specConfigValues spec) Vector.empty+        SubConfig hsh -> HashMap.foldr foldEnvSettings acc hsh+  in  foldEnvSettings (SubConfig $ specConfigValues spec) Vector.empty  {-|  -} getEnvMisspellingsPure :: ConfigSpec a -> Vector Text -> Vector EnvMisspell getEnvMisspellingsPure spec env = do-   specEnvName  <- lookupSpecEnvKeys spec-   currentEnvName <- env+  specEnvName    <- lookupSpecEnvKeys spec+  currentEnvName <- env -   let-     distance =-       Distance.levenshteinDistance-           Distance.defaultEditCosts-           (Text.unpack specEnvName)-           (Text.unpack currentEnvName)+  let distance = Distance.levenshteinDistance Distance.defaultEditCosts+                                              (Text.unpack specEnvName)+                                              (Text.unpack currentEnvName) -   guard (distance >= 1 && distance < 4)-   return $ EnvMisspell currentEnvName specEnvName+  guard (distance >= 1 && distance < 4)+  return $ EnvMisspell currentEnvName specEnvName  {-|  -} getEnvMisspellings :: ConfigSpec a -> IO (Vector EnvMisspell) getEnvMisspellings spec =-  getEnvironment-  & fmap (Vector.fromList . map (Text.pack . fst))-  & fmap (getEnvMisspellingsPure spec)+  getEnvironment & fmap (Vector.fromList . map (Text.pack . fst)) & fmap+    (getEnvMisspellingsPure spec)  {-|  -} renderEnvMisspellings :: Vector EnvMisspell -> Doc renderEnvMisspellings misspells-  | Vector.null misspells =-      mempty-  | otherwise =-      misspells-      & Vector.map-            (\misspell ->-                text "WARNING: Environment variable `"-                <> text (Text.unpack $ currentText misspell)-                <> text "' found, perhaps you meant `"-                <> text (Text.unpack $ suggestionText misspell)-                <> text "'")-      & Vector.foldl' (<$>) mempty-      & (<$> mempty)-      & (<$> mempty)+  | Vector.null misspells+  = mempty+  | otherwise+  = misspells+    & Vector.map+        (\misspell ->+          text "WARNING: Environment variable `"+            <> text (Text.unpack $ currentText misspell)+            <> text "' found, perhaps you meant `"+            <> text (Text.unpack $ suggestionText misspell)+            <> text "'"+        )+    & Vector.foldl' (<$>) mempty+    & (<$> mempty)+    & (<$> mempty)  {-|  -} hPrintEnvMisspellings :: Handle -> Vector EnvMisspell -> IO ()-hPrintEnvMisspellings h =-  hPutDoc h . renderEnvMisspellings+hPrintEnvMisspellings h = hPutDoc h . renderEnvMisspellings  {-|  -} reportEnvMisspellingWarnings :: ConfigSpec a -> IO () reportEnvMisspellingWarnings spec =-  getEnvMisspellings spec >>=-  hPrintEnvMisspellings stderr+  getEnvMisspellings spec >>= hPrintEnvMisspellings stderr
src/System/Etc/Internal/Extra/Printer.hs view
@@ -1,147 +1,120 @@ {-# OPTIONS_GHC -fno-warn-deprecations #-}+{-# LANGUAGE NamedFieldPuns    #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} module System.Etc.Internal.Extra.Printer (     renderConfig+  , renderConfigColor   , printPrettyConfig   , hPrintPrettyConfig   ) where -import Protolude hiding ((<>))+import           RIO         hiding ((<>))+import qualified RIO.HashMap as HashMap+import           RIO.List    (intersperse, maximum)+import qualified RIO.Set     as Set+import qualified RIO.Text    as Text -import qualified Data.Aeson          as JSON-import qualified Data.HashMap.Strict as HashMap-import qualified Data.Set            as Set-import qualified Data.Text           as Text+import qualified Data.Aeson as JSON  import Text.PrettyPrint.ANSI.Leijen  import System.Etc.Internal.Types -renderJsonValue :: JSON.Value -> (Doc, Int)-renderJsonValue value' =-  case value' of-    JSON.Null ->-      (text "null", 4)+renderJsonValue :: Text -> Value JSON.Value -> (Doc, Int)+renderJsonValue key value' = case value' of+  Plain JSON.Null         -> (text "null", 4) -    JSON.String str ->-      (text $ Text.unpack str, Text.length str)+  Plain (JSON.String str) -> (text $ Text.unpack str, Text.length str) -    JSON.Number scientific ->-      let-        number =-          show scientific-      in-        (text number, length number)-    JSON.Bool bool' ->-      if bool' then-        (text "true", 5)-      else-        (text "false", 5)-    _ ->-      value'-      & show+  Plain (JSON.Number scientific) ->+    let number = show scientific in (text number, length number)+  Plain     (JSON.Bool bool') -> if bool' then (text "true", 5) else (text "false", 5)+  Sensitive _                 -> (text "<<sensitive>>", 13)+  _ ->+    value'+      & tshow       & ("Invalid configuration value creation " `mappend`)-      & InvalidConfiguration+      & InvalidConfiguration (Just key)       & show       & error +data ColorFn+  = ColorFn {+    greenColor :: !(Doc -> Doc)+  , blueColor  :: !(Doc -> Doc)+  } -renderConfig :: Config -> Doc-renderConfig (Config configValue0) =+renderConfig' :: ColorFn -> Config -> Doc+renderConfig' ColorFn { greenColor, blueColor } (Config configValue0) =   let     brackets' = enclose (lbracket <> space) (space <> rbracket) -    renderSource :: ConfigSource -> ((Doc, Int), Doc)-    renderSource source' =-      case source' of-        Default value' ->-          ( renderJsonValue value'-          , brackets' (fill 10 (text "Default"))-          )+    renderSource :: Text -> ConfigSource -> ((Doc, Int), Doc)+    renderSource key source' = case source' of+      Default value' -> (renderJsonValue key value', brackets' (fill 10 (text "Default"))) -        File _index filepath' value' ->-          ( renderJsonValue value'-          , brackets' (fill 10 (text "File:" <+> text (Text.unpack filepath')))-          )+      File _index filepath' value' ->+        ( renderJsonValue key value'+        , brackets' (fill 10 (text "File:" <+> text (Text.unpack filepath')))+        ) -        Env varname value' ->-          ( renderJsonValue value'-          , brackets' (fill 10 (text "Env:" <+> text (Text.unpack varname)))-          )+      Env varname value' ->+        ( renderJsonValue key value'+        , brackets' (fill 10 (text "Env:" <+> text (Text.unpack varname)))+        ) -        Cli value' ->-          ( renderJsonValue value'-          , brackets' (fill 10 (text "Cli"))-          )+      Cli value' -> (renderJsonValue key value', brackets' (fill 10 (text "Cli"))) -        None ->-          ( (mempty, 0)-          , mempty-          )+      None       -> ((mempty, 0), mempty) -    renderSources :: [ConfigSource] -> Doc-    renderSources sources0 =+    renderSources :: Text -> [ConfigSource] -> Doc+    renderSources keys sources0 =       let-        sources@(((selValueDoc, _), selSourceDoc):others) =-          map renderSource sources0+        sources@(((selValueDoc, _), selSourceDoc) : others) =+          map (renderSource keys) sources0 -        fillingWidth =-          sources-          & map (snd . fst)-          & maximum-          & max 10+        fillingWidth  = sources & map (snd . fst) & maximum & max 10 -        selectedValue =-          [ green $ fill fillingWidth selValueDoc <+> selSourceDoc ]+        selectedValue = [greenColor $ fill fillingWidth selValueDoc <+> selSourceDoc] -        otherValues =-          map (\((valueDoc, _), sourceDoc) ->-                  fill fillingWidth valueDoc <+> sourceDoc)-              others+        otherValues   = map+          (\((valueDoc, _), sourceDoc) -> fill fillingWidth valueDoc <+> sourceDoc)+          others       in-        selectedValue-        & flip mappend otherValues-        & vcat-        & indent 2+        selectedValue & flip mappend otherValues & vcat & indent 2      configEntryRenderer :: [Text] -> [Doc] -> Text -> ConfigValue -> [Doc]     configEntryRenderer keys resultDoc configKey configValue =       resultDoc `mappend` loop (configKey : keys) configValue -    loop keys configValue =-      case configValue of-        SubConfig subConfigm ->-          HashMap.foldlWithKey'-            (configEntryRenderer keys)-            mempty-            subConfigm+    loop keys configValue = case configValue of+      SubConfig subConfigm ->+        HashMap.foldlWithKey' (configEntryRenderer keys) mempty subConfigm -        ConfigValue sources0 ->-          let-            configKey =-              keys-              & reverse-              & Text.intercalate "."+      ConfigValue sources0 ->+        let+          configKey = keys & reverse & Text.intercalate "." -            sources =-              Set.toDescList sources0-          in-            if null sources then-              []-            else-              [ blue (text (Text.unpack configKey))-               <$$> renderSources sources ]+          sources   = Set.toDescList sources0+        in+          if null sources+          then+            []+          else+            [blueColor (text (Text.unpack configKey)) <$$> renderSources configKey sources]   in-    loop [] configValue0-    & intersperse (linebreak <> linebreak)-    & hcat-    & (<> linebreak)+    loop [] configValue0 & intersperse (linebreak <> linebreak) & hcat & (<> linebreak) ++renderConfigColor :: Config -> Doc+renderConfigColor = renderConfig' ColorFn {greenColor = green, blueColor = blue}++renderConfig :: Config -> Doc+renderConfig = renderConfig' ColorFn {greenColor = id, blueColor = id}+ printPrettyConfig :: Config -> IO ()-printPrettyConfig =-  putDoc . renderConfig+printPrettyConfig = putDoc . renderConfig  hPrintPrettyConfig :: Handle -> Config -> IO ()-hPrintPrettyConfig handle' =-  hPutDoc handle' . renderConfig+hPrintPrettyConfig handle' = hPutDoc handle' . renderConfig
src/System/Etc/Internal/Resolver/Cli.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}+ module System.Etc.Internal.Resolver.Cli   ( PlainConfigSpec   -- , resolveCommandCli
src/System/Etc/Internal/Resolver/Cli/Command.hs view
@@ -3,17 +3,14 @@ {-# LANGUAGE OverloadedStrings #-} module System.Etc.Internal.Resolver.Cli.Command (resolveCommandCli, resolveCommandCliPure) where -import Protolude+import           RIO+import qualified RIO.HashMap as HashMap+import qualified RIO.Text    as Text -import Control.Monad.Catch (MonadThrow, throwM)-import Data.Hashable       (Hashable)-import Data.HashMap.Strict (HashMap)-import Data.Vector         (Vector)-import System.Environment  (getProgName)+import Data.Hashable      (Hashable)+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@@ -25,194 +22,148 @@  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-             )+  => Bool+  -> Spec.CliEntrySpec cmd+  -> m (Vector cmd, Opt.Parser (Maybe (Value JSON.Value)))+entrySpecToJsonCli sensitive entrySpec = case entrySpec of+  Spec.CmdEntry commandJsonValue specSettings ->+    return (commandJsonValue, settingsToJsonCli sensitive specSettings) -    Spec.PlainEntry {} ->-      throwM CommandKeyMissing+  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+  => HashMap cmd (Opt.Parser ConfigValue)+  -> Text+  -> Bool+  -> Spec.ConfigSources cmd+  -> m (HashMap cmd (Opt.Parser ConfigValue))+configValueSpecToCli acc0 specEntryKey sensitive 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+            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+        Just entrySpec -> do+          (commands, jsonOptParser) <- entrySpecToJsonCli sensitive entrySpec -        let-          configValueParser =-            jsonToConfigValue <$> jsonOptParser+          let configValueParser = jsonToConfigValue <$> jsonOptParser -        foldM (\acc command ->-                 acc-                   & HashMap.alter-                        (\mAccParser ->-                          mAccParser-                            & fromMaybe (pure mempty)-                            & updateAccConfigOptParser configValueParser-                            & Just)-                        command-                   & return)-              acc0-              commands+          foldM+            (\acc command ->+              acc+                & HashMap.alter+                    (\mAccParser ->+                      mAccParser+                        & fromMaybe (pure mempty)+                        & 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))+  => 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+  let updateAccConfigOptParser subConfigParser accOptParser =+        (\subConfig accSubConfig -> case accSubConfig of+            ConfigValue{} -> accSubConfig              SubConfig subConfigMap ->-              subConfigMap-                & HashMap.alter (const $ Just subConfig) specEntryKey-                & SubConfig)-        <$> subConfigParser-        <*> accOptParser+              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)+      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)+          Just accOptParser -> Just $ updateAccConfigOptParser subConfigParser accOptParser+        )         command--  in do-     parserPerCommand <--       foldM specToConfigValueCli-             HashMap.empty-             (HashMap.toList subConfigSpec)+  in  do+        parserPerCommand <- foldM specToConfigValueCli+                                  HashMap.empty+                                  (HashMap.toList subConfigSpec) -     parserPerCommand-       & HashMap.foldrWithKey-                  addSubParserCommand-                  acc-       & return+        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+  => HashMap cmd (Opt.Parser ConfigValue)+  -> (Text, Spec.ConfigValue cmd)+  -> m (HashMap cmd (Opt.Parser ConfigValue))+specToConfigValueCli acc (specEntryKey, specConfigValue) = case specConfigValue of+  Spec.ConfigValue _ sensitive sources ->+    configValueSpecToCli acc specEntryKey sensitive sources -    Spec.SubConfig subConfigSpec ->-      subConfigSpecToCli-        specEntryKey-        subConfigSpec-        acc+  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))+  => 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+  let zeroParser   = pure $ SubConfig HashMap.empty -  in-    case commandsSpec of-      Nothing ->-        throwM CommandsKeyNotDefined+      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)+        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)-    => HashMap cmd (Opt.Parser ConfigValue)-    -> m (Opt.Parser (cmd, Config))+  => 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 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+              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)+              [acc] & (++ commandParsers) & mconcat & return+  in  Opt.subparser <$> foldM joinParser Opt.idm (HashMap.toList parserPerCommand)  specToConfigCli   :: (MonadThrow m, JSON.FromJSON cmd, JSON.ToJSON cmd, Eq cmd, Hashable cmd)-    => Spec.ConfigSpec cmd-    -> m (Opt.Parser (cmd, Config))+  => Spec.ConfigSpec cmd+  -> m (Opt.Parser (cmd, Config)) specToConfigCli spec = do-  acc <- configValueCliAccInit spec-  parsers <--    foldM specToConfigValueCli-          acc-          (HashMap.toList $ Spec.specConfigValues spec)+  acc     <- configValueCliAccInit spec+  parsers <- foldM specToConfigValueCli acc (HashMap.toList $ Spec.specConfigValues spec)    joinCommandParsers parsers @@ -228,37 +179,25 @@ -} 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+  => 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+    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+    programParser = Opt.info (Opt.helper <*> configParser) programModFlags      programResult =-      args-        & map Text.unpack-        & Opt.execParserPure Opt.defaultPrefs programParser+      args & map Text.unpack & Opt.execParserPure Opt.defaultPrefs programParser    programResultToResolverResult progName programResult @@ -275,11 +214,10 @@ -} 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+  => 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+  handleCliResult $ resolveCommandCliPure configSpec progName args
src/System/Etc/Internal/Resolver/Cli/Common.hs view
@@ -4,19 +4,21 @@ {-# LANGUAGE OverloadedStrings #-} module System.Etc.Internal.Resolver.Cli.Common where -import qualified Prelude   as P-import           Protolude+import qualified Prelude as P -import           Control.Monad.Catch        (MonadThrow, throwM)-import qualified Data.Aeson                 as JSON-import qualified Data.Aeson.Internal        as JSON (IResult (..), iparse)-import qualified Data.ByteString.Lazy.Char8 as BL (unpack)-import qualified Data.Set                   as Set-import qualified Data.Text                  as Text-import qualified Data.Text.IO               as Text-import qualified Data.Vector                as Vector-import qualified Options.Applicative        as Opt+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 @@ -57,144 +59,86 @@ --------------------------------------------------------------------------------  specToCliSwitchFieldMod specSettings =-  maybe Opt.idm-        (Opt.long . Text.unpack)-        (Spec.optLong specSettings)-  `mappend` maybe Opt.idm-                  (Opt.short . Text.head)-                  (Spec.optShort specSettings)-  `mappend` maybe Opt.idm-                  (Opt.help . Text.unpack)-                  (Spec.optHelp specSettings)+  maybe Opt.idm (Opt.long . Text.unpack) (Spec.optLong specSettings)+    `mappend` maybe Opt.idm (Opt.short . Text.head)  (Spec.optShort specSettings)+    `mappend` maybe Opt.idm (Opt.help . Text.unpack) (Spec.optHelp specSettings) -specToCliVarFieldMod specSettings =-  specToCliSwitchFieldMod specSettings-  `mappend` maybe Opt.idm-                  (Opt.metavar . Text.unpack)-                  (Spec.optMetavar specSettings)+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.unpack-        & Text.pack-        & InvalidCliCommandKey-        & throwM+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-  :: Spec.CliEntryMetadata-    -> Opt.Parser (Maybe JSON.Value)-settingsToJsonCli specSettings =-  let-    requiredCombinator =-        if Spec.optRequired specSettings then-          (Just <$>)-        else-          Opt.optional+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 ->-            (JSON.String . Text.pack)-            <$> Opt.strOption (specToCliVarFieldMod specSettings)+    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 ->-            (JSON.Number . fromInteger)+        Spec.NumberOpt ->+          (boolToValue sensitive . JSON.Number . fromInteger)             <$> Opt.option Opt.auto (specToCliVarFieldMod specSettings) -          Spec.SwitchOpt ->-            JSON.Bool-            <$> Opt.switch (specToCliSwitchFieldMod specSettings)+        Spec.SwitchOpt -> (boolToValue sensitive . JSON.Bool)+          <$> Opt.switch (specToCliSwitchFieldMod specSettings) -      Spec.Arg {} ->-        case Spec.argValueType specSettings of-          Spec.StringArg ->-            (JSON.String . Text.pack)-            <$> Opt.strArgument ( specSettings-                                  & Spec.argMetavar-                                  & maybe Opt.idm (Opt.metavar . Text.unpack))-          Spec.NumberArg ->-            (JSON.Number . fromInteger)-            <$> Opt.argument Opt.auto-                             ( specSettings-                                  & Spec.argMetavar-                                  & maybe Opt.idm (Opt.metavar . Text.unpack))+      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)+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+  JSON.ISuccess result  -> return result -jsonToConfigValue-  :: Maybe JSON.Value-    -> ConfigValue+jsonToConfigValue :: Maybe (Value JSON.Value) -> ConfigValue jsonToConfigValue specEntryDefVal =-  ConfigValue-    $ Set.fromList-    $ maybe [] ((:[]) . Cli) specEntryDefVal+  ConfigValue $ Set.fromList $ maybe [] ((: []) . Cli) specEntryDefVal -handleCliResult-  :: Either SomeException a -> IO a-handleCliResult result =-  case result of-    Right config ->-      return config+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 >>= putStrLn-          exitSuccess+  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+    Just (CliEvalExited exitCode (GetErrorMessage getMsg)) -> do+      getMsg >>= Text.hPutStrLn stderr+      exitWith exitCode -        _ ->-          throwIO err+    _ -> throwIO err -programResultToResolverResult-  :: MonadThrow m-    => Text-    -> Opt.ParserResult a-    -> m a-programResultToResolverResult progName programResult =-  case programResult of-    Opt.Success result ->-      return result+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.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)+  Opt.CompletionInvoked compl ->+    let getMsg = Text.pack <$> Opt.execCompletion compl (Text.unpack progName)+    in  throwM $ CliEvalExited ExitSuccess (GetErrorMessage getMsg)
src/System/Etc/Internal/Resolver/Cli/Plain.hs view
@@ -1,15 +1,14 @@ {-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}+ module System.Etc.Internal.Resolver.Cli.Plain (PlainConfigSpec, resolvePlainCli, resolvePlainCliPure) where -import Protolude+import           RIO+import qualified RIO.HashMap as HashMap+import qualified RIO.Text    as Text -import           Control.Monad.Catch (MonadThrow, throwM) 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.Environment  (getProgName)+import           System.Environment  (getArgs, getProgName)  import           System.Etc.Internal.Resolver.Cli.Common import qualified System.Etc.Internal.Spec.Types          as Spec@@ -24,133 +23,94 @@  entrySpecToConfigValueCli   :: (MonadThrow m)-    => Spec.CliEntrySpec ()-    -> m (Opt.Parser (Maybe JSON.Value))-entrySpecToConfigValueCli entrySpec =-  case entrySpec of-    Spec.CmdEntry {} ->-      throwM CommandKeyOnPlainCli+  => Bool+  -> Spec.CliEntrySpec ()+  -> m (Opt.Parser (Maybe (Value JSON.Value)))+entrySpecToConfigValueCli sensitive entrySpec = case entrySpec of+  Spec.CmdEntry{}              -> throwM CommandKeyOnPlainCli -    Spec.PlainEntry specSettings ->-      return (settingsToJsonCli specSettings)+  Spec.PlainEntry specSettings -> return (settingsToJsonCli sensitive specSettings)   configValueSpecToCli   :: (MonadThrow m)-    => Text-    -> Spec.ConfigSources ()-    -> Opt.Parser ConfigValue-    -> m (Opt.Parser ConfigValue)-configValueSpecToCli specEntryKey sources acc =-  let-    updateAccConfigOptParser configValueParser accOptParser =-      (\configValue accSubConfig ->-        case accSubConfig of-          ConfigValue {} ->-            accSubConfig+  => Text+  -> Bool+  -> Spec.ConfigSources ()+  -> Opt.Parser ConfigValue+  -> m (Opt.Parser ConfigValue)+configValueSpecToCli specEntryKey sensitive sources acc =+  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 acc+            SubConfig subConfigMap ->+              subConfigMap & HashMap.alter (const $ Just configValue) specEntryKey & SubConfig+          )+          <$> configValueParser+          <*> accOptParser+  in  case Spec.cliEntry sources of+        Nothing        -> return acc -      Just entrySpec -> do-        jsonOptParser <- entrySpecToConfigValueCli entrySpec+        Just entrySpec -> do+          jsonOptParser <- entrySpecToConfigValueCli sensitive entrySpec -        let-          configValueParser =-            jsonToConfigValue <$> jsonOptParser+          let configValueParser = jsonToConfigValue <$> jsonOptParser -        return $ updateAccConfigOptParser configValueParser acc+          return $ updateAccConfigOptParser configValueParser acc  subConfigSpecToCli   :: (MonadThrow m)-    => Text-    -> HashMap.HashMap Text (Spec.ConfigValue ())-    -> Opt.Parser ConfigValue-    -> m (Opt.Parser ConfigValue)+  => Text+  -> HashMap.HashMap Text (Spec.ConfigValue ())+  -> Opt.Parser ConfigValue+  -> m (Opt.Parser ConfigValue) subConfigSpecToCli specEntryKey subConfigSpec acc =-  let-    updateAccConfigOptParser subConfigParser accOptParser =-      (\subConfig accSubConfig ->-          case accSubConfig of-            ConfigValue {} ->-              accSubConfig+  let updateAccConfigOptParser subConfigParser accOptParser =+        (\subConfig accSubConfig -> case accSubConfig of+            ConfigValue{} -> accSubConfig              SubConfig subConfigMap ->-              subConfigMap-                & HashMap.alter (const $ Just subConfig) specEntryKey-                & SubConfig)-        <$> subConfigParser-        <*> accOptParser-  in do-    configOptParser <--       foldM specToConfigValueCli-             (pure $ SubConfig HashMap.empty)-             (HashMap.toList subConfigSpec)+              subConfigMap & HashMap.alter (const $ Just subConfig) specEntryKey & SubConfig+          )+          <$> subConfigParser+          <*> accOptParser+  in  do+        configOptParser <- foldM specToConfigValueCli+                                 (pure $ SubConfig HashMap.empty)+                                 (HashMap.toList subConfigSpec) -    return-      $ updateAccConfigOptParser configOptParser acc+        return $ updateAccConfigOptParser configOptParser acc  specToConfigValueCli   :: (MonadThrow m)-    => Opt.Parser ConfigValue-    -> (Text, Spec.ConfigValue ())-    -> m (Opt.Parser ConfigValue)-specToConfigValueCli acc (specEntryKey, specConfigValue) =-  case specConfigValue of-    Spec.ConfigValue _ sources ->-      configValueSpecToCli-        specEntryKey-        sources-        acc+  => Opt.Parser ConfigValue+  -> (Text, Spec.ConfigValue ())+  -> m (Opt.Parser ConfigValue)+specToConfigValueCli acc (specEntryKey, specConfigValue) = case specConfigValue of+  Spec.ConfigValue _ sensitive sources ->+    configValueSpecToCli specEntryKey sensitive sources acc -    Spec.SubConfig subConfigSpec ->-      subConfigSpecToCli-        specEntryKey-        subConfigSpec-        acc+  Spec.SubConfig subConfigSpec -> subConfigSpecToCli specEntryKey subConfigSpec acc -configValueCliAccInit-  :: (MonadThrow m)-    => Spec.ConfigSpec ()-    -> m (Opt.Parser ConfigValue)+configValueCliAccInit :: (MonadThrow m) => Spec.ConfigSpec () -> m (Opt.Parser ConfigValue) configValueCliAccInit spec =-  let-    zeroParser =-      pure $ SubConfig HashMap.empty+  let zeroParser   = pure $ SubConfig HashMap.empty -    commandsSpec = do-      programSpec <- Spec.specCliProgramSpec spec-      Spec.cliCommands programSpec-  in-    case commandsSpec of-      Nothing ->-        return zeroParser+      commandsSpec = do+        programSpec <- Spec.specCliProgramSpec spec+        Spec.cliCommands programSpec+  in  case commandsSpec of+        Nothing -> return zeroParser -      Just _ ->-        throwM CommandKeyOnPlainCli+        Just _  -> throwM CommandKeyOnPlainCli -specToConfigCli-  :: (MonadThrow m)-    => Spec.ConfigSpec ()-    -> m (Opt.Parser Config)+specToConfigCli :: (MonadThrow m) => Spec.ConfigSpec () -> m (Opt.Parser Config) specToConfigCli spec = do-  acc <- configValueCliAccInit spec-  parser <--    foldM specToConfigValueCli-          acc-          (HashMap.toList $ Spec.specConfigValues spec)+  acc    <- configValueCliAccInit spec+  parser <- foldM specToConfigValueCli acc (HashMap.toList $ Spec.specConfigValues spec) -  parser-    & (Config <$>)-    & return+  parser & (Config <$>) & return  {-| @@ -172,29 +132,17 @@   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+    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+    programParser = Opt.info (Opt.helper <*> configParser) programModFlags      programResult =-      args-        & map Text.unpack-        & Opt.execParserPure Opt.defaultPrefs programParser+      args & map Text.unpack & Opt.execParserPure Opt.defaultPrefs programParser    programResultToResolverResult progName programResult @@ -215,5 +163,4 @@   progName <- Text.pack <$> getProgName   args     <- map Text.pack <$> getArgs -  handleCliResult-    $ resolvePlainCliPure configSpec progName args+  handleCliResult $ resolvePlainCliPure configSpec progName args
src/System/Etc/Internal/Resolver/Default.hs view
@@ -1,55 +1,42 @@ {-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}+ module System.Etc.Internal.Resolver.Default (resolveDefault) where -import Protolude+import           RIO+import qualified RIO.HashMap as HashMap+import qualified RIO.Set     as Set -import qualified Data.Aeson          as JSON-import qualified Data.HashMap.Strict as HashMap-import qualified Data.Set            as Set+import qualified Data.Aeson as JSON  import qualified System.Etc.Internal.Spec.Types as Spec import           System.Etc.Internal.Types -toDefaultConfigValue :: JSON.Value -> ConfigValue-toDefaultConfigValue =-  ConfigValue . Set.singleton . Default+toDefaultConfigValue :: Bool -> JSON.Value -> ConfigValue+toDefaultConfigValue sensitive =+  ConfigValue . Set.singleton . Default . boolToValue sensitive  buildDefaultResolver :: Spec.ConfigSpec cmd -> Maybe ConfigValue buildDefaultResolver spec =-  let-    resolverReducer :: Text -> Spec.ConfigValue cmd -> Maybe ConfigValue -> Maybe ConfigValue-    resolverReducer specKey specValue mConfig =-      case specValue of-        Spec.ConfigValue def _ ->-          let-            mConfigSource =-              toDefaultConfigValue <$> def+  let resolverReducer+        :: Text -> Spec.ConfigValue cmd -> Maybe ConfigValue -> Maybe ConfigValue+      resolverReducer specKey specValue mConfig = case specValue of+        Spec.ConfigValue def sensitive _ ->+          let mConfigSource = toDefaultConfigValue sensitive <$> def -            updateConfig =-              writeInSubConfig specKey <$> mConfigSource <*> mConfig-          in-            updateConfig <|> mConfig+              updateConfig  = writeInSubConfig specKey <$> mConfigSource <*> mConfig+          in  updateConfig <|> mConfig          Spec.SubConfig specConfigMap ->-          let-            mSubConfig =-              specConfigMap-              & HashMap.foldrWithKey-                    resolverReducer-                    (Just emptySubConfig)-              & filterMaybe isEmptySubConfig+          let mSubConfig =+                specConfigMap+                  & HashMap.foldrWithKey resolverReducer (Just emptySubConfig)+                  & filterMaybe isEmptySubConfig -            updateConfig =-              writeInSubConfig specKey <$> mSubConfig <*> mConfig-          in-            updateConfig <|> mConfig-  in-    Spec.specConfigValues spec-    & HashMap.foldrWithKey-          resolverReducer-          (Just emptySubConfig)-    & filterMaybe isEmptySubConfig+              updateConfig = writeInSubConfig specKey <$> mSubConfig <*> mConfig+          in  updateConfig <|> mConfig+  in  Spec.specConfigValues spec+      & HashMap.foldrWithKey resolverReducer (Just emptySubConfig)+      & filterMaybe isEmptySubConfig  {-| @@ -59,7 +46,4 @@ resolveDefault   :: Spec.ConfigSpec cmd -- ^ ConfigSpec   -> Config              -- ^ returns Configuration Map with default values included-resolveDefault spec =-  maybe (Config emptySubConfig)-        Config-        (buildDefaultResolver spec)+resolveDefault spec = maybe (Config emptySubConfig) Config (buildDefaultResolver spec)
src/System/Etc/Internal/Resolver/Env.hs view
@@ -1,70 +1,51 @@ {-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}+ module System.Etc.Internal.Resolver.Env (resolveEnv, resolveEnvPure) where -import Protolude-import System.Environment (getEnvironment)+import           RIO+import qualified RIO.HashMap        as HashMap+import qualified RIO.Set            as Set+import qualified RIO.Text           as Text+import           System.Environment (getEnvironment) -import           Control.Arrow       ((***))-import qualified Data.Aeson          as JSON-import qualified Data.HashMap.Strict as HashMap-import qualified Data.Set            as Set-import qualified Data.Text           as Text+import           Control.Arrow ((***))+import qualified Data.Aeson    as JSON  import qualified System.Etc.Internal.Spec.Types as Spec import           System.Etc.Internal.Types  resolveEnvVarSource-  :: (Text -> Maybe Text)-  -> Spec.ConfigSources cmd-  -> Maybe ConfigSource-resolveEnvVarSource lookupEnv specSources =-  let-    toEnvSource varname envValue =-      envValue-      & JSON.String-      & Env varname-  in do-    varname <- Spec.envVar specSources-    toEnvSource varname <$> lookupEnv varname+  :: (Text -> Maybe Text) -> Bool -> Spec.ConfigSources cmd -> Maybe ConfigSource+resolveEnvVarSource lookupEnv sensitive specSources =+  let toEnvSource varname envValue =+        envValue & JSON.String & boolToValue sensitive & Env varname+  in  do+        varname <- Spec.envVar specSources+        toEnvSource varname <$> lookupEnv varname  buildEnvVarResolver :: (Text -> Maybe Text) -> Spec.ConfigSpec cmd -> Maybe ConfigValue buildEnvVarResolver lookupEnv spec =-  let-    resolverReducer-      :: Text-      -> Spec.ConfigValue cmd-      -> Maybe ConfigValue-      -> Maybe ConfigValue-    resolverReducer specKey specValue mConfig =-      case specValue of-        Spec.ConfigValue _ sources ->-          let-            updateConfig = do-              envSource <- resolveEnvVarSource lookupEnv sources-              writeInSubConfig specKey (ConfigValue $ Set.singleton envSource) <$> mConfig-          in-            updateConfig <|> mConfig+  let resolverReducer+        :: Text -> Spec.ConfigValue cmd -> Maybe ConfigValue -> Maybe ConfigValue+      resolverReducer specKey specValue mConfig = case specValue of+        Spec.ConfigValue _ sensitive sources ->+          let updateConfig = do+                envSource <- resolveEnvVarSource lookupEnv sensitive sources+                writeInSubConfig specKey (ConfigValue $ Set.singleton envSource)+                  <$> mConfig+          in  updateConfig <|> mConfig          Spec.SubConfig specConfigMap ->-          let-            mSubConfig =-              specConfigMap-              & HashMap.foldrWithKey-                   resolverReducer-                   (Just emptySubConfig)-              & filterMaybe isEmptySubConfig+          let mSubConfig =+                specConfigMap+                  & HashMap.foldrWithKey resolverReducer (Just emptySubConfig)+                  & filterMaybe isEmptySubConfig -            updateConfig =-              writeInSubConfig specKey <$> mSubConfig <*> mConfig-          in-            updateConfig <|> mConfig-  in-    Spec.specConfigValues spec-    & HashMap.foldrWithKey-          resolverReducer-          (Just emptySubConfig)-    & filterMaybe isEmptySubConfig+              updateConfig = writeInSubConfig specKey <$> mSubConfig <*> mConfig+          in  updateConfig <|> mConfig+  in  Spec.specConfigValues spec+      & HashMap.foldrWithKey resolverReducer (Just emptySubConfig)+      & filterMaybe isEmptySubConfig {-|  Gathers all OS Environment Variable values (@env@ entries) from the @etc/spec@@@ -77,16 +58,10 @@   -> [(Text, Text)]      -- ^ Environment Variable tuples   -> Config              -- ^ returns Configuration Map with Environment Variables values filled in resolveEnvPure spec envMap0 =-  let-    envMap =-      HashMap.fromList envMap0+  let envMap = HashMap.fromList envMap0 -    lookupEnv key =-      HashMap.lookup key envMap-  in-    maybe (Config emptySubConfig)-          Config-          (buildEnvVarResolver lookupEnv spec)+      lookupEnv key = HashMap.lookup key envMap+  in  maybe (Config emptySubConfig) Config (buildEnvVarResolver lookupEnv spec)   {-|@@ -99,10 +74,5 @@   :: Spec.ConfigSpec cmd -- ^ Config Spec   -> IO Config           -- ^ returns Configuration Map with Environment Variables values filled in resolveEnv spec =-  let-    getEnvironmentTxt =-      map (Text.pack *** Text.pack)-        <$> getEnvironment-  in-    resolveEnvPure spec-      <$> getEnvironmentTxt+  let getEnvironmentTxt = map (Text.pack *** Text.pack) <$> getEnvironment+  in  resolveEnvPure spec <$> getEnvironmentTxt
src/System/Etc/Internal/Resolver/File.hs view
@@ -3,24 +3,20 @@ {-# LANGUAGE OverloadedStrings #-} module System.Etc.Internal.Resolver.File (resolveFiles) where -import Protolude--import Control.Monad.Catch (MonadThrow (..))-import Data.Vector         (Vector)-import System.Directory    (doesFileExist)-+import           RIO+import           RIO.Directory (doesFileExist)+import qualified RIO.HashMap   as HashMap+import qualified RIO.Set       as Set+import qualified RIO.Text      as Text+import qualified RIO.Vector    as Vector  #ifdef WITH_YAML import qualified Data.Yaml as YAML #endif -import qualified Data.Aeson                 as JSON-import qualified Data.Aeson.Internal        as JSON (IResult (..), iparse)-import qualified Data.ByteString.Lazy.Char8 as LB8-import qualified Data.HashMap.Strict        as HashMap-import qualified Data.Set                   as Set-import qualified Data.Text                  as Text-import qualified Data.Vector                as Vector+import qualified Data.Aeson          as JSON+import qualified Data.Aeson.Internal as JSON (IResult (..), iparse)+import qualified RIO.ByteString.Lazy as LB8  import qualified System.Etc.Internal.Spec.Types as Spec import           System.Etc.Internal.Types      hiding (filepath)@@ -35,28 +31,36 @@ --------------------------------------------------------------------------------  parseConfigValue-  :: Monad m-  => Int-  -> Text-  -> JSON.Value-  -> m ConfigValue-parseConfigValue fileIndex filepath json =-  case json of-    JSON.Object object ->-      SubConfig-        <$> foldM-              (\acc (key, subconfigValue) -> do-                  value1 <- parseConfigValue fileIndex filepath subconfigValue-                  return $ HashMap.insert key value1 acc)-              HashMap.empty-              (HashMap.toList object)+  :: Monad m => Maybe (Spec.ConfigValue cmd) -> Int -> Text -> JSON.Value -> m ConfigValue+parseConfigValue mSpec fileIndex filepath json = case json of+  JSON.Object object -> SubConfig <$> foldM+    (\acc (key, subConfigValue) -> do+      let msubConfigSpec = do+            spec <- mSpec+            case spec of+              Spec.SubConfig hsh -> HashMap.lookup key hsh+              _ ->+                -- TODO: This should be an error given the config doesn't match spec+                fail "configuration spec and configuration value are different" -    _ ->-      return $-        ConfigValue (Set.singleton $ File fileIndex filepath json)+      value1 <- parseConfigValue msubConfigSpec fileIndex filepath subConfigValue+      return $ HashMap.insert key value1 acc+    )+    HashMap.empty+    (HashMap.toList object) +  _ ->+    let mToValue = do+          spec <- mSpec+          case spec of+            Spec.ConfigValue{} -> return $ boolToValue (Spec.isSensitive spec)+            _ -> fail "configuration spec and configuration value are different" -eitherDecode :: ConfigFile -> Either [Char] JSON.Value+        toValue = fromMaybe Plain mToValue+    in  return $ ConfigValue (Set.singleton $ File fileIndex filepath (toValue json))+++eitherDecode :: ConfigFile -> Either String JSON.Value #ifdef WITH_YAML eitherDecode contents0 =   case contents0 of@@ -65,66 +69,61 @@     YamlFile _ contents ->       YAML.decodeEither (LB8.toStrict contents) #else-eitherDecode contents0 =-  case contents0 of-    JsonFile _ contents ->-      JSON.eitherDecode contents-    YamlFile filepath _ ->-      Left ("Unsupported yaml file: " <> Text.unpack filepath)+eitherDecode contents0 = case contents0 of+  JsonFile _        contents -> JSON.eitherDecode contents+  YamlFile filepath _        -> Left ("Unsupported yaml file: " <> Text.unpack filepath) #endif  -parseConfig-  :: MonadThrow m-  => Int-  -> Text-  -> ConfigFile-  -> m Config-parseConfig fileIndex filepath contents =-  case eitherDecode contents of-    Left err ->-      throwM $ InvalidConfiguration (Text.pack err)+parseConfig :: MonadThrow m => Spec.ConfigValue cmd -> Int -> Text -> ConfigFile -> m Config+parseConfig spec fileIndex filepath contents = case eitherDecode contents of+  Left  err  -> throwM $ InvalidConfiguration Nothing (Text.pack err) -    Right json ->-      case JSON.iparse (parseConfigValue fileIndex filepath) json of-        JSON.IError _ err ->-          throwM $ InvalidConfiguration (Text.pack err)+  Right json -> case JSON.iparse (parseConfigValue (Just spec) fileIndex filepath) json of+    JSON.IError _ err    -> throwM $ InvalidConfiguration Nothing (Text.pack err) -        JSON.ISuccess result ->-          return (Config result)+    JSON.ISuccess result -> return (Config result)  readConfigFile :: MonadThrow m => Text -> IO (m ConfigFile) readConfigFile filepath =-  let-    filepathStr = Text.unpack filepath-  in do-    fileExists <- doesFileExist filepathStr-    if fileExists then do-      contents <- LB8.readFile filepathStr-      if ".json" `Text.isSuffixOf` filepath then-        return $ return (JsonFile filepath contents)-      else if (".yaml" `Text.isSuffixOf` filepath) ||-              (".yml" `Text.isSuffixOf` filepath) then-        return $ return (YamlFile filepath contents) else-        return (throwM $ InvalidConfiguration "Unsupported file extension")-    else-      return $ throwM $ ConfigurationFileNotFound filepath+  let filepathStr = Text.unpack filepath+  in+    do+      fileExists <- doesFileExist filepathStr+      if fileExists+        then do+          contents <- LB8.readFile filepathStr+          if ".json" `Text.isSuffixOf` filepath+          then+            return $ return (JsonFile filepath contents)+          else+            if (".yaml" `Text.isSuffixOf` filepath) || (".yml" `Text.isSuffixOf` filepath)+              then return $ return (YamlFile filepath contents)+              else return+                (throwM $ InvalidConfiguration Nothing "Unsupported file extension")+        else return $ throwM $ ConfigurationFileNotFound filepath -readConfigFromFiles :: [Text] -> IO (Config, [SomeException])-readConfigFromFiles files =-  files-  & zip [1..]-  & mapM (\(fileIndex, filepath) -> do-                 mContents <- readConfigFile filepath-                 return (mContents >>= parseConfig fileIndex filepath))-  & (foldl' (\(result, errs) eCurrent ->-               case eCurrent of-                 Left err ->-                   (result, err:errs)-                 Right current ->-                   (result `mappend` current, errs))-             (mempty, [])-             <$>)+readConfigFromFiles :: Spec.ConfigSpec cmd -> IO (Config, [SomeException])+readConfigFromFiles spec =+  Spec.specConfigFilepaths spec+    & zip [1 ..]+    & mapM+        (\(fileIndex, filepath) -> do+          mContents <- readConfigFile filepath+          return+            (   mContents+            >>= parseConfig (Spec.SubConfig $ Spec.specConfigValues spec)+                            fileIndex+                            filepath+            )+        )+    & (foldl'+        (\(result, errs) eCurrent -> case eCurrent of+          Left  err     -> (result, err : errs)+          Right current -> (result `mappend` current, errs)+        )+        (mempty, []) <$>+      )  {-| @@ -138,5 +137,5 @@   :: Spec.ConfigSpec cmd -- ^ Config Spec   -> IO (Config, Vector SomeException) -- ^ Configuration Map with all values from files filled in and a list of warnings resolveFiles spec = do-  (config, exceptions) <- readConfigFromFiles (Spec.specConfigFilepaths spec)+  (config, exceptions) <- readConfigFromFiles spec   return (config, Vector.fromList exceptions)
src/System/Etc/Internal/Spec/JSON.hs view
@@ -1,30 +1,21 @@ {-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-module System.Etc.Internal.Spec.JSON where -import Protolude+module System.Etc.Internal.Spec.JSON where -import Control.Monad.Catch (MonadThrow (..))+import qualified Data.Text.IO        as Text (readFile)+import           RIO+import qualified RIO.ByteString.Lazy as LBS+import qualified RIO.Text            as Text -import qualified Data.Aeson              as JSON-import qualified Data.Text               as Text-import qualified Data.Text.IO            as Text (readFile)-import qualified Data.Text.Lazy          as Text (fromStrict)-import qualified Data.Text.Lazy.Encoding as Text (encodeUtf8)+import qualified Data.Aeson as JSON  import System.Etc.Internal.Spec.Types -parseConfigSpec-  :: (MonadThrow m, JSON.FromJSON cmd)-    => Text-    -> m (ConfigSpec cmd)-parseConfigSpec input =-  case JSON.eitherDecode (Text.encodeUtf8 $ Text.fromStrict input) of-    Left err ->-      throwM $ InvalidConfiguration (Text.pack err)+parseConfigSpec :: (MonadThrow m, JSON.FromJSON cmd) => Text -> m (ConfigSpec cmd)+parseConfigSpec input = case JSON.eitherDecode (LBS.fromStrict $ encodeUtf8 input) of+  Left  err    -> throwM $ InvalidConfiguration Nothing (Text.pack err) -    Right result ->-      return result+  Right result -> return result  readConfigSpec :: JSON.FromJSON cmd => Text -> IO (ConfigSpec cmd) readConfigSpec filepath = do
src/System/Etc/Internal/Spec/Types.hs view
@@ -2,24 +2,22 @@ {-# LANGUAGE OverloadedStrings #-} module System.Etc.Internal.Spec.Types where -import Prelude   (fail)-import Protolude+import           Prelude     (fail)+import           RIO+import qualified RIO.HashMap as HashMap -import Data.Aeson          ((.:), (.:?))-import Data.HashMap.Strict (HashMap)-import Data.Vector         (Vector)+import Data.Aeson ((.:), (.:?)) -import qualified Data.Aeson          as JSON-import qualified Data.Aeson.Types    as JSON (Parser, typeMismatch)-import qualified Data.HashMap.Strict as HashMap+import qualified Data.Aeson       as JSON+import qualified Data.Aeson.Types as JSON (Parser, typeMismatch)  -------------------------------------------------------------------------------- -- Error Types  data ConfigurationError-  = InvalidConfiguration Text-  | InvalidConfigKeyPath [Text]-  | ConfigurationFileNotFound Text+  = InvalidConfiguration !(Maybe Text) !Text+  | InvalidConfigKeyPath ![Text]+  | ConfigurationFileNotFound !Text   deriving (Show)  instance Exception ConfigurationError@@ -39,67 +37,68 @@  data CliEntryMetadata   = Opt {-    optLong      :: Maybe Text-  , optShort     :: Maybe Text-  , optMetavar   :: Maybe Text-  , optHelp      :: Maybe Text-  , optRequired  :: Bool-  , optValueType :: CliOptValueType+    optLong      :: !(Maybe Text)+  , optShort     :: !(Maybe Text)+  , optMetavar   :: !(Maybe Text)+  , optHelp      :: !(Maybe Text)+  , optRequired  :: !Bool+  , optValueType :: !CliOptValueType   }   | Arg {-    argMetavar   :: Maybe Text-  , optRequired  :: Bool-  , argValueType :: CliArgValueType+    argMetavar   :: !(Maybe Text)+  , optRequired  :: !Bool+  , argValueType :: !CliArgValueType   }   deriving (Show, Eq)  data CliEntrySpec cmd   = CmdEntry {-    cliEntryCmdValue :: Vector cmd-  , cliEntryMetadata :: CliEntryMetadata+    cliEntryCmdValue :: !(Vector cmd)+  , cliEntryMetadata :: !CliEntryMetadata   }   | PlainEntry {-    cliEntryMetadata :: CliEntryMetadata+    cliEntryMetadata :: !CliEntryMetadata   }   deriving (Show, Eq)  data CliCmdSpec   = CliCmdSpec {-    cliCmdDesc   :: Text-  , cliCmdHeader :: Text+    cliCmdDesc   :: !Text+  , cliCmdHeader :: !Text   }   deriving (Show, Eq)  data ConfigSources cmd   = ConfigSources {-    envVar   :: Maybe Text-  , cliEntry :: Maybe (CliEntrySpec cmd)+    envVar   :: !(Maybe Text)+  , cliEntry :: !(Maybe (CliEntrySpec cmd))   }   deriving (Show, Eq)  data ConfigValue cmd   = ConfigValue {-    defaultValue  :: Maybe JSON.Value-  , configSources :: ConfigSources cmd+    defaultValue  :: !(Maybe JSON.Value)+  , isSensitive   :: !Bool+  , configSources :: !(ConfigSources cmd)   }   | SubConfig {-    subConfig :: HashMap Text (ConfigValue cmd)+    subConfig :: !(HashMap Text (ConfigValue cmd))   }   deriving (Show, Eq)  data CliProgramSpec   = CliProgramSpec {-    cliProgramDesc   :: Text-  , cliProgramHeader :: Text-  , cliCommands      :: Maybe (HashMap Text CliCmdSpec)+    cliProgramDesc   :: !Text+  , cliProgramHeader :: !Text+  , cliCommands      :: !(Maybe (HashMap Text CliCmdSpec))   }   deriving (Show, Eq)  data ConfigSpec cmd   = ConfigSpec {-    specConfigFilepaths :: [Text]-  , specCliProgramSpec  :: Maybe CliProgramSpec-  , specConfigValues    :: HashMap Text (ConfigValue cmd)+    specConfigFilepaths :: ![Text]+  , specCliProgramSpec  :: !(Maybe CliProgramSpec)+  , specConfigValues    :: !(HashMap Text (ConfigValue cmd))   }   deriving (Show, Eq) @@ -127,63 +126,45 @@       _ ->         JSON.typeMismatch "CliProgramSpec" json -cliArgTypeParser-  :: JSON.Object-    -> JSON.Parser CliArgValueType+cliArgTypeParser :: JSON.Object -> JSON.Parser CliArgValueType cliArgTypeParser object = do   value <- object .: "type"   case value of     JSON.String typeName-      | typeName == "string" ->-        return StringArg-      | typeName == "number" ->-        return NumberArg-      | otherwise ->-        JSON.typeMismatch "CliArgValueType (string, number)" value-    _ ->-      JSON.typeMismatch "CliArgValueType (string, number)" value+      | typeName == "string" -> return StringArg+      | typeName == "number" -> return NumberArg+      | otherwise            -> JSON.typeMismatch "CliArgValueType (string, number)" value+    _ -> JSON.typeMismatch "CliArgValueType (string, number)" value -cliArgParser-  :: JSON.Object-    -> JSON.Parser CliEntryMetadata+cliArgParser :: JSON.Object -> JSON.Parser CliEntryMetadata cliArgParser object =   Arg     <$> (object .:? "metavar")     <*> (fromMaybe True <$> (object .:? "required"))     <*> cliArgTypeParser object -cliOptTypeParser-  :: JSON.Object-    -> JSON.Parser CliOptValueType+cliOptTypeParser :: JSON.Object -> JSON.Parser CliOptValueType cliOptTypeParser object = do   mvalue <- object .:? "type"   case mvalue of     Just value@(JSON.String typeName)-      | typeName == "string" ->-        return StringOpt-      | typeName == "number" ->-        return NumberOpt-      | typeName == "switch" ->-        return SwitchOpt-      | otherwise ->-        JSON.typeMismatch "CliOptValueType (string, number, switch)" value+      | typeName == "string" -> return StringOpt+      | typeName == "number" -> return NumberOpt+      | typeName == "switch" -> return SwitchOpt+      | otherwise -> JSON.typeMismatch "CliOptValueType (string, number, switch)" value -    Just value ->-      JSON.typeMismatch "CliOptValueType" value+    Just value -> JSON.typeMismatch "CliOptValueType" value -    Nothing ->-      fail "CLI Option type is required"+    Nothing    -> fail "CLI Option type is required" -cliOptParser-  :: JSON.Object-    -> JSON.Parser CliEntryMetadata+cliOptParser :: JSON.Object -> JSON.Parser CliEntryMetadata cliOptParser object = do   long  <- object .:? "long"   short <- object .:? "short"-  if isNothing long && isNothing short then-    fail "'option' field input requires either 'long' or 'short' settings"-  else-    Opt+  if isNothing long && isNothing short+    then fail "'option' field input requires either 'long' or 'short' settings"+    else+      Opt       <$> pure long       <*> pure short       <*> (object .:? "metavar")@@ -252,12 +233,15 @@               return (SubConfig result)            -- etc spec value object-          Just (JSON.Object spec) ->-            if HashMap.size object == 1 then+          Just (JSON.Object fieldSpec) ->+            if HashMap.size object == 1 then do+              mSensitive <- fieldSpec .:? "sensitive"+              let sensitive = fromMaybe False mSensitive               ConfigValue-                <$> spec .:? "default"-                <*> (ConfigSources <$> (spec .:? "env")-                                   <*> (spec .:? "cli"))+                <$> fieldSpec .:? "default"+                <*> pure sensitive+                <*> (ConfigSources <$> fieldSpec .:? "env"+                                   <*> fieldSpec .:? "cli")             else               fail "etc/spec object can only contain one key" @@ -266,8 +250,13 @@             fail "etc/spec value must be a JSON object"        _ ->-        return $-          ConfigValue (Just json) (ConfigSources Nothing Nothing)+        return+          ConfigValue+          {+            defaultValue = Just json+          , isSensitive = False+          , configSources = ConfigSources Nothing Nothing+          }  instance JSON.FromJSON cmd => JSON.FromJSON (ConfigSpec cmd) where   parseJSON json  =
src/System/Etc/Internal/Spec/YAML.hs view
@@ -1,30 +1,23 @@+ {-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-module System.Etc.Internal.Spec.YAML where -import Protolude+module System.Etc.Internal.Spec.YAML where -import Control.Monad.Catch (MonadThrow (..))+import           RIO+import qualified RIO.Text as Text  import qualified Data.Aeson         as JSON-import qualified Data.Text          as Text import qualified Data.Text.Encoding as Text (encodeUtf8) import qualified Data.Text.IO       as Text (readFile) import qualified Data.Yaml          as YAML  import System.Etc.Internal.Spec.Types -parseConfigSpec-  :: (MonadThrow m, JSON.FromJSON cmd)-    => Text-    -> m (ConfigSpec cmd)-parseConfigSpec input =-  case YAML.decodeEither (Text.encodeUtf8 input) of-    Left err ->-      throwM $ InvalidConfiguration (Text.pack err)+parseConfigSpec :: (MonadThrow m, JSON.FromJSON cmd) => Text -> m (ConfigSpec cmd)+parseConfigSpec input = case YAML.decodeEither (Text.encodeUtf8 input) of+  Left  err    -> throwM $ InvalidConfiguration Nothing (Text.pack err) -    Right result ->-      return result+  Right result -> return result  readConfigSpec :: JSON.FromJSON cmd => Text -> IO (ConfigSpec cmd) readConfigSpec filepath = do
src/System/Etc/Internal/Types.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE DeriveGeneric              #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE NoImplicitPrelude          #-} {-# LANGUAGE RankNTypes                 #-}@@ -7,35 +8,66 @@   , module System.Etc.Internal.Spec.Types   ) where -import Control.Monad.Catch (MonadThrow)-import Data.HashMap.Strict (HashMap)-import Protolude+import           RIO+import qualified RIO.HashMap as HashMap+import qualified RIO.Set     as Set -import qualified Data.Aeson          as JSON-import qualified Data.Aeson.Types    as JSON (Parser)-import qualified Data.HashMap.Strict as HashMap-import qualified Data.Set            as Set+import           Data.Bool      (bool)+import qualified Data.Semigroup as Semigroup +import qualified Data.Aeson       as JSON+import qualified Data.Aeson.Types as JSON (Parser)+ import System.Etc.Internal.Spec.Types (ConfigurationError (..))  -------------------- -- Configuration Types +data Value a+  = Plain { fromValue :: !a }+  | Sensitive { fromValue :: !a }+  deriving (Generic, Eq, Ord)++instance Show a => Show (Value a) where+  show (Plain a)     = show a+  show (Sensitive _) = "<<sensitive>>"++instance Functor Value where+  fmap f val =+    case val of+      Plain a     -> Plain (f a)+      Sensitive a -> Sensitive (f a)++instance Applicative Value where+  pure a = Plain a+  (<*>) vf va =+    case (vf, va) of+      (Plain f, Plain a)         -> Plain (f a)+      (Sensitive f, Sensitive a) -> Sensitive (f a)+      (Sensitive f, Plain a)     -> Sensitive (f a)+      (Plain f, Sensitive a)     -> Sensitive (f a)++instance IsString a => IsString (Value a) where+  fromString = Plain . fromString++boolToValue :: Bool -> (a -> Value a)+boolToValue = bool Plain Sensitive+ data ConfigSource   = File {-      configIndex :: Int-    , filepath    :: Text-    , value       :: JSON.Value+      configIndex :: !Int+    , filepath    :: !Text+    , value       :: !(Value JSON.Value)     }   | Env {-      envVar :: Text-    , value  :: JSON.Value+      envVar :: !Text+    , value  :: !(Value JSON.Value)     }   | Cli {-      value :: JSON.Value+      value :: !(Value JSON.Value)     }   | Default {-      value :: JSON.Value+      value :: !(Value JSON.Value)     }   | None   deriving (Show, Eq)@@ -52,6 +84,10 @@         (_, None) ->           GT +        (_, _)+          | fromValue (value a) == JSON.Null -> LT+          | fromValue (value b) == JSON.Null -> GT+         (Default {}, _) ->           LT @@ -75,71 +111,55 @@  data ConfigValue   = ConfigValue {-      configSource :: Set ConfigSource+      configSource :: !(Set ConfigSource)     }   | SubConfig {-      configMap :: HashMap Text ConfigValue+      configMap :: !(HashMap Text ConfigValue)     }   deriving (Eq, Show)  deepMerge :: ConfigValue -> ConfigValue -> ConfigValue-deepMerge left right =-  case (left, right) of-    (SubConfig leftm, SubConfig rightm) ->-      SubConfig $-        HashMap.foldrWithKey-            (\key rightv result ->-                case HashMap.lookup key result of-                  Just leftv ->-                    HashMap.insert key (deepMerge leftv rightv) result-                  _ ->-                    HashMap.insert key rightv result)-            leftm-            rightm-    (ConfigValue leftSources, ConfigValue rightSources) ->-      ConfigValue $ Set.union leftSources rightSources-    _ ->-      right+deepMerge left right = case (left, right) of+  (SubConfig leftm, SubConfig rightm) -> SubConfig $ HashMap.foldrWithKey+    (\key rightv result -> case HashMap.lookup key result of+      Just leftv -> HashMap.insert key (deepMerge leftv rightv) result+      _          -> HashMap.insert key rightv result+    )+    leftm+    rightm+  (ConfigValue leftSources, ConfigValue rightSources) ->+    ConfigValue $ Set.union leftSources rightSources+  _ -> right +instance Semigroup.Semigroup ConfigValue where+  (<>) = deepMerge+ instance Monoid ConfigValue where   mempty  = emptySubConfig-  mappend = deepMerge+  mappend = (Semigroup.<>)  newtype Config   = Config { fromConfig :: ConfigValue }-  deriving (Eq, Show, Monoid)+  deriving (Eq, Show, Semigroup, Monoid)  isEmptySubConfig :: ConfigValue -> Bool-isEmptySubConfig val =-  case val of-    SubConfig hsh ->-      HashMap.null hsh-    ConfigValue {} ->-      False+isEmptySubConfig val = case val of+  SubConfig hsh -> HashMap.null hsh+  ConfigValue{} -> False  emptySubConfig :: ConfigValue-emptySubConfig =-  SubConfig HashMap.empty+emptySubConfig = SubConfig HashMap.empty  writeInSubConfig :: Text -> ConfigValue -> ConfigValue -> ConfigValue-writeInSubConfig key val subConfig =-  case subConfig of-    SubConfig hsh ->-      SubConfig-        $ HashMap.insert key val hsh-    _ ->-      subConfig+writeInSubConfig key val subConfig = case subConfig of+  SubConfig hsh -> SubConfig $ HashMap.insert key val hsh+  _             -> subConfig  filterMaybe :: (a -> Bool) -> Maybe a -> Maybe a-filterMaybe pfn mvalue =-  case mvalue of-    Just a-      | pfn a ->-          Nothing-      | otherwise ->-          mvalue-    Nothing ->-      Nothing+filterMaybe pfn mvalue = case mvalue of+  Just a | pfn a     -> Nothing+         | otherwise -> mvalue+  Nothing -> Nothing   class IConfig config where
src/System/Etc/Spec.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE CPP                 #-} {-# LANGUAGE NoImplicitPrelude   #-}-{-# LANGUAGE OverloadedStrings   #-}+ {-# LANGUAGE ScopedTypeVariables #-} module System.Etc.Spec (     module Types@@ -8,22 +8,21 @@   , readConfigSpec   ) where -import Protolude hiding (catch)+import qualified Data.Text.IO as Text (readFile)+import           RIO+import qualified RIO.Text     as Text  import System.Etc.Internal.Spec.Types as Types     (ConfigSpec, ConfigValue, ConfigurationError (..)) -import Control.Monad.Catch (MonadCatch (..))- #ifdef WITH_CLI import qualified Data.Aeson as JSON #endif-import qualified Data.Text    as Text-import qualified Data.Text.IO as Text (readFile) -import qualified System.Etc.Internal.Spec.JSON as JSON #ifdef WITH_YAML import qualified System.Etc.Internal.Spec.YAML as YAML+#else+import qualified System.Etc.Internal.Spec.JSON as JSONSpec #endif  {-|@@ -34,23 +33,20 @@ -} #ifdef WITH_CLI parseConfigSpec-  :: (MonadCatch m, JSON.FromJSON cmd)+  :: (MonadThrow m, JSON.FromJSON cmd)     => Text               -- ^ Text to be parsed     -> m (ConfigSpec cmd) -- ^ returns ConfigSpec #else parseConfigSpec-  :: (MonadCatch m)-    => Text               -- ^ Text to be parsed-    -> m (ConfigSpec ()) -- ^ returns ConfigSpec+  :: (MonadThrow m)+  => Text               -- ^ Text to be parsed+  -> m (ConfigSpec ()) -- ^ returns ConfigSpec #endif  #ifdef WITH_YAML-parseConfigSpec input =-   catch (JSON.parseConfigSpec input)-         (\(_ :: SomeException) ->  YAML.parseConfigSpec input)+parseConfigSpec = YAML.parseConfigSpec #else-parseConfigSpec =-  JSON.parseConfigSpec+parseConfigSpec = JSONSpec.parseConfigSpec #endif  {-|
+ test/System/Etc/ConfigTest.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE NoImplicitPrelude   #-}+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE ScopedTypeVariables #-}+module System.Etc.ConfigTest where++import RIO++import Test.Tasty       (TestTree, testGroup)+import Test.Tasty.HUnit (assertEqual, assertFailure, testCase)++import qualified System.Etc as SUT++tests :: TestTree+tests = testGroup+  "System.Etc.Config"+  [ testCase "InvalidConfiguration error contains key when types don't match" $ do+      let input = "{\"etc/entries\":{\"greeting\":123}}"++      (spec :: SUT.ConfigSpec ()) <- SUT.parseConfigSpec input+      let config = SUT.resolveDefault spec+      case SUT.getConfigValue ["greeting"] config of+        Left err -> case fromException err of+          Just (SUT.InvalidConfiguration key _) ->+            assertEqual "expecting key to be greeting, but wasn't" (Just "greeting") key+          _ ->+            assertFailure+              $  "expecting InvalidConfiguration; got something else: "+              <> show err+        Right (_ :: Text) -> assertFailure "expecting error; got none"+  ]
test/System/Etc/Extra/EnvMisspellTest.hs view
@@ -4,36 +4,28 @@ {-# LANGUAGE ScopedTypeVariables #-} module System.Etc.Extra.EnvMisspellTest where -import Protolude+import           RIO+import qualified RIO.Vector as Vector  import Test.Tasty       (TestTree, testGroup) import Test.Tasty.HUnit (assertBool, assertEqual, testCase) -import qualified Data.Vector as Vector- import System.Etc  tests :: TestTree-tests =-  testGroup "env misspells"-  [-    testCase "it warns when misspell is present" $ do-      let-        input =-          mconcat-            [-              "{\"etc/entries\": {"+tests = testGroup+  "env misspells"+  [ testCase "it warns when misspell is present" $ do+      let input = mconcat+            [ "{\"etc/entries\": {"             , " \"greeting\": { \"etc/spec\": { \"env\": \"GREETING\" }}}}"             ]        (spec :: ConfigSpec ()) <- parseConfigSpec input -      let-        result =-          getEnvMisspellingsPure spec ["GREEING"]+      let result = getEnvMisspellingsPure spec ["GREEING"] -      assertBool "expecting to get a warning for typo"-                 (not $ Vector.null result)+      assertBool "expecting to get a warning for typo" (not $ Vector.null result)        assertEqual "expecting to get typo for key GREETING"                   (EnvMisspell "GREEING" "GREETING")
test/System/Etc/Resolver/Cli/CommandTest.hs view
@@ -3,359 +3,254 @@ {-# LANGUAGE ScopedTypeVariables #-} module System.Etc.Resolver.Cli.CommandTest where -import Protolude+import           RIO+import qualified RIO.Set as Set  import Test.Tasty       (TestTree, testGroup) import Test.Tasty.HUnit (assertBool, assertEqual, assertFailure, testCase) -import qualified Data.Set as Set  import System.Etc  with_command_option_tests :: TestTree-with_command_option_tests =-  testGroup "option input"-  [-    testCase "entry accepts short" $ do-      let-        input =-          mconcat-            [-              "{ \"etc/cli\": {"-            , "    \"desc\": \"\""-            , "  , \"header\": \"\""-            , "  , \"commands\": {"-            , "      \"test\": {\"header\": \"\", \"desc\": \"\"}}}"-            , ", \"etc/entries\": {"-            , "    \"greeting\": {"-            , "      \"etc/spec\": {"-            , "        \"cli\": {"-            , "          \"input\": \"option\""-            , "        , \"short\": \"g\""-            , "        , \"long\": \"greeting\""-            , "        , \"type\": \"string\""-            , "        , \"commands\": [\"test\"]"-            , "}}}}}"-            ]-      (spec :: ConfigSpec Text) <- parseConfigSpec input-      (cmd, config) <- resolveCommandCliPure spec "program" ["test", "-g", "hello cli"]--      assertEqual "invalid command output" "test" cmd+with_command_option_tests = testGroup+  "option input"+  [ testCase "entry accepts short" $ do+    let input = mconcat+          [ "{ \"etc/cli\": {"+          , "    \"desc\": \"\""+          , "  , \"header\": \"\""+          , "  , \"commands\": {"+          , "      \"test\": {\"header\": \"\", \"desc\": \"\"}}}"+          , ", \"etc/entries\": {"+          , "    \"greeting\": {"+          , "      \"etc/spec\": {"+          , "        \"cli\": {"+          , "          \"input\": \"option\""+          , "        , \"short\": \"g\""+          , "        , \"long\": \"greeting\""+          , "        , \"type\": \"string\""+          , "        , \"commands\": [\"test\"]"+          , "}}}}}"+          ]+    (spec :: ConfigSpec Text) <- parseConfigSpec input+    (cmd, config) <- resolveCommandCliPure spec "program" ["test", "-g", "hello cli"] -      case getAllConfigSources ["greeting"] config of-        Nothing ->-          assertFailure ("expecting to get entries for greeting\n"-                         <> show config)-        Just set ->-          assertBool ("expecting to see entry from env; got " <> show set)-                   (Set.member (Cli "hello cli") set)+    assertEqual "invalid command output" "test" cmd +    case getAllConfigSources ["greeting"] config of+      Nothing   -> assertFailure ("expecting to get entries for greeting\n" <> show config)+      Just aSet -> assertBool ("expecting to see entry from env; got " <> show aSet)+                              (Set.member (Cli "hello cli") aSet)   , testCase "entry accepts long" $ do-      let-        input =-          mconcat-            [-              "{ \"etc/cli\": {"-            , "    \"desc\": \"\""-            , "  , \"header\": \"\""-            , "  , \"commands\": {"-            , "      \"test\": {\"header\": \"\", \"desc\": \"\"}}}"-            , ", \"etc/entries\": {"-            , "    \"greeting\": {"-            , "      \"etc/spec\": {"-            , "        \"cli\": {"-            , "          \"input\": \"option\""-            , "        , \"short\": \"g\""-            , "        , \"long\": \"greeting\""-            , "        , \"type\": \"string\""-            , "        , \"commands\": [\"test\"]"-            , "}}}}}"-            ]-      (spec :: ConfigSpec Text) <- parseConfigSpec input--      (cmd, config) <- resolveCommandCliPure spec "program" ["test", "--greeting", "hello cli"]+    let input = mconcat+          [ "{ \"etc/cli\": {"+          , "    \"desc\": \"\""+          , "  , \"header\": \"\""+          , "  , \"commands\": {"+          , "      \"test\": {\"header\": \"\", \"desc\": \"\"}}}"+          , ", \"etc/entries\": {"+          , "    \"greeting\": {"+          , "      \"etc/spec\": {"+          , "        \"cli\": {"+          , "          \"input\": \"option\""+          , "        , \"short\": \"g\""+          , "        , \"long\": \"greeting\""+          , "        , \"type\": \"string\""+          , "        , \"commands\": [\"test\"]"+          , "}}}}}"+          ]+    (spec :: ConfigSpec Text) <- parseConfigSpec input -      assertEqual "invalid command output" "test" cmd+    (cmd, config)             <- resolveCommandCliPure spec+                                                       "program"+                                                       ["test", "--greeting", "hello cli"] -      case getAllConfigSources ["greeting"] config of-        Nothing ->-          assertFailure ("expecting to get entries for greeting\n"-                         <> show config)-        Just set ->-          assertBool ("expecting to see entry from env; got " <> show set)-                   (Set.member (Cli "hello cli") set)+    assertEqual "invalid command output" "test" cmd +    case getAllConfigSources ["greeting"] config of+      Nothing   -> assertFailure ("expecting to get entries for greeting\n" <> show config)+      Just aSet -> assertBool ("expecting to see entry from env; got " <> show aSet)+                              (Set.member (Cli "hello cli") aSet)   , testCase "entry gets validated with a type" $ do-      let-        input =-          mconcat-            [-              "{ \"etc/cli\": {"-            , "    \"desc\": \"\""-            , "  , \"header\": \"\""-            , "  , \"commands\": {"-            , "      \"test\": {\"header\": \"\", \"desc\": \"\"}}}"-            , ", \"etc/entries\": {"-            , "    \"greeting\": {"-            , "      \"etc/spec\": {"-            , "        \"cli\": {"-            , "          \"input\": \"option\""-            , "        , \"short\": \"g\""-            , "        , \"long\": \"greeting\""-            , "        , \"type\": \"number\""-            , "        , \"commands\": [\"test\"]"-            , "}}}}}"-            ]-      (spec :: ConfigSpec Text) <- parseConfigSpec input+    let input = mconcat+          [ "{ \"etc/cli\": {"+          , "    \"desc\": \"\""+          , "  , \"header\": \"\""+          , "  , \"commands\": {"+          , "      \"test\": {\"header\": \"\", \"desc\": \"\"}}}"+          , ", \"etc/entries\": {"+          , "    \"greeting\": {"+          , "      \"etc/spec\": {"+          , "        \"cli\": {"+          , "          \"input\": \"option\""+          , "        , \"short\": \"g\""+          , "        , \"long\": \"greeting\""+          , "        , \"type\": \"number\""+          , "        , \"commands\": [\"test\"]"+          , "}}}}}"+          ]+    (spec :: ConfigSpec Text) <- parseConfigSpec input -      case resolveCommandCliPure spec "program" ["test", "--greeting", "hello cli"] of-        Left err ->-          case fromException err of-            Just CliEvalExited {} ->-              return ()+    case resolveCommandCliPure spec "program" ["test", "--greeting", "hello cli"] of+      Left err -> case fromException err of+        Just CliEvalExited{} -> return () -            _ ->-              assertFailure ("Expecting type validation to work on cli; got "-                             <> show err)+        _ -> assertFailure ("Expecting type validation to work on cli; got " <> show err)  -        Right _ ->-          assertFailure "Expecting type validation to work on cli"-+      Right _ -> assertFailure "Expecting type validation to work on cli"   , testCase "entry with required false does not barf" $ do-      let-        input =-          mconcat-            [-              "{ \"etc/cli\": {"-            , "    \"desc\": \"\""-            , "  , \"header\": \"\""-            , "  , \"commands\": {"-            , "      \"test\": {\"header\": \"\", \"desc\": \"\"}}}"-            , ", \"etc/entries\": {"-            , "    \"greeting\": {"-            , "      \"etc/spec\": {"-            , "        \"cli\": {"-            , "          \"input\": \"option\""-            , "        , \"short\": \"g\""-            , "        , \"long\": \"greeting\""-            , "        , \"type\": \"string\""-            , "        , \"required\": false"-            , "        , \"commands\": [\"test\"]"-            , "}}}}}"-            ]-      (spec :: ConfigSpec Text) <- parseConfigSpec input-      (cmd, config) <- resolveCommandCliPure spec "program" ["test"]--      assertEqual "invalid command output" "test" cmd+    let input = mconcat+          [ "{ \"etc/cli\": {"+          , "    \"desc\": \"\""+          , "  , \"header\": \"\""+          , "  , \"commands\": {"+          , "      \"test\": {\"header\": \"\", \"desc\": \"\"}}}"+          , ", \"etc/entries\": {"+          , "    \"greeting\": {"+          , "      \"etc/spec\": {"+          , "        \"cli\": {"+          , "          \"input\": \"option\""+          , "        , \"short\": \"g\""+          , "        , \"long\": \"greeting\""+          , "        , \"type\": \"string\""+          , "        , \"required\": false"+          , "        , \"commands\": [\"test\"]"+          , "}}}}}"+          ]+    (spec :: ConfigSpec Text) <- parseConfigSpec input+    (cmd, config)             <- resolveCommandCliPure spec "program" ["test"] -      case getConfigValue ["greeting"] config of-        Just set ->-          assertFailure ("expecting to have no entry for greeting; got\n"-                       <> show set)+    assertEqual "invalid command output" "test" cmd -        (_ :: Maybe ()) ->-          return ()+    case getConfigValue ["greeting"] config of+      Just aSet ->+        assertFailure ("expecting to have no entry for greeting; got\n" <> show aSet) +      (_ :: Maybe ()) -> return ()   , testCase "entry with required fails when option not given" $ do-      let-        input =-          mconcat-            [-              "{ \"etc/cli\": {"-            , "    \"desc\": \"\""-            , "  , \"header\": \"\""-            , "  , \"commands\": {"-            , "      \"test\": {\"header\": \"\", \"desc\": \"\"}}}"-            , ", \"etc/entries\": {"-            , "    \"greeting\": {"-            , "      \"etc/spec\": {"-            , "        \"cli\": {"-            , "          \"input\": \"option\""-            , "        , \"short\": \"g\""-            , "        , \"long\": \"greeting\""-            , "        , \"type\": \"string\""-            , "        , \"required\": true"-            , "        , \"commands\": [\"test\"]"-            , "}}}}}"-            ]-      (spec :: ConfigSpec Text) <- parseConfigSpec input-      case resolveCommandCliPure spec "program" ["test"] of-        Left err ->-          case fromException err of-            Just CliEvalExited {} ->-              return ()+    let input = mconcat+          [ "{ \"etc/cli\": {"+          , "    \"desc\": \"\""+          , "  , \"header\": \"\""+          , "  , \"commands\": {"+          , "      \"test\": {\"header\": \"\", \"desc\": \"\"}}}"+          , ", \"etc/entries\": {"+          , "    \"greeting\": {"+          , "      \"etc/spec\": {"+          , "        \"cli\": {"+          , "          \"input\": \"option\""+          , "        , \"short\": \"g\""+          , "        , \"long\": \"greeting\""+          , "        , \"type\": \"string\""+          , "        , \"required\": true"+          , "        , \"commands\": [\"test\"]"+          , "}}}}}"+          ]+    (spec :: ConfigSpec Text) <- parseConfigSpec input+    case resolveCommandCliPure spec "program" ["test"] of+      Left err -> case fromException err of+        Just CliEvalExited{} -> return () -            _ ->-              assertFailure ("Expecting required validation to work on cli; got "-                             <> show err)+        _ ->+          assertFailure ("Expecting required validation to work on cli; got " <> show err) -        Right _ ->-          assertFailure "Expecting required option to fail cli resolving"+      Right _ -> assertFailure "Expecting required option to fail cli resolving"   ]  with_command_argument_tests :: TestTree-with_command_argument_tests =-  testGroup "argument input"-  [-    testCase "entry gets validated with a type" $ do-      let-        input =-          mconcat-            [-              "{ \"etc/cli\": {"-            , "    \"desc\": \"\""-            , "  , \"header\": \"\""-            , "  , \"commands\": {"-            , "      \"test\": {\"header\": \"\", \"desc\": \"\"}}}"-            , ", \"etc/entries\": {"-            , "    \"greeting\": {"-            , "      \"etc/spec\": {"-            , "        \"cli\": {"-            , "          \"input\": \"argument\""-            , "        , \"type\": \"number\""-            , "        , \"metavar\": \"GREETING\""-            , "        , \"commands\": [\"test\"]"-            , "}}}}}"-            ]-      (spec :: ConfigSpec Text) <- parseConfigSpec input--      case resolveCommandCliPure spec "program" ["test", "hello cli"] of-        Left err ->-          case fromException err of-            Just CliEvalExited {} ->-              return ()+with_command_argument_tests = testGroup+  "argument input"+  [ testCase "entry gets validated with a type" $ do+    let input = mconcat+          [ "{ \"etc/cli\": {"+          , "    \"desc\": \"\""+          , "  , \"header\": \"\""+          , "  , \"commands\": {"+          , "      \"test\": {\"header\": \"\", \"desc\": \"\"}}}"+          , ", \"etc/entries\": {"+          , "    \"greeting\": {"+          , "      \"etc/spec\": {"+          , "        \"cli\": {"+          , "          \"input\": \"argument\""+          , "        , \"type\": \"number\""+          , "        , \"metavar\": \"GREETING\""+          , "        , \"commands\": [\"test\"]"+          , "}}}}}"+          ]+    (spec :: ConfigSpec Text) <- parseConfigSpec input -            _ ->-              assertFailure ("Expecting type validation to work on cli; got "-                             <> show err)+    case resolveCommandCliPure spec "program" ["test", "hello cli"] of+      Left err -> case fromException err of+        Just CliEvalExited{} -> return () -        Right _ ->-          assertFailure "Expecting type validation to work on cli"+        _ -> assertFailure ("Expecting type validation to work on cli; got " <> show err) +      Right _ -> assertFailure "Expecting type validation to work on cli"   , testCase "entry with required false does not barf" $ do-      let-        input =-          mconcat-            [-              "{ \"etc/cli\": {"-            , "    \"desc\": \"\""-            , "  , \"header\": \"\""-            , "  , \"commands\": {"-            , "      \"test\": {\"header\": \"\", \"desc\": \"\"}}}"-            , ", \"etc/entries\": {"-            , "    \"greeting\": {"-            , "      \"etc/spec\": {"-            , "        \"cli\": {"-            , "          \"input\": \"argument\""-            , "        , \"type\": \"string\""-            , "        , \"metavar\": \"GREETING\""-            , "        , \"required\": false"-            , "        , \"commands\": [\"test\"]"-            , "}}}}}"-            ]-      (spec :: ConfigSpec Text) <- parseConfigSpec input-      (cmd, config) <- resolveCommandCliPure spec "program" ["test"]--      assertEqual "invalid command output" "test" cmd+    let input = mconcat+          [ "{ \"etc/cli\": {"+          , "    \"desc\": \"\""+          , "  , \"header\": \"\""+          , "  , \"commands\": {"+          , "      \"test\": {\"header\": \"\", \"desc\": \"\"}}}"+          , ", \"etc/entries\": {"+          , "    \"greeting\": {"+          , "      \"etc/spec\": {"+          , "        \"cli\": {"+          , "          \"input\": \"argument\""+          , "        , \"type\": \"string\""+          , "        , \"metavar\": \"GREETING\""+          , "        , \"required\": false"+          , "        , \"commands\": [\"test\"]"+          , "}}}}}"+          ]+    (spec :: ConfigSpec Text) <- parseConfigSpec input+    (cmd, config)             <- resolveCommandCliPure spec "program" ["test"] -      case getConfigValue ["greeting"] config of-        (Nothing :: Maybe ()) ->-          return ()+    assertEqual "invalid command output" "test" cmd -        Just set ->-          assertFailure ("expecting to have no entry for greeting; got\n"-                       <> show set)+    case getConfigValue ["greeting"] config of+      (Nothing :: Maybe ()) -> return () +      Just aSet ->+        assertFailure ("expecting to have no entry for greeting; got\n" <> show aSet)   , testCase "entry with required fails when argument not given" $ do-      let-        input =-          mconcat-            [-              "{ \"etc/cli\": {"-            , "    \"desc\": \"\""-            , "  , \"header\": \"\""-            , "  , \"commands\": {"-            , "      \"test\": {\"header\": \"\", \"desc\": \"\"}}}"-            , ", \"etc/entries\": {"-            , "    \"greeting\": {"-            , "      \"etc/spec\": {"-            , "        \"cli\": {"-            , "          \"input\": \"argument\""-            , "        , \"type\": \"string\""-            , "        , \"metavar\": \"GREETING\""-            , "        , \"required\": true"-            , "        , \"commands\": [\"test\"]"-            , "}}}}}"-            ]-      (spec :: ConfigSpec Text) <- parseConfigSpec input-      case resolveCommandCliPure spec "program" ["test"] of-        Left err ->-          case fromException err of-            Just CliEvalExited {} ->-              return ()--            _ ->-              assertFailure ("Expecting required validation to work on cli; got "-                             <> show err)+    let input = mconcat+          [ "{ \"etc/cli\": {"+          , "    \"desc\": \"\""+          , "  , \"header\": \"\""+          , "  , \"commands\": {"+          , "      \"test\": {\"header\": \"\", \"desc\": \"\"}}}"+          , ", \"etc/entries\": {"+          , "    \"greeting\": {"+          , "      \"etc/spec\": {"+          , "        \"cli\": {"+          , "          \"input\": \"argument\""+          , "        , \"type\": \"string\""+          , "        , \"metavar\": \"GREETING\""+          , "        , \"required\": true"+          , "        , \"commands\": [\"test\"]"+          , "}}}}}"+          ]+    (spec :: ConfigSpec Text) <- parseConfigSpec input+    case resolveCommandCliPure spec "program" ["test"] of+      Left err -> case fromException err of+        Just CliEvalExited{} -> return () -        Right _ ->-          assertFailure "Expecting required argument to fail cli resolving"+        _ ->+          assertFailure ("Expecting required validation to work on cli; got " <> show err) +      Right _ -> assertFailure "Expecting required argument to fail cli resolving"   , testCase "supports same cli input on multiple arguments" $ do-      let-        input =-          mconcat-            [-              "{ \"etc/cli\": {"-            , "    \"desc\": \"\""-            , "  , \"header\": \"\""-            , "  , \"commands\": {"-            , "      \"test\": {\"header\": \"\", \"desc\": \"\"}"-            , "    , \"other\": {\"header\": \"\", \"desc\": \"\"}}}"-            , ", \"etc/entries\": {"-            , "    \"greeting\": {"-            , "      \"etc/spec\": {"-            , "        \"cli\": {"-            , "          \"input\": \"option\""-            , "        , \"short\": \"g\""-            , "        , \"type\": \"string\""-            , "        , \"metavar\": \"GREETING\""-            , "        , \"required\": false"-            , "        , \"commands\": [\"test\", \"other\"]"-            , "}}}}}"-            ]-      (spec :: ConfigSpec Text) <- parseConfigSpec input--      (cmd1, config1) <- resolveCommandCliPure spec "program" ["test", "-g", "hello"]-      (cmd2, config2) <- resolveCommandCliPure spec "program" ["other", "-g", "hello"]--      assertEqual "" "test" cmd1-      assertEqual "" "other" cmd2-      assertEqual "" config1 config2-  ]---with_command :: TestTree-with_command =-  testGroup "when command given"-  [-    with_command_option_tests-  , with_command_argument_tests-  ]--without_command :: TestTree-without_command =-  testCase "fails when command not given" $ do-    let-      input =-        mconcat-          [-            "{ \"etc/cli\": {"+    let input = mconcat+          [ "{ \"etc/cli\": {"           , "    \"desc\": \"\""           , "  , \"header\": \"\""           , "  , \"commands\": {"-          , "      \"test\": {\"header\": \"\", \"desc\": \"\"}}}"+          , "      \"test\": {\"header\": \"\", \"desc\": \"\"}"+          , "    , \"other\": {\"header\": \"\", \"desc\": \"\"}}}"           , ", \"etc/entries\": {"           , "    \"greeting\": {"           , "      \"etc/spec\": {"@@ -364,29 +259,54 @@           , "        , \"short\": \"g\""           , "        , \"type\": \"string\""           , "        , \"metavar\": \"GREETING\""-          , "        , \"required\": true"-          , "        , \"commands\": [\"test\"]"+          , "        , \"required\": false"+          , "        , \"commands\": [\"test\", \"other\"]"           , "}}}}}"           ]     (spec :: ConfigSpec Text) <- parseConfigSpec input-    case resolveCommandCliPure spec "program" [] of-      Left err ->-        case fromException err of-          Just CliEvalExited {} ->-            return () -          _ ->-            assertFailure ("Expecting sub-command to be required; got "-                           <> show err)-      Right _ ->-        assertFailure "Expecting sub-command to be required; it wasn't"+    (cmd1, config1) <- resolveCommandCliPure spec "program" ["test", "-g", "hello"]+    (cmd2, config2) <- resolveCommandCliPure spec "program" ["other", "-g", "hello"] +    assertEqual "" "test"  cmd1+    assertEqual "" "other" cmd2+    assertEqual "" config1 config2+  ]  +with_command :: TestTree+with_command =+  testGroup "when command given" [with_command_option_tests, with_command_argument_tests]++without_command :: TestTree+without_command = testCase "fails when command not given" $ do+  let input = mconcat+        [ "{ \"etc/cli\": {"+        , "    \"desc\": \"\""+        , "  , \"header\": \"\""+        , "  , \"commands\": {"+        , "      \"test\": {\"header\": \"\", \"desc\": \"\"}}}"+        , ", \"etc/entries\": {"+        , "    \"greeting\": {"+        , "      \"etc/spec\": {"+        , "        \"cli\": {"+        , "          \"input\": \"option\""+        , "        , \"short\": \"g\""+        , "        , \"type\": \"string\""+        , "        , \"metavar\": \"GREETING\""+        , "        , \"required\": true"+        , "        , \"commands\": [\"test\"]"+        , "}}}}}"+        ]+  (spec :: ConfigSpec Text) <- parseConfigSpec input+  case resolveCommandCliPure spec "program" [] of+    Left err -> case fromException err of+      Just CliEvalExited{} -> return ()++      _ -> assertFailure ("Expecting sub-command to be required; got " <> show err)+    Right _ -> assertFailure "Expecting sub-command to be required; it wasn't"+++ tests :: TestTree-tests =-  testGroup "command"-  [-    with_command-  , without_command-  ]+tests = testGroup "command" [with_command, without_command]
test/System/Etc/Resolver/Cli/PlainTest.hs view
@@ -3,256 +3,187 @@ {-# LANGUAGE ScopedTypeVariables #-} module System.Etc.Resolver.Cli.PlainTest where -import Protolude+import           RIO+import qualified RIO.Set as Set  import Test.Tasty       (TestTree, testGroup) import Test.Tasty.HUnit (assertBool, assertFailure, testCase) -import qualified Data.Set as Set--import System.Etc+import qualified System.Etc as SUT  option_tests :: TestTree-option_tests =-  testGroup "option input"-  [-    testCase "entry accepts short" $ do-      let-        input =-          mconcat-            [-              "{ \"etc/entries\": {"-            , "    \"greeting\": {"-            , "      \"etc/spec\": {"-            , "        \"cli\": {"-            , "          \"input\": \"option\""-            , "        , \"short\": \"g\""-            , "        , \"long\": \"greeting\""-            , "        , \"type\": \"string\""-            , "}}}}}"-            ]-      (spec :: ConfigSpec ()) <- parseConfigSpec input-      config <- resolvePlainCliPure spec "program" ["-g", "hello cli"]--      case getAllConfigSources ["greeting"] config of-        Nothing ->-          assertFailure ("expecting to get entries for greeting\n"-                         <> show config)-        Just set ->-          assertBool ("expecting to see entry from env; got " <> show set)-                   (Set.member (Cli "hello cli") set)+option_tests = testGroup+  "option input"+  [ testCase "entry accepts short" $ do+    let input = mconcat+          [ "{ \"etc/entries\": {"+          , "    \"greeting\": {"+          , "      \"etc/spec\": {"+          , "        \"cli\": {"+          , "          \"input\": \"option\""+          , "        , \"short\": \"g\""+          , "        , \"long\": \"greeting\""+          , "        , \"type\": \"string\""+          , "}}}}}"+          ]+    (spec :: SUT.ConfigSpec ()) <- SUT.parseConfigSpec input+    config <- SUT.resolvePlainCliPure spec "program" ["-g", "hello cli"] +    case SUT.getAllConfigSources ["greeting"] config of+      Nothing   -> assertFailure ("expecting to get entries for greeting\n" <> show config)+      Just aSet -> assertBool ("expecting to see entry from env; got " <> show aSet)+                              (Set.member (SUT.Cli "hello cli") aSet)   , testCase "entry accepts long" $ do-      let-        input =-          mconcat-            [-              "{ \"etc/entries\": {"-            , "    \"greeting\": {"-            , "      \"etc/spec\": {"-            , "        \"cli\": {"-            , "          \"input\": \"option\""-            , "        , \"short\": \"g\""-            , "        , \"long\": \"greeting\""-            , "        , \"type\": \"string\""-            , "}}}}}"-            ]-      (spec :: ConfigSpec ()) <- parseConfigSpec input-      config <- resolvePlainCliPure spec "program" ["--greeting", "hello cli"]--      case getAllConfigSources ["greeting"] config of-        Nothing ->-          assertFailure ("expecting to get entries for greeting\n"-                         <> show config)-        Just set ->-          assertBool ("expecting to see entry from env; got " <> show set)-                   (Set.member (Cli "hello cli") set)+    let input = mconcat+          [ "{ \"etc/entries\": {"+          , "    \"greeting\": {"+          , "      \"etc/spec\": {"+          , "        \"cli\": {"+          , "          \"input\": \"option\""+          , "        , \"short\": \"g\""+          , "        , \"long\": \"greeting\""+          , "        , \"type\": \"string\""+          , "}}}}}"+          ]+    (spec :: SUT.ConfigSpec ()) <- SUT.parseConfigSpec input+    config <- SUT.resolvePlainCliPure spec "program" ["--greeting", "hello cli"] +    case SUT.getAllConfigSources ["greeting"] config of+      Nothing   -> assertFailure ("expecting to get entries for greeting\n" <> show config)+      Just aSet -> assertBool ("expecting to see entry from env; got " <> show aSet)+                              (Set.member (SUT.Cli "hello cli") aSet)   , testCase "entry gets validated with a type" $ do-      let-        input =-          mconcat-            [-              "{ \"etc/entries\": {"-            , "    \"greeting\": {"-            , "      \"etc/spec\": {"-            , "        \"cli\": {"-            , "          \"input\": \"option\""-            , "        , \"short\": \"g\""-            , "        , \"long\": \"greeting\""-            , "        , \"type\": \"number\""-            , "}}}}}"-            ]-      (spec :: ConfigSpec ()) <- parseConfigSpec input+    let input = mconcat+          [ "{ \"etc/entries\": {"+          , "    \"greeting\": {"+          , "      \"etc/spec\": {"+          , "        \"cli\": {"+          , "          \"input\": \"option\""+          , "        , \"short\": \"g\""+          , "        , \"long\": \"greeting\""+          , "        , \"type\": \"number\""+          , "}}}}}"+          ]+    (spec :: SUT.ConfigSpec ()) <- SUT.parseConfigSpec input -      case resolvePlainCliPure spec "program" ["--greeting", "hello cli"] of-        Left err ->-          case fromException err of-            Just CliEvalExited {} ->-              return ()+    case SUT.resolvePlainCliPure spec "program" ["--greeting", "hello cli"] of+      Left err -> case fromException err of+        Just SUT.CliEvalExited{} -> return () -            _ ->-              assertFailure ("Expecting type validation to work on cli; got "-                             <> show err)+        _ -> assertFailure ("Expecting type validation to work on cli; got " <> show err)  -        Right _ ->-          assertFailure "Expecting type validation to work on cli"-+      Right _ -> assertFailure "Expecting type validation to work on cli"   , testCase "entry with required false does not barf" $ do-      let-        input =-          mconcat-            [-              "{ \"etc/entries\": {"-            , "    \"greeting\": {"-            , "      \"etc/spec\": {"-            , "        \"cli\": {"-            , "          \"input\": \"option\""-            , "        , \"short\": \"g\""-            , "        , \"long\": \"greeting\""-            , "        , \"type\": \"string\""-            , "        , \"required\": false"-            , "}}}}}"-            ]-      (spec :: ConfigSpec ()) <- parseConfigSpec input-      config <- resolvePlainCliPure spec "program" []--      case getConfigValue ["greeting"] config of-        Just set ->-          assertFailure ("expecting to have no entry for greeting; got\n"-                       <> show set)+    let input = mconcat+          [ "{ \"etc/entries\": {"+          , "    \"greeting\": {"+          , "      \"etc/spec\": {"+          , "        \"cli\": {"+          , "          \"input\": \"option\""+          , "        , \"short\": \"g\""+          , "        , \"long\": \"greeting\""+          , "        , \"type\": \"string\""+          , "        , \"required\": false"+          , "}}}}}"+          ]+    (spec :: SUT.ConfigSpec ()) <- SUT.parseConfigSpec input+    config                      <- SUT.resolvePlainCliPure spec "program" [] -        (_ :: Maybe ()) ->-          return ()+    case SUT.getConfigValue ["greeting"] config of+      Just aSet ->+        assertFailure ("expecting to have no entry for greeting; got\n" <> show aSet) +      (_ :: Maybe ()) -> return ()   , testCase "entry with required fails when option not given" $ do-      let-        input =-          mconcat-            [-              "{ \"etc/entries\": {"-            , "    \"greeting\": {"-            , "      \"etc/spec\": {"-            , "        \"cli\": {"-            , "          \"input\": \"option\""-            , "        , \"short\": \"g\""-            , "        , \"long\": \"greeting\""-            , "        , \"type\": \"string\""-            , "        , \"required\": true"-            , "}}}}}"-            ]-      (spec :: ConfigSpec ()) <- parseConfigSpec input-      case resolvePlainCliPure spec "program" [] of-        Left err ->-          case fromException err of-            Just CliEvalExited {} ->-              return ()--            _ ->-              assertFailure ("Expecting required validation to work on cli; got "-                             <> show err)+    let input = mconcat+          [ "{ \"etc/entries\": {"+          , "    \"greeting\": {"+          , "      \"etc/spec\": {"+          , "        \"cli\": {"+          , "          \"input\": \"option\""+          , "        , \"short\": \"g\""+          , "        , \"long\": \"greeting\""+          , "        , \"type\": \"string\""+          , "        , \"required\": true"+          , "}}}}}"+          ]+    (spec :: SUT.ConfigSpec ()) <- SUT.parseConfigSpec input+    case SUT.resolvePlainCliPure spec "program" [] of+      Left err -> case fromException err of+        Just SUT.CliEvalExited{} -> return () -        Right _ ->-          assertFailure "Expecting required option to fail cli resolving"+        _ ->+          assertFailure ("Expecting required validation to work on cli; got " <> show err) +      Right _ -> assertFailure "Expecting required option to fail cli resolving"   ]  argument_tests :: TestTree-argument_tests =-  testGroup "argument input"-  [-    testCase "entry gets validated with a type" $ do-      let-        input =-          mconcat-            [-              "{ \"etc/entries\": {"-            , "    \"greeting\": {"-            , "      \"etc/spec\": {"-            , "        \"cli\": {"-            , "          \"input\": \"argument\""-            , "        , \"type\": \"number\""-            , "        , \"metavar\": \"GREETING\""-            , "}}}}}"-            ]-      (spec :: ConfigSpec ()) <- parseConfigSpec input--      case resolvePlainCliPure spec "program" ["hello cli"] of-        Left err ->-          case fromException err of-            Just CliEvalExited {} ->-              return ()+argument_tests = testGroup+  "argument input"+  [ testCase "entry gets validated with a type" $ do+    let input = mconcat+          [ "{ \"etc/entries\": {"+          , "    \"greeting\": {"+          , "      \"etc/spec\": {"+          , "        \"cli\": {"+          , "          \"input\": \"argument\""+          , "        , \"type\": \"number\""+          , "        , \"metavar\": \"GREETING\""+          , "}}}}}"+          ]+    (spec :: SUT.ConfigSpec ()) <- SUT.parseConfigSpec input -            _ ->-              assertFailure ("Expecting type validation to work on cli; got "-                             <> show err)+    case SUT.resolvePlainCliPure spec "program" ["hello cli"] of+      Left err -> case fromException err of+        Just SUT.CliEvalExited{} -> return () -        Right _ ->-          assertFailure "Expecting type validation to work on cli"+        _ -> assertFailure ("Expecting type validation to work on cli; got " <> show err) +      Right _ -> assertFailure "Expecting type validation to work on cli"   , testCase "entry with required false does not barf" $ do-      let-        input =-          mconcat-            [-              "{ \"etc/entries\": {"-            , "    \"greeting\": {"-            , "      \"etc/spec\": {"-            , "        \"cli\": {"-            , "          \"input\": \"argument\""-            , "        , \"type\": \"string\""-            , "        , \"metavar\": \"GREETING\""-            , "        , \"required\": false"-            , "}}}}}"-            ]-      (spec :: ConfigSpec ()) <- parseConfigSpec input-      config <- resolvePlainCliPure spec "program" []--      case getConfigValue ["greeting"] config of-        (Nothing :: Maybe ()) ->-          return ()+    let input = mconcat+          [ "{ \"etc/entries\": {"+          , "    \"greeting\": {"+          , "      \"etc/spec\": {"+          , "        \"cli\": {"+          , "          \"input\": \"argument\""+          , "        , \"type\": \"string\""+          , "        , \"metavar\": \"GREETING\""+          , "        , \"required\": false"+          , "}}}}}"+          ]+    (spec :: SUT.ConfigSpec ()) <- SUT.parseConfigSpec input+    config                      <- SUT.resolvePlainCliPure spec "program" [] -        Just set ->-          assertFailure ("expecting to have no entry for greeting; got\n"-                       <> show set)+    case SUT.getConfigValue ["greeting"] config of+      (Nothing :: Maybe ()) -> return () +      Just aSet ->+        assertFailure ("expecting to have no entry for greeting; got\n" <> show aSet)   , testCase "entry with required fails when argument not given" $ do-      let-        input =-          mconcat-            [-              "{ \"etc/entries\": {"-            , "    \"greeting\": {"-            , "      \"etc/spec\": {"-            , "        \"cli\": {"-            , "          \"input\": \"argument\""-            , "        , \"type\": \"string\""-            , "        , \"metavar\": \"GREETING\""-            , "        , \"required\": true"-            , "}}}}}"-            ]-      (spec :: ConfigSpec ()) <- parseConfigSpec input-      case resolvePlainCliPure spec "program" [] of-        Left err ->-          case fromException err of-            Just CliEvalExited {} ->-              return ()--            _ ->-              assertFailure ("Expecting required validation to work on cli; got "-                             <> show err)--        Right _ ->-          assertFailure "Expecting required argument to fail cli resolving"+    let input = mconcat+          [ "{ \"etc/entries\": {"+          , "    \"greeting\": {"+          , "      \"etc/spec\": {"+          , "        \"cli\": {"+          , "          \"input\": \"argument\""+          , "        , \"type\": \"string\""+          , "        , \"metavar\": \"GREETING\""+          , "        , \"required\": true"+          , "}}}}}"+          ]+    (spec :: SUT.ConfigSpec ()) <- SUT.parseConfigSpec input+    case SUT.resolvePlainCliPure spec "program" [] of+      Left err -> case fromException err of+        Just SUT.CliEvalExited{} -> return () +        _ ->+          assertFailure ("Expecting required validation to work on cli; got " <> show err) +      Right _ -> assertFailure "Expecting required argument to fail cli resolving"   ]  tests :: TestTree-tests =-  testGroup "plain"-  [-    option_tests-  , argument_tests-  ]+tests = testGroup "plain" [option_tests, argument_tests]
test/System/Etc/Resolver/CliTest.hs view
@@ -10,9 +10,6 @@   tests :: TestTree-tests =-  testGroup "cli"-  [-    System.Etc.Resolver.Cli.CommandTest.tests-  , System.Etc.Resolver.Cli.PlainTest.tests-  ]+tests = testGroup+  "cli"+  [System.Etc.Resolver.Cli.CommandTest.tests, System.Etc.Resolver.Cli.PlainTest.tests]
test/System/Etc/Resolver/DefaultTest.hs view
@@ -3,86 +3,51 @@ {-# LANGUAGE ScopedTypeVariables #-} module System.Etc.Resolver.DefaultTest (tests) where -import Protolude+import           RIO+import qualified RIO.Set as Set  import qualified Data.Aeson       as JSON import           Test.Tasty       (TestTree, testGroup) import           Test.Tasty.HUnit (assertBool, assertFailure, testCase)  -import qualified Data.Set as Set  import System.Etc  tests :: TestTree-tests =-  testGroup "default"-  [-    testCase "default is used when defined on spec" $ do-      let-        input =-          mconcat-            [-              "{\"etc/entries\": {"-            , " \"greeting\": { \"etc/spec\": { \"default\": \"hello default\" }}}}"-            ]-      (spec :: ConfigSpec ()) <- parseConfigSpec input--      let-        config =-            resolveDefault spec+tests = testGroup+  "default"+  [ testCase "default is used when defined on spec" $ do+    let input = mconcat+          [ "{\"etc/entries\": {"+          , " \"greeting\": { \"etc/spec\": { \"default\": \"hello default\" }}}}"+          ]+    (spec :: ConfigSpec ()) <- parseConfigSpec input -      case getAllConfigSources ["greeting"] config of-        Nothing ->-          assertFailure ("expecting to get entries for greeting\n"-                         <> show config)-        Just set ->-          assertBool ("expecting to see entry from env; got " <> show set)-                   (Set.member (Default "hello default") set)+    let config = resolveDefault spec +    case getAllConfigSources ["greeting"] config of+      Nothing   -> assertFailure ("expecting to get entries for greeting\n" <> show config)+      Just aSet -> assertBool ("expecting to see entry from env; got " <> show aSet)+                              (Set.member (Default "hello default") aSet)   , testCase "default can be raw JSON value on entries spec" $ do-      let-        input =-          mconcat-            [-              "{\"etc/entries\": {"-            , " \"greeting\": \"hello default\"}}"-            ]-      (spec :: ConfigSpec ()) <- parseConfigSpec input--      let-        config =-            resolveDefault spec+    let input = mconcat ["{\"etc/entries\": {", " \"greeting\": \"hello default\"}}"]+    (spec :: ConfigSpec ()) <- parseConfigSpec input -      case getAllConfigSources ["greeting"] config of-        Nothing ->-          assertFailure ("expecting to get entries for greeting\n"-                         <> show config)-        Just set ->-          assertBool ("expecting to see entry from env; got " <> show set)-                   (Set.member (Default "hello default") set)+    let config = resolveDefault spec +    case getAllConfigSources ["greeting"] config of+      Nothing   -> assertFailure ("expecting to get entries for greeting\n" <> show config)+      Just aSet -> assertBool ("expecting to see entry from env; got " <> show aSet)+                              (Set.member (Default "hello default") aSet)   , testCase "default can be a null JSON value" $ do-      let-        input =-          mconcat-            [-              "{\"etc/entries\": {"-            , " \"greeting\": null}}"-            ]-      (spec :: ConfigSpec ()) <- parseConfigSpec input--      let-        config =-            resolveDefault spec--      case getAllConfigSources ["greeting"] config of-        Nothing ->-          assertFailure ("expecting to get entries for greeting\n"-                         <> show config)-        Just set ->-          assertBool ("expecting to see entry from env; got " <> show set)-                     (Set.member (Default JSON.Null) set)+    let input = mconcat ["{\"etc/entries\": {", " \"greeting\": null}}"]+    (spec :: ConfigSpec ()) <- parseConfigSpec input +    let config = resolveDefault spec +    case getAllConfigSources ["greeting"] config of+      Nothing   -> assertFailure ("expecting to get entries for greeting\n" <> show config)+      Just aSet -> assertBool ("expecting to see entry from env; got " <> show aSet)+                              (Set.member (Default $ Plain JSON.Null) aSet)   ]
test/System/Etc/Resolver/EnvTest.hs view
@@ -3,96 +3,72 @@ {-# LANGUAGE ScopedTypeVariables #-} module System.Etc.Resolver.EnvTest where -import Protolude+import           RIO+import qualified RIO.Set  as Set+import qualified RIO.Text as Text  import Test.Tasty       (TestTree, testGroup) import Test.Tasty.HUnit (assertBool, assertEqual, assertFailure, testCase)  -import qualified Data.Set  as Set-import qualified Data.Text as Text- import Paths_etc (getDataFileName)  import System.Etc   tests :: TestTree-tests =-  testGroup "env"-  [-    testCase "env entry is present when env var is defined" $ do-      let-        input =-          mconcat-            [-              "{\"etc/entries\": {"-            , " \"greeting\": { \"etc/spec\": { \"env\": \"GREETING\" }}}}"-            ]-      (spec :: ConfigSpec ()) <- parseConfigSpec input--      let-        config =-            resolveEnvPure spec [("GREETING", "hello env")]+tests = testGroup+  "env"+  [ testCase "env entry is present when env var is defined" $ do+    let input = mconcat+          [ "{\"etc/entries\": {"+          , " \"greeting\": { \"etc/spec\": { \"env\": \"GREETING\" }}}}"+          ]+    (spec :: ConfigSpec ()) <- parseConfigSpec input -      case getAllConfigSources ["greeting"] config of-        Nothing ->-          assertFailure ("expecting to get entries for greeting (check fixtures)\n"-                         <> show config)-        Just set ->-          assertBool ("expecting to see entry from env; got " <> show set)-                   (Set.member (Env "GREETING" "hello env") set)+    let config = resolveEnvPure spec [("GREETING", "hello env")] +    case getAllConfigSources ["greeting"] config of+      Nothing -> assertFailure+        ("expecting to get entries for greeting (check fixtures)\n" <> show config)+      Just aSet -> assertBool ("expecting to see entry from env; got " <> show aSet)+                              (Set.member (Env "GREETING" "hello env") aSet)   , testCase "has precedence over default and file values" $ do-      jsonFilepath <- getDataFileName "test/fixtures/config.json"-      let-        input =-          mconcat-            [-              "{\"etc/filepaths\": ["-            , "\"" <> Text.pack jsonFilepath <> "\""-            , "],"-            , " \"etc/entries\": {"-            , " \"greeting\": { \"etc/spec\": { \"env\": \"GREETING\" }}}}"-            ]-      (spec :: ConfigSpec ()) <- parseConfigSpec input-      (configFile, _) <- resolveFiles spec--      let-        configEnv =-          resolveEnvPure spec [("GREETING", "hello env")]+    jsonFilepath <- getDataFileName "test/fixtures/config.json"+    let input = mconcat+          [ "{\"etc/filepaths\": ["+          , "\"" <> Text.pack jsonFilepath <> "\""+          , "],"+          , " \"etc/entries\": {"+          , " \"greeting\": { \"etc/spec\": { \"env\": \"GREETING\" }}}}"+          ]+    (spec :: ConfigSpec ()) <- parseConfigSpec input+    (configFile, _)         <- resolveFiles spec -        config =-            configEnv `mappend` configFile+    let configEnv = resolveEnvPure spec [("GREETING", "hello env")] -      case getConfigValue ["greeting"] config of-        Nothing ->-          assertFailure ("expecting to get entries for greeting (check fixtures)\n"-                         <> show config)-        Just result ->-          assertEqual ("expecting to see entry from env " <> show result)-                    ("hello env" :: Text)-                    result+        config    = configEnv `mappend` configFile +    case getConfigValue ["greeting"] config of+      Nothing -> assertFailure+        ("expecting to get entries for greeting (check fixtures)\n" <> show config)+      Just result -> assertEqual ("expecting to see entry from env " <> show result)+                                 ("hello env" :: Text)+                                 result   , testCase "does not add entries to config if env var is not present" $ do-      let-        input =-          mconcat-            [-              "{\"etc/entries\": {"-            , " \"nested\": {\"greeting\": { \"etc/spec\": { \"env\": \"GREETING\" }}}}}"-            ]-      (spec :: ConfigSpec ()) <- parseConfigSpec input+    let input = mconcat+          [ "{\"etc/entries\": {"+          , " \"nested\": {\"greeting\": { \"etc/spec\": { \"env\": \"GREETING\" }}}}}"+          ]+    (spec :: ConfigSpec ()) <- parseConfigSpec input -      let-        config =-          resolveEnvPure spec []+    let config = resolveEnvPure spec [] -      assertEqual "expecting to not have an entry for key"-                  (Nothing :: Maybe Text)-                  (getConfigValue ["nested"] config)+    assertEqual "expecting to not have an entry for key"+                (Nothing :: Maybe Text)+                (getConfigValue ["nested"] config) -      assertEqual "expecting to not have an entry for key"-                  (Nothing :: Maybe Text)-                  (getConfigValue ["nested", "greeting"] config)+    assertEqual "expecting to not have an entry for key"+                (Nothing :: Maybe Text)+                (getConfigValue ["nested", "greeting"] config)   ]
test/System/Etc/Resolver/FileTest.hs view
@@ -5,127 +5,136 @@ {-# LANGUAGE ScopedTypeVariables #-} module System.Etc.Resolver.FileTest (tests) where -import Protolude+import           RIO+import qualified RIO.Set    as Set+import qualified RIO.Text   as Text+import qualified RIO.Vector as Vector  import Test.Tasty       (TestTree, testGroup) import Test.Tasty.HUnit (assertBool, assertEqual, assertFailure, testCase) -import qualified Data.Set    as Set-import qualified Data.Text   as Text-import qualified Data.Vector as Vector--import Paths_etc (getDataFileName)+import qualified Data.Aeson as JSON+import           Paths_etc  (getDataFileName)  import System.Etc   tests :: TestTree-tests =-  testGroup "file"-  [-    testCase "supports json, yaml and yml extensions" $ do-      jsonFilepath <- getDataFileName "test/fixtures/config.json"+tests = testGroup+  "file"+  [ testCase "supports json, yaml and yml extensions" $ do+    jsonFilepath <- getDataFileName "test/fixtures/config.json" #ifdef WITH_YAML-      yamlFilepath <- getDataFileName "test/fixtures/config.yaml"-      ymlFilepath  <- getDataFileName "test/fixtures/config.yml"+    yamlFilepath <- getDataFileName "test/fixtures/config.yaml"+    ymlFilepath  <- getDataFileName "test/fixtures/config.yml" #endif -      let-        input =-          mconcat-            [-              "{\"etc/filepaths\": ["-            , "\"" <> Text.pack jsonFilepath <> "\""+    let input = mconcat+          [ "{\"etc/filepaths\": ["+          , "\"" <> Text.pack jsonFilepath <> "\"" #ifdef WITH_YAML             , ", \"" <> Text.pack yamlFilepath <> "\""             , ", \"" <> Text.pack ymlFilepath <> "\"" #endif-            , "]}"-            ]+          , "]}"+          ] -      (spec :: ConfigSpec ()) <- parseConfigSpec input-      (config, _) <- resolveFiles spec+    (spec :: ConfigSpec ()) <- parseConfigSpec input+    (config, _)             <- resolveFiles spec -      case getAllConfigSources ["greeting"] config of-        Nothing ->-          assertFailure ("expecting to get entries for greeting (check fixtures)\n"-                         <> show config)-        Just set -> do-          assertBool ("expecting to see entry from json config file " <> show set)-                     (Set.member (File 1 (Text.pack jsonFilepath) "hello json") set)+    case getAllConfigSources ["greeting"] config of+      Nothing -> assertFailure+        ("expecting to get entries for greeting (check fixtures)\n" <> show config)+      Just aSet -> assertBool+        ("expecting to see entry from json config file " <> show aSet)+        (Set.member (File 1 (Text.pack jsonFilepath) "hello json") aSet)  #ifdef WITH_YAML-          assertBool ("expecting to see entry from yaml config file " <> show set)-                     (Set.member (File 2 (Text.pack jsonFilepath) "hello yaml") set)+       >> assertBool ("expecting to see entry from yaml config file " <> show aSet)+                     (Set.member (File 2 (Text.pack jsonFilepath) "hello yaml") aSet) -          assertBool ("expecting to see entry from yml config file " <> show set)-                     (Set.member (File 3 (Text.pack jsonFilepath) "hello yml") set)+       >> assertBool ("expecting to see entry from yml config file " <> show aSet)+                     (Set.member (File 3 (Text.pack jsonFilepath) "hello yml") aSet) #endif-   , testCase "does not support any other file extension" $ do-      fooFilepath <- getDataFileName "test/fixtures/config.foo"-      let-        input =-          mconcat-            [-              "{\"etc/filepaths\": ["-            , "\"" <> Text.pack fooFilepath <> "\""-            , "]}"-            ]+    fooFilepath <- getDataFileName "test/fixtures/config.foo"+    let input =+          mconcat ["{\"etc/filepaths\": [", "\"" <> Text.pack fooFilepath <> "\"", "]}"] -      (spec :: ConfigSpec ()) <- parseConfigSpec input-      (config, errs) <- resolveFiles spec+    (spec :: ConfigSpec ()) <- parseConfigSpec input+    (config, errs)          <- resolveFiles spec -      assertEqual "config should be empty" mempty config+    assertEqual "config should be empty" mempty config -      if Vector.null errs then-        assertFailure "expecting one error, got none"+    if Vector.null errs+      then assertFailure "expecting one error, got none"       else-        let-          err = Vector.head errs+        let err = Vector.head errs         in           case fromException err of-            Just (InvalidConfiguration _) ->-              return ()-            _ ->-              assertFailure ("Expecting InvalidConfigurationError; got instead "-                             <> show err)--+            Just (InvalidConfiguration _ _) -> return ()+            _                               -> assertFailure+              ("Expecting InvalidConfigurationError; got instead " <> show err)   , testCase "does not fail if file doesn't exist" $ do-      jsonFilepath <- getDataFileName "test/fixtures/config.json"-      let-        input =-          mconcat-            [-              "{\"etc/filepaths\": ["-            , "\"" <> Text.pack jsonFilepath <> "\""-            , ", \"unknown_file.json\""-            , "]}"-            ]+    jsonFilepath <- getDataFileName "test/fixtures/config.json"+    let input = mconcat+          [ "{\"etc/filepaths\": ["+          , "\"" <> Text.pack jsonFilepath <> "\""+          , ", \"unknown_file.json\""+          , "]}"+          ] -      (spec :: ConfigSpec ()) <- parseConfigSpec input-      (config, errs) <- resolveFiles spec+    (spec :: ConfigSpec ()) <- parseConfigSpec input+    (config, errs)          <- resolveFiles spec -      case getAllConfigSources ["greeting"] config of-        Nothing ->-          assertFailure ("expecting to get entries for greeting (check fixtures)\n"-                         <> show config)-        Just set ->-          assertBool ("expecting to see entry from json config file " <> show set)-                   (Set.member (File 1 (Text.pack jsonFilepath) "hello json") set)+    case getAllConfigSources ["greeting"] config of+      Nothing -> assertFailure+        ("expecting to get entries for greeting (check fixtures)\n" <> show config)+      Just aSet -> assertBool+        ("expecting to see entry from json config file " <> show aSet)+        (Set.member (File 1 (Text.pack jsonFilepath) "hello json") aSet) -      if Vector.null errs then-        assertFailure "expecting one error, got none"+    if Vector.null errs+      then assertFailure "expecting one error, got none"       else-        let-          err =-            Vector.head errs+        let err = Vector.head errs         in           case fromException err of-            Just (ConfigurationFileNotFound _) ->-              return ()-            _ ->-              assertFailure ("Expecting ConfigurationFileNotFound; got instead "-                             <> show err)+            Just (ConfigurationFileNotFound _) -> return ()+            _ -> assertFailure+              ("Expecting ConfigurationFileNotFound; got instead " <> show err)+  , testCase "null file input has no precedence over default values" $ do+    jsonFilepath <- getDataFileName "test/fixtures/config.null.json"+    let input = mconcat+          [ "{\"etc/filepaths\": ["+          , "\"" <> Text.pack jsonFilepath <> "\""+          , "]"+          , ", \"etc/entries\": {"+          , "    \"greeting\": {"+          , "      \"etc/spec\": {"+          , "        \"default\": \"hola\""+          , "}}}}"+          ]++    (spec :: ConfigSpec ()) <- parseConfigSpec input+    (configFiles, _errs)    <- resolveFiles spec+    let configDef = resolveDefault spec+        config    = configDef <> configFiles++    case getAllConfigSources ["greeting"] config of+      Nothing -> assertFailure ("expecting to get entries for greeting\n" <> show config)+      Just aSet ->+        let result = any+              (\entry -> case entry of+                File _ _ (Plain JSON.Null) -> True+                _                          -> False+              )+              aSet+        in  assertBool ("expecting to see entry from env; got " <> show aSet) result++    case getConfigValue ["greeting"] config of+      Just greeting -> assertEqual "Didn't take default value, despite CLI being null"+                                   ("hola" :: Text)+                                   greeting+      Nothing -> assertFailure "Expecting config value, but got nothing"   ]
test/System/Etc/SpecTest.hs view
@@ -5,15 +5,13 @@ {-# LANGUAGE ScopedTypeVariables #-} module System.Etc.SpecTest (tests) where -import Protolude+import           RIO+import qualified RIO.HashMap as HashMap  import Test.Tasty       (TestTree, testGroup) import Test.Tasty.HUnit (assertBool, assertEqual, assertFailure, testCase) -import Data.HashMap.Strict (HashMap)--import qualified Data.Aeson          as JSON-import qualified Data.HashMap.Strict as HashMap+import qualified Data.Aeson as JSON  import System.Etc.Internal.Spec.Types import System.Etc.Spec@@ -23,151 +21,111 @@ import           Paths_etc (getDataFileName) #endif - getConfigValue :: [Text] -> HashMap Text (ConfigValue cmd) -> Maybe (ConfigValue cmd) getConfigValue keys hsh =   -- NOTE: For some reason, pattern matching doesn't work on lts-6, using   -- if/else instead-  if null keys then-      Nothing+                          if null keys+  then Nothing   else do-    let-      (k:ks) =-        keys+    let (k : ks) = keys     cv <- HashMap.lookup k hsh     case cv of-      SubConfig hsh1 ->-        getConfigValue ks hsh1-      value ->-        if null ks then-          Just value-        else-          Nothing+      SubConfig hsh1 -> getConfigValue ks hsh1+      value          -> if null ks then Just value else Nothing   general_tests :: TestTree-general_tests =-  testGroup "general"-  [-    testCase "does not fail when etc/entries is not defined" $ do-      let-        input = "{}"--      case parseConfigSpec input of-        Left _ ->-          assertFailure "should not fail if no etc/entries key is present"-        Right (_ :: ConfigSpec ()) ->-          assertBool "" True+general_tests = testGroup+  "general"+  [ testCase "does not fail when etc/entries is not defined" $ do+    let input = "{}" +    case parseConfigSpec input of+      Nothing -> assertFailure "should not fail if no etc/entries key is present"+      Just (_ :: ConfigSpec ()) -> assertBool "" True   , testCase "entries cannot finish in an empty map" $ do-      let-        input = "{\"etc/entries\":{\"greeting\":{}}}"--      case parseConfigSpec input of-        Right (result :: ConfigSpec ()) ->-          assertFailure $ "entries should not accept empty maps as values " ++ show result-        Left _ ->-          assertBool "" True+    let input = "{\"etc/entries\":{\"greeting\":{}}}" +    case parseConfigSpec input of+      Just (result :: ConfigSpec ()) ->+        assertFailure $ "entries should not accept empty maps as values " ++ show result+      Nothing -> assertBool "" True   , testCase "entries cannot finish in an array" $ do-      let-        input = "{\"etc/entries\":{\"greeting\":[]}}"--      case parseConfigSpec input of-        Right (result :: ConfigSpec ()) ->-          assertFailure $ "entries should not accept arrays as values " ++ show result-        Left _ ->-          assertBool "" True+    let input = "{\"etc/entries\":{\"greeting\":[]}}" +    case parseConfigSpec input of+      Just (result :: ConfigSpec ()) ->+        assertFailure $ "entries should not accept arrays as values " ++ show result+      Nothing -> assertBool "" True   , testCase "entries that finish with raw values sets them as default value" $ do-      let-        input = "{\"etc/entries\":{\"greeting\":123}}"-        keys = ["greeting"]--      config <- parseConfigSpec input+    let input = "{\"etc/entries\":{\"greeting\":123}}"+        keys  = ["greeting"] -      case getConfigValue keys (specConfigValues config) of-        Nothing ->-          assertFailure (show keys ++ " should map to a config value, got sub config map instead")-        Just (value :: ConfigValue ()) ->-          assertEqual "should contain default value"-                      (Just (JSON.Number 123))-                      (defaultValue value)+    config <- parseConfigSpec input +    case getConfigValue keys (specConfigValues config) of+      Nothing -> assertFailure+        (show keys ++ " should map to a config value, got sub config map instead")+      Just (value :: ConfigValue ()) -> assertEqual "should contain default value"+                                                    (Just (JSON.Number 123))+                                                    (defaultValue value)   , testCase "entries can have many levels of nesting" $ do-      let-        input = "{\"etc/entries\":{\"english\":{\"greeting\":\"hello\"}}}"-        keys = ["english", "greeting"]--      config <- parseConfigSpec input+    let input = "{\"etc/entries\":{\"english\":{\"greeting\":\"hello\"}}}"+        keys  = ["english", "greeting"] -      case getConfigValue keys (specConfigValues config) of-        Nothing ->-          assertFailure (show keys ++ " should map to a config value, got sub config map instead")-        Just (value :: ConfigValue ()) ->-          assertEqual "should contain default value"-                      (Just (JSON.String "hello"))-                      (defaultValue value)+    config <- parseConfigSpec input +    case getConfigValue keys (specConfigValues config) of+      Nothing -> assertFailure+        (show keys ++ " should map to a config value, got sub config map instead")+      Just (value :: ConfigValue ()) -> assertEqual "should contain default value"+                                                    (Just (JSON.String "hello"))+                                                    (defaultValue value)   , testCase "spec map cannot be empty object" $ do-      let-        input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{}}}"--      case parseConfigSpec input of-        Right (result :: ConfigSpec ()) ->-          assertFailure $ "etc/spec map should not be an empty object " ++ show result-        Left _ ->-          assertBool "" True+    let input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{}}}" +    case parseConfigSpec input of+      Just (result :: ConfigSpec ()) ->+        assertFailure $ "etc/spec map should not be an empty object " ++ show result+      Nothing -> assertBool "" True   , testCase "spec map cannot be a JSON array" $ do-      let-        input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":[]}}"--      case parseConfigSpec input of-        Right (result :: ConfigSpec ()) ->-          assertFailure $ "etc/spec map should not be an array " ++ show result-        Left _ ->-          assertBool "" True+    let input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":[]}}" +    case parseConfigSpec input of+      Just (result :: ConfigSpec ()) ->+        assertFailure $ "etc/spec map should not be an array " ++ show result+      Nothing -> assertBool "" True   , testCase "spec map cannot be a JSON bool" $ do-      let-        input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":true}}"--      case parseConfigSpec input of-        Right (result :: ConfigSpec ()) ->-          assertFailure $ "etc/spec map should not be a boolean " ++ show result-        Left _ ->-          assertBool "" True+    let input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":true}}" +    case parseConfigSpec input of+      Just (result :: ConfigSpec ()) ->+        assertFailure $ "etc/spec map should not be a boolean " ++ show result+      Nothing -> assertBool "" True   , testCase "spec map cannot be a JSON string" $ do-      let-        input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":\"hello\"}}}"--      case parseConfigSpec input of-        Right (result :: ConfigSpec ()) ->-          assertFailure $ "etc/spec map should not be a string " ++ show result-        Left _ ->-          assertBool "" True+    let input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":\"hello\"}}}" +    case parseConfigSpec input of+      Just (result :: ConfigSpec ()) ->+        assertFailure $ "etc/spec map should not be a string " ++ show result+      Nothing -> assertBool "" True   , testCase "spec map cannot be a JSON number" $ do-      let-        input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":123}}}"--      case parseConfigSpec input of-        Right (result :: ConfigSpec ()) ->-          assertFailure $ "etc/spec map should not be a number " ++ show result-        Left _ ->-          assertBool "" True+    let input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":123}}}" +    case parseConfigSpec input of+      Just (result :: ConfigSpec ()) ->+        assertFailure $ "etc/spec map should not be a number " ++ show result+      Nothing -> assertBool "" True   , testCase "spec map cannot have any other key that is not etc/spec" $ do-      let-        input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"default\":\"hello\"},\"foobar\":123}}}"+    let+      input+        = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"default\":\"hello\"},\"foobar\":123}}}" -      case parseConfigSpec input of-        Right (result :: ConfigSpec ()) ->-          assertFailure $ "etc/spec map should not contain more than one key " ++ show result-        Left _ ->-          assertBool "" True+    case parseConfigSpec input of+      Just (result :: ConfigSpec ()) ->+        assertFailure $ "etc/spec map should not contain more than one key " ++ show result+      Nothing -> assertBool "" True   ]  #ifdef WITH_CLI@@ -180,9 +138,9 @@         input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"cli\":{}}}}}"        case parseConfigSpec input of-        Right (result :: ConfigSpec ()) ->+        Just (result :: ConfigSpec ()) ->           assertFailure $ "cli entry should require a type " ++ show result-        Left _ ->+        Nothing ->           assertBool "" True    , testCase "cli option entry requires either short or long" $ do@@ -190,9 +148,9 @@         input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"cli\":{\"input\":\"option\"}}}}}"        case parseConfigSpec input of-        Right (result :: ConfigSpec ()) ->+        Just (result :: ConfigSpec ()) ->           assertFailure $ "cli entry should require a type " ++ show result-        Left _ ->+        Nothing ->           assertBool "" True    , testCase "cli option entry works when setting short and type" $ do@@ -268,32 +226,29 @@         input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"cli\":{\"input\":\"option\",\"long\":\"greeting\",\"type\":\"string\",\"command\":[\"foo\"]}}}}}"        case parseConfigSpec input of-        Right (result :: ConfigSpec ()) ->+        Just (result :: ConfigSpec ()) ->           assertFailure $ "cli entry should fail on invalid entry " ++ show result-        Left _ ->+        Nothing ->           assertBool "" True    ] #endif  envvar_tests :: TestTree-envvar_tests =-  testGroup "env"-  [-    testCase "env key creates an ENV source" $ do-      let-        input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"env\":\"GREETING\"}}}}"-        keys  = ["greeting"]+envvar_tests = testGroup+  "env"+  [ testCase "env key creates an ENV source" $ do+      let input = "{\"etc/entries\":{\"greeting\":{\"etc/spec\":{\"env\":\"GREETING\"}}}}"+          keys  = ["greeting"]        (config :: ConfigSpec ()) <- parseConfigSpec input        case getConfigValue keys (specConfigValues config) of-        Nothing ->-          assertFailure (show keys ++ " should map to a config value, got sub config map instead")-        Just value ->-          assertEqual "should contain EnvVar value"-                      (ConfigSources (Just "GREETING") Nothing)-                      (configSources value)+        Nothing -> assertFailure+          (show keys ++ " should map to a config value, got sub config map instead")+        Just value -> assertEqual "should contain EnvVar value"+                                  (ConfigSources (Just "GREETING") Nothing)+                                  (configSources value)   ]  #ifdef WITH_YAML@@ -324,10 +279,9 @@ #endif  tests :: TestTree-tests =-  testGroup "spec"-  [-    general_tests+tests = testGroup+  "spec"+  [ general_tests #ifdef WITH_YAML   , yaml_tests #endif
test/TestSuite.hs view
@@ -3,12 +3,12 @@ {-# LANGUAGE OverloadedStrings #-} module Main where -import Protolude+import RIO -import Test.Tasty                   (defaultMainWithIngredients, testGroup)-import Test.Tasty.Ingredients.Rerun (rerunningTests)-import Test.Tasty.Runners           (consoleTestReporter, listingTests)+import Test.Tasty         (defaultMainWithIngredients, testGroup)+import Test.Tasty.Runners (consoleTestReporter, listingTests) +import qualified System.Etc.ConfigTest import qualified System.Etc.Resolver.DefaultTest import qualified System.Etc.Resolver.EnvTest import qualified System.Etc.Resolver.FileTest@@ -23,13 +23,15 @@ #endif  main :: IO ()-main =-  defaultMainWithIngredients-    [ rerunningTests [listingTests, consoleTestReporter] ]-    (testGroup "etc" [ System.Etc.SpecTest.tests-                     , System.Etc.Resolver.DefaultTest.tests-                     , System.Etc.Resolver.FileTest.tests-                     , System.Etc.Resolver.EnvTest.tests+main = defaultMainWithIngredients+  [listingTests, consoleTestReporter]+  (testGroup+    "etc"+    [ System.Etc.SpecTest.tests+    , System.Etc.ConfigTest.tests+    , System.Etc.Resolver.DefaultTest.tests+    , System.Etc.Resolver.FileTest.tests+    , System.Etc.Resolver.EnvTest.tests #ifdef WITH_CLI                      , System.Etc.Resolver.CliTest.tests #endif@@ -37,4 +39,5 @@ #ifdef WITH_EXTRA                      , System.Etc.Extra.EnvMisspellTest.tests #endif-                     ])+    ]+  )
+ test/fixtures/config.null.json view
@@ -0,0 +1,3 @@+{+    "greeting": null+}