packages feed

etc 0.3.0.0 → 0.3.1.0

raw patch · 9 files changed

+194/−42 lines, 9 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

+ System.Etc.Internal.Spec.Types: FilePathsSpec :: ![Text] -> FilesSpec
+ System.Etc.Internal.Spec.Types: FilesSpec :: !(Maybe Text) -> ![Text] -> FilesSpec
+ System.Etc.Internal.Spec.Types: [fileLocationEnvVar] :: FilesSpec -> !(Maybe Text)
+ System.Etc.Internal.Spec.Types: [fileLocationPaths] :: FilesSpec -> ![Text]
+ System.Etc.Internal.Spec.Types: data FilesSpec
+ System.Etc.Internal.Spec.Types: instance GHC.Classes.Eq System.Etc.Internal.Spec.Types.FilesSpec
+ System.Etc.Internal.Spec.Types: instance GHC.Show.Show System.Etc.Internal.Spec.Types.FilesSpec
+ System.Etc.Internal.Spec.Types: parseFilePaths :: Value -> Parser FilesSpec
+ System.Etc.Internal.Spec.Types: parseFileSpec :: Value -> Parser (Maybe FilesSpec)
+ System.Etc.Internal.Spec.Types: parseFiles :: Value -> Parser FilesSpec
+ System.Etc.Internal.Types: EnvVarFileSource :: !Text -> !Text -> FileSource
+ System.Etc.Internal.Types: FilePathSource :: !Text -> FileSource
+ System.Etc.Internal.Types: [fileSourceEnvVar] :: FileSource -> !Text
+ System.Etc.Internal.Types: [fileSourcePath] :: FileSource -> !Text
+ System.Etc.Internal.Types: data FileSource
+ System.Etc.Internal.Types: instance GHC.Classes.Eq System.Etc.Internal.Types.FileSource
+ System.Etc.Internal.Types: instance GHC.Show.Show System.Etc.Internal.Types.FileSource
- System.Etc: File :: !Int -> !Text -> !(Value Value) -> ConfigSource
+ System.Etc: File :: !Int -> !FileSource -> !(Value Value) -> ConfigSource
- System.Etc: [filepath] :: ConfigSource -> !Text
+ System.Etc: [filepath] :: ConfigSource -> !FileSource
- System.Etc.Internal.Spec.Types: ConfigSpec :: ![Text] -> !(Maybe CliProgramSpec) -> !(HashMap Text (ConfigValue cmd)) -> ConfigSpec cmd
+ System.Etc.Internal.Spec.Types: ConfigSpec :: !(Maybe FilesSpec) -> !(Maybe CliProgramSpec) -> !(HashMap Text (ConfigValue cmd)) -> ConfigSpec cmd
- System.Etc.Internal.Spec.Types: [specConfigFilepaths] :: ConfigSpec cmd -> ![Text]
+ System.Etc.Internal.Spec.Types: [specConfigFilepaths] :: ConfigSpec cmd -> !(Maybe FilesSpec)
- System.Etc.Internal.Types: File :: !Int -> !Text -> !(Value Value) -> ConfigSource
+ System.Etc.Internal.Types: File :: !Int -> !FileSource -> !(Value Value) -> ConfigSource
- System.Etc.Internal.Types: [filepath] :: ConfigSource -> !Text
+ System.Etc.Internal.Types: [filepath] :: ConfigSource -> !FileSource

Files

CHANGELOG.md view
@@ -1,3 +1,10 @@+0.3.1.0+----++* Add new `etc/files` entry to the spec file which allows to specify an+  environment variable to fetch a file++ 0.3.0.0 ---- **BREAKING CHANGES**
etc.cabal view
@@ -1,5 +1,5 @@ name: etc-version: 0.3.0.0+version: 0.3.1.0 cabal-version: >=1.10 build-type: Simple license: MIT@@ -18,6 +18,7 @@ data-files:     test/fixtures/config.foo     test/fixtures/config.json+    test/fixtures/config.env.json     test/fixtures/config.null.json     test/fixtures/config.spec.yaml     test/fixtures/config.yaml
src/System/Etc/Internal/Extra/Printer.hs view
@@ -54,10 +54,21 @@     renderSource key source' = case source' of       Default value' -> (renderJsonValue key value', brackets' (fill 10 (text "Default"))) -      File _index filepath' value' ->-        ( renderJsonValue key value'-        , brackets' (fill 10 (text "File:" <+> text (Text.unpack filepath')))-        )+      File _index fileSource value' -> case fileSource of+        FilePathSource filepath' ->+          ( renderJsonValue key value'+          , brackets' (fill 10 (text "File:" <+> text (Text.unpack filepath')))+          )+        EnvVarFileSource envVar filepath' ->+          ( renderJsonValue key value'+          , brackets'+            (fill+              10+              (text "File:" <+> text (Text.unpack envVar) <> "=" <> text+                (Text.unpack filepath')+              )+            )+          )        Env varname value' ->         ( renderJsonValue key value'
src/System/Etc/Internal/Resolver/File.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE CPP               #-}+{-# LANGUAGE NamedFieldPuns    #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} module System.Etc.Internal.Resolver.File (resolveFiles) where@@ -18,6 +19,8 @@ import qualified Data.Aeson.Internal as JSON (IResult (..), iparse) import qualified RIO.ByteString.Lazy as LB8 +import System.Environment (lookupEnv)+ import qualified System.Etc.Internal.Spec.Types as Spec import           System.Etc.Internal.Types      hiding (filepath) @@ -31,8 +34,13 @@ --------------------------------------------------------------------------------  parseConfigValue-  :: Monad m => Maybe (Spec.ConfigValue cmd) -> Int -> Text -> JSON.Value -> m ConfigValue-parseConfigValue mSpec fileIndex filepath json = case json of+  :: Monad m+  => Maybe (Spec.ConfigValue cmd)+  -> Int+  -> FileSource+  -> JSON.Value+  -> m ConfigValue+parseConfigValue mSpec fileIndex fileSource json = case json of   JSON.Object object -> SubConfig <$> foldM     (\acc (key, subConfigValue) -> do       let msubConfigSpec = do@@ -43,7 +51,7 @@                 -- TODO: This should be an error given the config doesn't match spec                 fail "configuration spec and configuration value are different" -      value1 <- parseConfigValue msubConfigSpec fileIndex filepath subConfigValue+      value1 <- parseConfigValue msubConfigSpec fileIndex fileSource subConfigValue       return $ HashMap.insert key value1 acc     )     HashMap.empty@@ -57,7 +65,7 @@             _ -> fail "configuration spec and configuration value are different"          toValue = fromMaybe Plain mToValue-    in  return $ ConfigValue (Set.singleton $ File fileIndex filepath (toValue json))+    in  return $ ConfigValue (Set.singleton $ File fileIndex fileSource (toValue json))   eitherDecode :: ConfigFile -> Either String JSON.Value@@ -75,14 +83,15 @@ #endif  -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 (Just spec) fileIndex filepath) json of-    JSON.IError _ err    -> throwM $ InvalidConfiguration Nothing (Text.pack err)+parseConfig+  :: MonadThrow m => Spec.ConfigValue cmd -> Int -> FileSource -> ConfigFile -> m Config+parseConfig spec fileIndex fileSource contents = case eitherDecode contents of+  Left err -> throwM $ InvalidConfiguration Nothing (Text.pack err) -    JSON.ISuccess result -> return (Config result)+  Right json ->+    case JSON.iparse (parseConfigValue (Just spec) fileIndex fileSource) json of+      JSON.IError _ err    -> throwM $ InvalidConfiguration Nothing (Text.pack err)+      JSON.ISuccess result -> return (Config result)  readConfigFile :: MonadThrow m => Text -> IO (m ConfigFile) readConfigFile filepath =@@ -103,18 +112,19 @@                 (throwM $ InvalidConfiguration Nothing "Unsupported file extension")         else return $ throwM $ ConfigurationFileNotFound filepath -readConfigFromFiles :: Spec.ConfigSpec cmd -> IO (Config, [SomeException])-readConfigFromFiles spec =-  Spec.specConfigFilepaths spec+readConfigFromFileSources+  :: Spec.ConfigSpec cmd -> [FileSource] -> IO (Config, [SomeException])+readConfigFromFileSources spec fileSources =+  fileSources     & zip [1 ..]     & mapM-        (\(fileIndex, filepath) -> do-          mContents <- readConfigFile filepath+        (\(fileIndex, fileSource) -> do+          mContents <- readConfigFile (fileSourcePath fileSource)           return             (   mContents             >>= parseConfig (Spec.SubConfig $ Spec.specConfigValues spec)                             fileIndex-                            filepath+                            fileSource             )         )     & (foldl'@@ -125,6 +135,23 @@         (mempty, []) <$>       ) +processFilesSpec :: Spec.ConfigSpec cmd -> IO (Config, [SomeException])+processFilesSpec spec = case Spec.specConfigFilepaths spec of+  Nothing -> readConfigFromFileSources spec []+  Just (Spec.FilePathsSpec paths) ->+    readConfigFromFileSources spec (map FilePathSource paths)+  Just (Spec.FilesSpec fileEnvVar paths0) -> do+    let getPaths = case fileEnvVar of+          Nothing       -> return $ map FilePathSource paths0+          Just filePath -> do+            envFilePath <- lookupEnv (Text.unpack filePath)+            let envPath =+                  maybeToList ((EnvVarFileSource filePath . Text.pack) <$> envFilePath)+            return $ map FilePathSource paths0 ++ envPath++    paths <- getPaths+    readConfigFromFileSources spec paths+ {-|  Gathers configuration values from a list of files specified on the@@ -137,5 +164,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+  (config, exceptions) <- processFilesSpec spec   return (config, Vector.fromList exceptions)
src/System/Etc/Internal/Spec/Types.hs view
@@ -94,9 +94,14 @@   }   deriving (Show, Eq) +data FilesSpec+  = FilePathsSpec ![Text]+  | FilesSpec { fileLocationEnvVar :: !(Maybe Text), fileLocationPaths :: ![Text] }+  deriving (Show, Eq)+ data ConfigSpec cmd   = ConfigSpec {-    specConfigFilepaths :: ![Text]+    specConfigFilepaths :: !(Maybe FilesSpec)   , specCliProgramSpec  :: !(Maybe CliProgramSpec)   , specConfigValues    :: !(HashMap Text (ConfigValue cmd))   }@@ -258,12 +263,37 @@           , configSources = ConfigSources Nothing Nothing           } ++parseFiles :: JSON.Value -> JSON.Parser FilesSpec+parseFiles = JSON.withObject "FilesSpec" $ \object -> do+  files  <- object .: "etc/files"+  mEnv   <- files .:? "env"+  mPaths <- files .:? "paths"+  if isNothing mEnv && isNothing mPaths+    then fail "either `env` or a `paths` keys are required when using `etc/files`"+    else return $ FilesSpec mEnv (fromMaybe [] mPaths)++parseFilePaths :: JSON.Value -> JSON.Parser FilesSpec+parseFilePaths =+  JSON.withObject "FilesSpec" $ \object -> FilePathsSpec <$> object .: "etc/filepaths"++parseFileSpec :: JSON.Value -> JSON.Parser (Maybe FilesSpec)+parseFileSpec json@(JSON.Object object) = do+  mFiles     <- object .:? "etc/files"+  mFilePaths <- object .:? "etc/filepaths"+  if isJust (mFiles :: Maybe JSON.Value) && isJust (mFilePaths :: Maybe JSON.Value)+    then fail "either the `etc/files` or `etc/filepaths` key can be used; not both"+    else if isJust mFiles+      then Just <$> parseFiles json+      else if isJust mFilePaths then Just <$> parseFilePaths json else pure Nothing+parseFileSpec _ = pure Nothing+ instance JSON.FromJSON cmd => JSON.FromJSON (ConfigSpec cmd) where-  parseJSON json  =+  parseJSON json =     case json of       JSON.Object object ->         ConfigSpec-        <$> (fromMaybe [] <$> (object .:?  "etc/filepaths"))+        <$> parseFileSpec json         <*> (object .:? "etc/cli")         <*> (fromMaybe HashMap.empty <$> (object .:? "etc/entries"))       _ ->
src/System/Etc/Internal/Types.hs view
@@ -53,10 +53,15 @@ boolToValue :: Bool -> (a -> Value a) boolToValue = bool Plain Sensitive +data FileSource+  = FilePathSource { fileSourcePath :: !Text }+  | EnvVarFileSource { fileSourceEnvVar :: !Text,  fileSourcePath :: !Text }+  deriving (Show, Eq)+ data ConfigSource   = File {       configIndex :: !Int-    , filepath    :: !Text+    , filepath    :: !FileSource     , value       :: !(Value JSON.Value)     }   | Env {
test/System/Etc/Resolver/FileTest.hs view
@@ -16,12 +16,30 @@ import qualified Data.Aeson as JSON import           Paths_etc  (getDataFileName) -import System.Etc+import System.Environment (setEnv) +import System.Etc+import System.Etc.Internal.Types (FileSource (..))  tests :: TestTree tests = testGroup-  "file"+  "files"+  [ testCase "fail if using both `etc/files` and `etc/filepaths`" $ do+    let input = "{\"etc/files\": {}, \"etc/filepaths\": []}"++    (espec :: Either ConfigurationError (ConfigSpec ())) <- try $ parseConfigSpec input+    case espec of+      Left (InvalidConfiguration _ _) -> return ()+      _ ->+        assertFailure ("Expecting InvalidConfigurationError; got instead " <> show espec)+  , filePathsTests+  , filesTest+  ]+++filePathsTests :: TestTree+filePathsTests = testGroup+  "etc/filepaths"   [ testCase "supports json, yaml and yml extensions" $ do     jsonFilepath <- getDataFileName "test/fixtures/config.json" #ifdef WITH_YAML@@ -47,14 +65,14 @@         ("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)+        (Set.member (File 1 (FilePathSource $ Text.pack jsonFilepath) "hello json") aSet)  #ifdef WITH_YAML        >> assertBool ("expecting to see entry from yaml config file " <> show aSet)-                     (Set.member (File 2 (Text.pack jsonFilepath) "hello yaml") aSet)+                     (Set.member (File 2 (FilePathSource $ Text.pack jsonFilepath) "hello yaml") aSet)         >> assertBool ("expecting to see entry from yml config file " <> show aSet)-                     (Set.member (File 3 (Text.pack jsonFilepath) "hello yml") aSet)+                     (Set.member (File 3 (FilePathSource $ Text.pack jsonFilepath) "hello yml") aSet) #endif   , testCase "does not support any other file extension" $ do     fooFilepath <- getDataFileName "test/fixtures/config.foo"@@ -69,9 +87,11 @@     if Vector.null errs       then assertFailure "expecting one error, got none"       else-        let err = Vector.head errs+        let err = do+              e <- errs Vector.!? 0+              fromException e         in-          case fromException err of+          case err of             Just (InvalidConfiguration _ _) -> return ()             _                               -> assertFailure               ("Expecting InvalidConfigurationError; got instead " <> show err)@@ -92,14 +112,16 @@         ("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)+        (Set.member (File 1 (FilePathSource $ Text.pack jsonFilepath) "hello json") aSet)      if Vector.null errs       then assertFailure "expecting one error, got none"       else-        let err = Vector.head errs+        let err = do+              e <- errs Vector.!? 0+              fromException e         in-          case fromException err of+          case err of             Just (ConfigurationFileNotFound _) -> return ()             _ -> assertFailure               ("Expecting ConfigurationFileNotFound; got instead " <> show err)@@ -137,4 +159,50 @@                                    ("hola" :: Text)                                    greeting       Nothing -> assertFailure "Expecting config value, but got nothing"+  ]++filesTest :: TestTree+filesTest = testGroup+  "etc/files"+  [ testCase "at least the `env` or `paths` keys must be present" $ do+    let input = "{\"etc/files\": {}}"++    (espec :: Either ConfigurationError (ConfigSpec ())) <- try $ parseConfigSpec input+    case espec of+      Left (InvalidConfiguration _ _) -> return ()+      _ ->+        assertFailure ("Expecting InvalidConfigurationError; got instead " <> show espec)+  , testCase "environment variable has precedence over all others" $ do+    jsonFilepath <- getDataFileName "test/fixtures/config.json"+    envFilePath  <- getDataFileName "test/fixtures/config.env.json"+    let input = mconcat+          [ "{\"etc/files\": {"+          , "  \"env\": \"ENV_FILE_TEST\","+          , "  \"paths\": [\"" <> Text.pack jsonFilepath <> "\"]"+          , "}}"+          ]++        envFileTest = "ENV_FILE_TEST"++    setEnv (Text.unpack envFileTest) envFilePath++    (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 aSet -> do+        assertBool+          ("expecting to see entry from env config file " <> show aSet)+          (Set.member+            (File 1+                  (EnvVarFileSource envFileTest $ Text.pack envFilePath)+                  "hello environment"+            )+            aSet+          )+        assertBool+          ("expecting to see entry from json config file " <> show aSet)+          (Set.member (File 2 (FilePathSource $ Text.pack jsonFilepath) "hello json") aSet)   ]
test/System/Etc/SpecTest.hs view
@@ -260,13 +260,13 @@       let         keys  = ["greeting"]       path   <- getDataFileName "test/fixtures/config.spec.yaml"-      mconfig <- YAML.decodeFile path+      econfig <- YAML.decodeFileEither path -      case mconfig of-        Nothing ->-          assertFailure "yaml file did not have a configuration spec"+      case econfig of+        Left err ->+          assertFailure $ "yaml file did not have a configuration spec: " <> show err -        Just (config :: ConfigSpec ()) ->+        Right (config :: ConfigSpec ()) ->           case getConfigValue keys (specConfigValues config) of             Nothing ->               assertFailure (show keys ++ " should map to a config value, got sub config map instead")
+ test/fixtures/config.env.json view
@@ -0,0 +1,3 @@+{+    "greeting": "hello environment"+}