conferer 0.4.1.1 → 1.1.0.0
raw patch · 49 files changed
Files
- CHANGELOG.md +49/−0
- conferer.cabal +41/−15
- src/Conferer.hs +77/−102
- src/Conferer/Config.hs +37/−0
- src/Conferer/Config/Internal.hs +256/−0
- src/Conferer/Config/Internal/Types.hs +38/−0
- src/Conferer/Core.hs +0/−116
- src/Conferer/FromConfig.hs +43/−0
- src/Conferer/FromConfig/Basics.hs +0/−215
- src/Conferer/FromConfig/Internal.hs +486/−0
- src/Conferer/Key.hs +25/−0
- src/Conferer/Key/Internal.hs +103/−0
- src/Conferer/Source.hs +18/−0
- src/Conferer/Source/CLIArgs.hs +62/−29
- src/Conferer/Source/Env.hs +66/−49
- src/Conferer/Source/Files.hs +21/−17
- src/Conferer/Source/InMemory.hs +44/−0
- src/Conferer/Source/Internal.hs +35/−0
- src/Conferer/Source/Mapping.hs +0/−34
- src/Conferer/Source/Namespaced.hs +39/−19
- src/Conferer/Source/Null.hs +30/−16
- src/Conferer/Source/PropertiesFile.hs +61/−31
- src/Conferer/Source/Simple.hs +0/−34
- src/Conferer/Test.hs +24/−0
- src/Conferer/Types.hs +0/−118
- test/Conferer/ConfigSpec.hs +211/−0
- test/Conferer/FromConfig/BasicsSpec.hs +0/−88
- test/Conferer/FromConfig/BoolSpec.hs +26/−0
- test/Conferer/FromConfig/Extended.hs +148/−0
- test/Conferer/FromConfig/FileSpec.hs +81/−0
- test/Conferer/FromConfig/ListSpec.hs +171/−0
- test/Conferer/FromConfig/MaybeSpec.hs +33/−0
- test/Conferer/FromConfig/NumbersSpec.hs +29/−0
- test/Conferer/FromConfig/StringLikeSpec.hs +28/−0
- test/Conferer/FromConfigSpec.hs +14/−0
- test/Conferer/GenericsSpec.hs +55/−63
- test/Conferer/KeySpec.hs +39/−0
- test/Conferer/Source/ArgsSpec.hs +0/−38
- test/Conferer/Source/CLIArgsSpec.hs +42/−0
- test/Conferer/Source/EnvSpec.hs +44/−27
- test/Conferer/Source/InMemorySpec.hs +32/−0
- test/Conferer/Source/MappingSpec.hs +0/−43
- test/Conferer/Source/NamespacedSpec.hs +13/−9
- test/Conferer/Source/NullSpec.hs +3/−4
- test/Conferer/Source/PropertiesFileSpec.hs +15/−22
- test/Conferer/Source/SimpleSpec.hs +0/−22
- test/ConfererSpec.hs +37/−23
- test/ConfererSpecMain.hs +7/−0
- test/Spec.hs +1/−1
+ CHANGELOG.md view
@@ -0,0 +1,49 @@+# Changelog+All notable changes to this project will be documented in this file.++The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),+and this project adheres to [PVP](https://pvp.haskell.org/).++## [Unreleased]++Nothing++## [v1.1.0.0] - 2021-03-01++### Changed++* Rename `fromFilePath` to `fromFilePath'`.+* Define a new `fromFilePath` whose type is `FilePath -> SourceCreator` instaed of `FilePath -> IO Source`.+* (Internal) Remove the mismatched type exception since it's not actionable by the user+* (Internal) Use a list for default values so that many different defaults are available,+ possible point of extension in the future.+* Constraint valid key names to lowercase ascii and numbers (previously some non ascii characters were allowed)++### Added++* `isValidKeyFragment` and `isKeyCharacter` to validate `Key`s+* Added an overriding method based on type and key (details in the docs)++## [v1.0.0.1] - 2021-01-17++### Fixed++* In the `File`'s `FromConfig` instance, if the default is present and it's type+is `File`, it throws, which doesn't follow the rest of the library.++### Added++* Add `mkConfig'` which allows creating a config by passing a list of defaults and+a list of source creators.+* Add `addSources`, which allows to add several sources to a config.+* Define the type `Defaults`, which is a list of associations from `Key` to+`Dynamic`.++## [v1.0.0.0] - 2020-12-29++First release++[Unreleased]: https://github.com/ludat/conferer/compare/conferer_v1.1.0.0...HEAD+[v1.1.0.0]: https://github.com/ludat/conferer/releases/tag/conferer_v1.0.0.1...conferer_v1.1.0.0+[v1.0.0.1]: https://github.com/ludat/conferer/releases/tag/conferer_v1.0.0.0...conferer_v1.0.0.1+[v1.0.0.0]: https://github.com/ludat/conferer/releases/tag/v0.0.0.0...conferer_v1.0.0.0
conferer.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 41b921ef11b0981ed712b7e72a531f123613815eb7b3ebe4f9e0778a998118c6+-- hash: 80fdd520360a49e55b6dfe3ff9e148c716a7af0b6cf3d0326f8ed3d4a8f67611 name: conferer-version: 0.4.1.1+version: 1.1.0.0 synopsis: Configuration management library description: Library to abstract the parsing of many haskell config values from different config sources@@ -15,65 +15,91 @@ homepage: https://conferer.ludat.io author: Lucas David Traverso maintainer: lucas6246@gmail.com+copyright: (c) 2020 Lucas David Traverso license: MPL-2.0 license-file: LICENSE build-type: Simple extra-doc-files: README.md+ CHANGELOG.md LICENSE library exposed-modules: Conferer- Conferer.Core- Conferer.FromConfig.Basics+ Conferer.Config+ Conferer.Config.Internal+ Conferer.Config.Internal.Types+ Conferer.FromConfig+ Conferer.FromConfig.Internal+ Conferer.Key+ Conferer.Key.Internal+ Conferer.Source Conferer.Source.CLIArgs Conferer.Source.Env Conferer.Source.Files- Conferer.Source.Mapping+ Conferer.Source.InMemory+ Conferer.Source.Internal Conferer.Source.Namespaced Conferer.Source.Null Conferer.Source.PropertiesFile- Conferer.Source.Simple- Conferer.Types+ Conferer.Test other-modules: Paths_conferer hs-source-dirs: src- default-extensions: OverloadedStrings LambdaCase QuasiQuotes ScopedTypeVariables+ default-extensions: OverloadedStrings LambdaCase QuasiQuotes ScopedTypeVariables RecordWildCards StrictData+ ghc-options: -Wall -Wredundant-constraints -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns build-depends: base >=4.3 && <5 , bytestring >=0.10 && <0.11 , containers >=0.5 && <0.7 , directory >=1.2 && <2.0+ , filepath >=1.0 && <2.0 , text >=1.1 && <1.3+ if impl(ghc >= 8.4.1)+ ghc-options: -Wpartial-fields default-language: Haskell2010 test-suite specs type: exitcode-stdio-1.0- main-is: Spec.hs+ main-is: ConfererSpecMain.hs other-modules:- Conferer.FromConfig.BasicsSpec+ Conferer.ConfigSpec+ Conferer.FromConfig.BoolSpec+ Conferer.FromConfig.Extended+ Conferer.FromConfig.FileSpec+ Conferer.FromConfig.ListSpec+ Conferer.FromConfig.MaybeSpec+ Conferer.FromConfig.NumbersSpec+ Conferer.FromConfig.StringLikeSpec+ Conferer.FromConfigSpec Conferer.GenericsSpec- Conferer.Source.ArgsSpec+ Conferer.KeySpec+ Conferer.Source.CLIArgsSpec Conferer.Source.EnvSpec- Conferer.Source.MappingSpec+ Conferer.Source.InMemorySpec Conferer.Source.NamespacedSpec Conferer.Source.NullSpec Conferer.Source.PropertiesFileSpec- Conferer.Source.SimpleSpec ConfererSpec+ Spec Paths_conferer hs-source-dirs: test- default-extensions: OverloadedStrings LambdaCase QuasiQuotes ScopedTypeVariables+ default-extensions: OverloadedStrings LambdaCase QuasiQuotes ScopedTypeVariables RecordWildCards StrictData+ ghc-options: -Wall -Wredundant-constraints -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -main-is ConfererSpecMain build-depends:- base >=4.3 && <5+ QuickCheck+ , base >=4.3 && <5 , bytestring >=0.10 && <0.11 , conferer , containers >=0.5 && <0.7 , deepseq , directory >=1.2 && <2.0+ , filepath >=1.0 && <2.0 , hspec , text >=1.1 && <1.3+ if impl(ghc >= 8.4.1)+ ghc-options: -Wpartial-fields default-language: Haskell2010
src/Conferer.hs view
@@ -1,117 +1,92 @@-{-# LANGUAGE DeriveGeneric #-} -- |--- Module: Conferer--- Copyright: (c) 2019 Lucas David Traverso--- License: MIT--- Maintainer: Lucas David Traverso <lucas6246@gmail.com>--- Stability: experimental+-- Copyright: (c) 2019 Lucas David Traverso+-- License: MPL-2.0+-- Maintainer: Lucas David Traverso <lucas6246@gmail.com>+-- Stability: stable -- Portability: portable ----- Types and functions for managing configuration effectively+-- Public and stable API for the most basic usage of this library module Conferer ( - -- * How to use this library- -- | This is the most basic example: which uses the default configuration- -- to get a configuration for warp, which can be overriden via env vars,- -- command line arguments of @.properties@ files- --- -- @- -- import Conferer- -- import Conferer.FromConfig.Warp () -- from package conferer-warp- --- -- main = do- -- config <- 'defaultConfig' \"awesomeapp\"- -- warpSettings <- 'getFromConfig' \"warp\" config- -- runSettings warpSettings application- -- @- --- -- In the above example we see that we are getting a configuration value for- -- warp under the key warp, so for example to override it's default value- -- provided by warp the config keys for warp will always look like- -- @warp.something@, for example to override the port for warp (3000 by- -- default) we could call our program as @./my_program --warp.port=8000@.- --- -- There are two sides to conferer: Getting configuration for other libraries- -- like warp, hspec, snap, etc. and the way we choose to provide values like- -- json files, properties files, env vars, etc.+ -- * How to use this doc+ -- | This doc is mostly for reference, so you probably won't learn how to+ -- use conferer by reading it. For more detailed and guided documentation+ -- the best place is the webpage: <https://conferer.ludat.io/docs> - -- ** Getting configuration for existing libraries- -- | There is a typeclass 'FromConfig' that defines how to get a type- -- from a config, they are implemented in different packages since the- -- weight of the dependencies would be too high, the package is usually- -- named as @conferer-DEPENDENCY@ where DEPENDENCY is the name of the dependency (- -- for example: conferer-snap, conferer-warp, conferer-hspec), if you find- -- a library without a conferer port for its config you can create an issue- -- or maybe even create the library yourself!+ -- * Creating a Config+ mkConfig+ , mkConfig'+ -- * Getting values from a config+ -- | These functions allow you to get any type that implements 'FromConfig'+ , fetch+ , fetch'+ , fetchKey+ , fetchFromConfig+ , safeFetchKey+ , unsafeFetchKey+ , DefaultConfig(..)+ , FromConfig+ -- * Some useful types+ , Config+ , Key+ ) where - -- ** Providing key value pairs for configuration- -- | There is one important type in conferer: 'Config' from which, given a key- -- (eg: @warp@) you can get anything that implements 'FromConfig' (like- -- 'Warp.Settings')- --- -- Internally a 'Config' is made of many 'Source's which have a simpler- -- interface:- --- -- @- -- 'getKeyInSource' :: Source -> Key -> IO (Maybe Text)- -- @- --- -- Most configuration sources can be abstracted away as Map String String,- -- and they can use whatever logic they want to turn conferer keys (a list of- -- strings) into a place to look for a string, (for example the env source- -- requires a string to namespace the env vars that can affect the- -- configuration)- --- -- Once you have your 'Source' you can add it to a 'Config' using the- -- 'addSource' function. One final note: each source has a different- -- priority, which depends on when is was added to the config ('Source's- -- added later have lower priority) so the config searches keys in sources- -- in the same order they were added.+import Data.Text (Text)+import Data.Typeable (Typeable) - module Conferer.Types- , module Conferer.Core- , defaultConfig- , defaultConfigWithDefaults- , Key(..)+import Conferer.Config.Internal+import Conferer.Config.Internal.Types+import Conferer.FromConfig.Internal+import Conferer.Key+import qualified Conferer.Source.Env as Env+import qualified Conferer.Source.CLIArgs as Cli+import qualified Conferer.Source.PropertiesFile as PropertiesFile+import Conferer.Config (Defaults) - -- * Sources- , module Conferer.Source.Env- , module Conferer.Source.Simple- , module Conferer.Source.Namespaced- , module Conferer.Source.Mapping- , module Conferer.Source.CLIArgs- , module Conferer.Source.Null- , module Conferer.Source.PropertiesFile- -- * Re-Exports- , (&)- ) where+-- | Use the 'FromConfig' instance to get a value of type @a@ from the config+-- using some default fallback. The most common use for this is creating a custom+-- record and using this function to fetch it at initialization time.+--+-- This function throws only parsing exceptions when the values are present+-- but malformed somehow (@"abc"@ as an Int) but that depends on the 'FromConfig'+-- implementation for the type.+fetch :: forall a. (FromConfig a, Typeable a, DefaultConfig a) => Config -> IO a+fetch c = fetchFromRootConfigWithDefault c configDef -import Data.Text (Text)-import Data.Function ((&))+-- | Same as 'fetch' but it accepts the default as a parameter instead of using+-- the default from 'configDef'+fetch' :: forall a. (FromConfig a, Typeable a) => Config -> a -> IO a+fetch' = fetchFromRootConfigWithDefault -import Conferer.Core (emptyConfig, addSource, getFromConfig, getFromRootConfig, getFromConfigWithDefault, safeGetFromConfig, safeGetFromConfigWithDefault, getKey, unsafeGetKey, (/.), withDefaults)-import Conferer.Types (Config, Key(..), SourceCreator, Source(..), DefaultConfig(..), FromConfig(..))-import Conferer.Source.Env-import Conferer.Source.Simple-import Conferer.Source.Namespaced-import Conferer.Source.Mapping-import Conferer.Source.CLIArgs-import Conferer.Source.Null-import Conferer.Source.PropertiesFile+-- | Same as 'fetch'' but you can specify a 'Key' instead of the root key which allows+-- you to fetch smaller values when you need them instead of a big one at+-- initialization time.+fetchKey :: forall a. (FromConfig a, Typeable a) => Config -> Key -> a -> IO a+fetchKey = fetchFromConfigWithDefault --- | Default config which reads from command line arguments, env vars and--- property files-defaultConfig :: Text -> IO Config-defaultConfig appName =- defaultConfigWithDefaults appName []+-- | Same as 'fetchKey' but it returns a 'Nothing' when the value isn't present+safeFetchKey :: forall a. (FromConfig a, Typeable a) => Config -> Key -> IO (Maybe a)+safeFetchKey c k = fetchFromConfig k c --- | Default config which reads from command line arguments, env vars,--- property files and some default key/values-defaultConfigWithDefaults :: Text -> [(Key, Text)] -> IO Config-defaultConfigWithDefaults appName configMap =- pure (emptyConfig & withDefaults configMap)- >>= addSource (mkCLIArgsSource)- >>= addSource (mkEnvSource appName)- >>= addSource (mkPropertiesFileSource)+-- | Same as 'fetchKey' but it throws when the value isn't present.+unsafeFetchKey :: forall a. (FromConfig a, Typeable a) => Config -> Key -> IO a+unsafeFetchKey c k = fetchFromConfig k c ++-- | Create a 'Config' which reads from command line arguments, env vars and+-- property files that depend on the environment (@config/development.properties@)+-- by default+mkConfig :: Text -> IO Config+mkConfig appName =+ pure emptyConfig+ >>= addSource (Cli.fromConfig)+ >>= addSource (Env.fromConfig appName)+ >>= addSource (PropertiesFile.fromConfig "config.file")++-- | Create a 'Config' with the given defaults and source creators.+-- The sources will take precedence by the order they have in the list (earlier in+-- the list means it's tried first).+-- If the requested key is not found in any source it'll be looked up in the defaults.+mkConfig' :: Defaults -> [SourceCreator] -> IO Config+mkConfig' defaults sources = addSources sources . addDefaults defaults $ emptyConfig
+ src/Conferer/Config.hs view
@@ -0,0 +1,37 @@+-- |+-- Copyright: (c) 2019 Lucas David Traverso+-- License: MPL-2.0+-- Maintainer: Lucas David Traverso <lucas6246@gmail.com>+-- Stability: stable+-- Portability: portable+--+-- Public API providing Config functionality+module Conferer.Config+ (+ -- * Data+ Config+ -- * Querying a config+ , getKey+ , KeyLookupResult(..)+ , listSubkeys+ -- * Config creation and initialization+ , emptyConfig+ , addSource+ , addSources+ , addDefault+ , addDefaults+ , addKeyMappings+ , removeDefault+ , Defaults+ -- * Re-Exports+ , module Conferer.Key+ , (&)+ ) where++import Conferer.Config.Internal+import Conferer.Config.Internal.Types+import Conferer.Key+import Data.Function+import Data.Dynamic (Dynamic)++type Defaults = [(Key, Dynamic)]
+ src/Conferer/Config/Internal.hs view
@@ -0,0 +1,256 @@+{-# LANGUAGE TypeApplications #-}+-- |+-- Copyright: (c) 2019 Lucas David Traverso+-- License: MPL-2.0+-- Maintainer: Lucas David Traverso <lucas6246@gmail.com>+-- Stability: unstable+-- Portability: portable+--+-- Internal module providing Config functionality+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FlexibleContexts #-}+module Conferer.Config.Internal where++import Control.Monad (foldM, forM, msum)+import Data.Dynamic+import Data.List (sort, nub, union)+import Data.Text (Text)+import Data.Maybe (isJust)+import qualified Data.Map as Map++import Conferer.Key+import Conferer.Source.Internal+import Conferer.Config.Internal.Types++-- | This function runs lookups on the 'Config', first in 'Source's in order and+-- then on the 'Dynamic' based defaults.+getKey :: Key -> Config -> IO KeyLookupResult+getKey key config = do+ let possibleKeys = getKeysFromMappings (configKeyMappings config) key+ untilJust (fmap (\MappedKey{..} -> getRawKeyInSources mappedKey config) possibleKeys)+ >>= \case+ Just (k, textResult) ->+ return $ FoundInSources k textResult+ Nothing ->+ case msum $ fmap (\MappedKey{..} -> fmap (mappedKey,) $ (getKeyFromDefaults mappedKey config)) possibleKeys of+ Just (k, dynResult) -> return $ FoundInDefaults k dynResult+ Nothing -> return $ MissingKey [key]++-- | Alias for a mapping from one key to another used for transforming keys+type KeyMapping = (Key, Key)++-- | A key that has been transformed using one or many 'KeyMapping's, so that+-- that process can be reversed.+data MappedKey = MappedKey+ { mappingsChain :: [KeyMapping]+ , mappedKey :: Key+ } deriving (Show, Eq)+++-- | This function lists all available keys under some key, that could be fetched+-- successfully.+listSubkeys :: Key -> Config -> IO [Key]+listSubkeys originalKey Config{..} = do+ let mappedKeys = getKeysFromMappings configKeyMappings originalKey+ subkeysFromSources <- forM mappedKeys $ \MappedKey{..} -> do+ subkeysFromSources <- listRawSubkeysInSources mappedKey configSources+ let subkeysFromDefaults =+ filter (mappedKey `isKeyPrefixOf`) $ Map.keys configDefaults+ return $ fmap (MappedKey mappingsChain) $ subkeysFromSources ++ subkeysFromDefaults+ let subkeys = mconcat subkeysFromSources+ return $ sort $ nub $ fmap undoMappings subkeys++-- | This function lists subkeys in some 'Source's and combines the results+listRawSubkeysInSources :: Key -> [Source] -> IO [Key]+listRawSubkeysInSources mappedKey configSources = go mappedKey [] configSources+ where+ go :: Key -> [Key] -> [Source] -> IO [Key]+ go _ result [] = return result+ go k result (source:otherSources) = do+ subkeys <- getSubkeysInSource source k+ go k (result `union` subkeys) otherSources++-- | This function reverses the mappings in a 'MappedKey' to retrieve the+-- original key.+--+-- Assumes that mappings were really used, otherwise it ignores bad values+undoMappings :: MappedKey -> Key+undoMappings MappedKey{..} =+ go (reverse mappingsChain) mappedKey+ where+ go [] key = key+ go ((src, dest):others) key =+ case stripKeyPrefix dest key of+ Just k -> go others (src /. k)+ Nothing -> go others key++-- | This utility function run a list of IO actions and returns the+-- first that return a 'Just', if no one does, returns 'Nothing'+untilJust :: [IO (Maybe a)] -> IO (Maybe a)+untilJust actions = go actions+ where+ go [] = return Nothing+ go (action:rest) = do+ action+ >>= \case+ Just res -> return $ Just res+ Nothing -> go rest++-- | This function tries to apply a list of mappings to a key meaning+-- replace the prefix with the new value from the mapping, if the mapping+-- isn't a prefix that mapping is ignored+--+-- This function always terminates even in presence of recursive mappings,+-- since it removes the mapping after it was first used, and that causes that+-- eventually the function will run out of keymappings and terminate.+getKeysFromMappings :: [KeyMapping] -> Key -> [MappedKey]+getKeysFromMappings originalKeyMappings originalKey =+ go (MappedKey [] originalKey) originalKeyMappings+ where+ go :: MappedKey -> [KeyMapping] -> [MappedKey]+ go k [] = [k]+ go currKey keyMappings =+ nub $+ currKey :+ mconcat (+ fmap generateDerivedKeys $+ findAndSplitList tryMappingKey+ keyMappings)+ where+ tryMappingKey :: (Key, Key) -> Maybe MappedKey+ tryMappingKey (source, dest) =+ case stripKeyPrefix source (mappedKey currKey) of+ Just aKey ->+ Just $ MappedKey (mappingsChain currKey ++ [(source, dest)]) (dest /. aKey)+ Nothing -> Nothing+ generateDerivedKeys :: ([KeyMapping], MappedKey, [KeyMapping]) -> [MappedKey]+ generateDerivedKeys (prevMappings, aKey, nextMappings) =+ go aKey $ prevMappings ++ nextMappings++-- | This utility function splits a list based on a @cond@ function and returns a tuple+-- of previous value, next values and the mapped found value.+findAndSplitList :: forall a b. (a -> Maybe b) -> [a] -> [([a], b, [a])]+findAndSplitList cond list = go [] list+ where+ go :: [a] -> [a] -> [([a], b, [a])]+ go _ [] = []+ go prevElems (curElem:nextElems) =+ case cond curElem of+ Just res ->+ (prevElems, res, nextElems) : go (curElem:prevElems) nextElems+ Nothing ->+ go (curElem:prevElems) nextElems++-- | This function gets a value from 'Source's but ignores mappings and defaults+getRawKeyInSources :: Key -> Config -> IO (Maybe (Key, Text))+getRawKeyInSources k Config{..} =+ go configSources+ where+ go [] = return Nothing+ go (source:otherSources) = do+ res <- getKeyInSource source k+ case res of+ Just t -> return $ Just (k, t)+ Nothing -> go otherSources++-- | This function gets values from the defaults+getKeyFromDefaults :: Key -> Config -> Maybe [Dynamic]+getKeyFromDefaults key Config{..} =+ let+ possibleKeys = fmap mappedKey $ getKeysFromMappings configKeyMappings key+ in msum $ fmap (\k -> Map.lookup k configDefaults) possibleKeys++-- | The empty configuration, this 'Config' is used as the base for+-- most config creating functions.+emptyConfig :: Config+emptyConfig = Config+ { configSources = []+ , configDefaults = Map.empty+ , configKeyMappings = []+ }++-- | This function adds some key mappings to a 'Config'+addKeyMappings :: [KeyMapping] -> Config -> Config+addKeyMappings keyMappings config =+ config+ { configKeyMappings = configKeyMappings config ++ keyMappings+ }++-- | This function adds defaults to a 'Config'+addDefaults :: [(Key, Dynamic)] -> Config -> Config+addDefaults configMap config =+ let+ constructedMap =+ Map.fromListWith (++)+ . fmap (\(k,v) -> (k, [v]))+ $ configMap+ in config+ { configDefaults =+ Map.unionWith (++) constructedMap+ $ configDefaults config+ }++-- | This function removes a default from a 'Config', this is the+-- oposite of 'addDefault', it deletes the first element of+-- matching type in a certain 'Key'.+removeDefault :: forall t. Typeable t => Key -> Config -> Config+removeDefault key config =+ config+ { configDefaults =+ Map.update removeFirstDynamic key $ configDefaults config+ }+ where+ removeFirst :: (a -> Bool) -> [a] -> [a]+ removeFirst _ [] = []+ removeFirst condition (x:xs) =+ if condition x+ then xs+ else x : removeFirst condition xs++ removeFirstDynamic :: [Dynamic] -> Maybe [Dynamic]+ removeFirstDynamic dynamics =+ let result = removeFirst (isJust . fromDynamic @t) dynamics+ in if null result+ then Nothing + else Just result++-- | This function adds one default of a custom type to a 'Config'+--+-- Note that unlike 'addDefaults' this function does the toDyn so+-- no need to do it on the user's side+addDefault :: (Typeable a) => Key -> a -> Config -> Config+addDefault key value config =+ config+ { configDefaults = Map.insertWith (++) key [toDyn value] $ configDefaults config+ }++-- | Instantiate a 'Source' using an 'SourceCreator' and a 'Config' and add+-- to the config+addSource :: SourceCreator -> Config -> IO Config+addSource mkSource config = do+ newSource <- mkSource config+ return $+ config+ { configSources = configSources config ++ [ newSource ]+ }++-- | Instantiate several 'Source's using a 'SourceCreator's and a 'Config' and add+-- them to the config in the order defined by the list+addSources :: [SourceCreator] -> Config -> IO Config+addSources sources config = foldM (flip addSource) config sources++-- orElse :: IO KeyLookupResult -> IO KeyLookupResult -> IO KeyLookupResult+-- orElse getKey1 getKey2 = do+-- result1 <- getKey1+-- case result1 of+-- MissingKey _ -> getKey2+-- FoundInSources _ _ -> return result1+-- FoundInDefaults _ _ -> do+-- result2 <- getKey2+-- case result2 of+-- MissingKey _ -> return result1+-- FoundInSources _ _ -> return result2+-- FoundInDefaults _ _ -> return result1
+ src/Conferer/Config/Internal/Types.hs view
@@ -0,0 +1,38 @@+-- |+-- Copyright: (c) 2019 Lucas David Traverso+-- License: MPL-2.0+-- Maintainer: Lucas David Traverso <lucas6246@gmail.com>+-- Stability: unstable+-- Portability: portable+--+-- Core types for Config (here because of depedency cycles)+module Conferer.Config.Internal.Types where++import Data.Map (Map)+import Conferer.Key (Key)+import Data.Dynamic ( Dynamic )+import Conferer.Source.Internal (Source)+import Data.Text (Text)++-- | This type acts as the entry point for most of the library, it's main purpouse+-- is to expose a uniform interface into multiple configuration sources (such as+-- env vars, cli args, and many others including use defined ones using the+-- 'Source' interface)+data Config =+ Config+ { configSources :: [Source]+ , configDefaults :: Map Key [Dynamic]+ , configKeyMappings :: [(Key, Key)]+ } deriving (Show)++-- | Result of a key lookup in a 'Config'+data KeyLookupResult+ = MissingKey [Key]+ | FoundInSources Key Text+ | FoundInDefaults Key [Dynamic]+ deriving (Show)++-- | The type for creating a source given a 'Config', some sources require a+-- certain configuration to be initialized (for example: the redis source+-- needs connection info to connect to the server)+type SourceCreator = Config -> IO Source
− src/Conferer/Core.hs
@@ -1,116 +0,0 @@-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE FlexibleContexts #-}-module Conferer.Core where--import Data.Text (Text)-import qualified Data.Text as Text-import Data.Map (Map)-import qualified Data.Map as Map-import Data.Maybe (fromMaybe)-import Data.Typeable (Typeable, Proxy(..), typeRep)-import Control.Exception (try, throw, throwIO, evaluate)--import Conferer.Source.Simple-import Conferer.Types---- | Most Basic function to interact directly with a 'Config'. It always returns--- 'Text' in the case of success and implements the logic to traverse--- sources inside the 'Config'.-getKey :: Key -> Config -> IO (Maybe Text)-getKey k config =- go $ sources config ++ [mkPureMapSource (defaults config)]- where- go [] = return Nothing- go (source:sources) = do- res <- getKeyInSource source k- case res of- Just t -> return $ Just t- Nothing -> go sources----- | Fetch a value from a config under some specific key that's parsed using the 'FromConfig'--- instance, and as a default it uses the value from 'DefaultConfig'.------ Notes:--- - This function may throw an exception if parsing fails for any subkey-getFromConfig :: forall a. (Typeable a, FromConfig a, DefaultConfig a) => Key -> Config -> IO a-getFromConfig key config =- getFromConfigWithDefault key config configDef---- | Same as 'getFromConfig' using the root key------ Notes:--- - This function may throw an exception if parsing fails for any subkey-getFromRootConfig :: forall a. (Typeable a, FromConfig a, DefaultConfig a) => Config -> IO a-getFromRootConfig config =- getFromConfig "" config----- | Same as 'getFromConfig' but with a user defined default (instead of 'DefaultConfig' instance)------ Useful for fetching primitive types-getFromConfigWithDefault :: forall a. (Typeable a, FromConfig a) => Key -> Config -> a -> IO a-getFromConfigWithDefault key config configDefault =- safeGetFromConfigWithDefault key config configDefault- >>= \case- Just value -> do- evaluate value- Nothing ->- throwIO $ FailedToFetchError key (typeRep (Proxy :: Proxy a))---- | Fetch a value from a config key that's parsed using the FromConfig instance.------ Note: This function does not use default so the value must be fully defined by the config only,--- meaning using this function for many records will always result in 'Nothing' (if the record contains--- a value that can never be retrieved like a function)-safeGetFromConfig :: forall a. (Typeable a, FromConfig a, DefaultConfig a) => Key -> Config -> IO (Maybe a)-safeGetFromConfig key config =- safeGetFromConfigWithDefault key config configDef---- | Same as 'safeGetFromConfig' but with a user defined default-safeGetFromConfigWithDefault :: forall a. (Typeable a, FromConfig a) => Key -> Config -> a -> IO (Maybe a)-safeGetFromConfigWithDefault key config configDefault = do- totalValue <- evaluate =<< fetchFromConfig key config- case totalValue of- Just value -> do- Just <$> evaluate value- Nothing -> do- result :: Either FailedToFetchError a <- try . (evaluate =<<) . updateFromConfig key config $ configDefault- case result of- Right a -> Just <$> evaluate a- Left e -> return Nothing---- | Create a new 'Key' by concatenating two existing keys.-(/.) :: Key -> Key -> Key-parent /. child = Path (unKey parent ++ unKey child)---- | The empty configuration, this 'Config' is used as the base for--- most config creating functions.-emptyConfig :: Config-emptyConfig = Config [] Map.empty--withDefaults :: [(Key, Text)] -> Config -> Config-withDefaults configMap config =- config { defaults = Map.fromList configMap }---- | Instantiate a 'SourceCreator' using the 'emptyConfig'-mkStandaloneSource :: SourceCreator -> IO Source-mkStandaloneSource mkSource =- mkSource emptyConfig----- | Instantiate a 'Source' using an 'SourceCretor' and a 'Config' and add--- to the config-addSource :: SourceCreator -> Config -> IO Config-addSource mkSource config = do- newSource <- mkSource config- return $- config- { sources = sources config ++ [ newSource ]- }---- | Same as 'getKey' but it throws if the 'Key' isn't found-unsafeGetKey :: Key -> Config -> IO Text-unsafeGetKey key config =- fromMaybe (throw $ FailedToFetchError key (typeRep (Proxy :: Proxy Text)))- <$> getKey key config
+ src/Conferer/FromConfig.hs view
@@ -0,0 +1,43 @@+-- |+-- Copyright: (c) 2019 Lucas David Traverso+-- License: MPL-2.0+-- Maintainer: Lucas David Traverso <lucas6246@gmail.com>+-- Stability: stable+-- Portability: portable+--+-- Public API module providing FromConfig functionality+module Conferer.FromConfig + ( FromConfig(fromConfig)+ , fetchFromConfig+ , DefaultConfig(configDef)+ , fetchFromConfigWithDefault+ , fetchFromRootConfig+ , fetchFromRootConfigWithDefault++ , fetchFromConfigByIsString+ , fetchFromConfigByRead+ , fetchFromConfigWith+ , addDefaultsAfterDeconstructingToDefaults++ , MissingRequiredKey+ , throwMissingRequiredKey+ , missingRequiredKey++ , ConfigParsingError+ , throwConfigParsingError+ , configParsingError++ , Key+ , (/.)+ , File(..)+ , KeyLookupResult(..)+ , OverrideFromConfig(..)+ , overrideFetch++ , fetchFromDefaults+ , fetchRequiredFromDefaults+ ) where++import Conferer.FromConfig.Internal+import Conferer.Config.Internal.Types+import Conferer.Key
− src/Conferer/FromConfig/Basics.hs
@@ -1,215 +0,0 @@-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeOperators #-}-{-# LANGUAGE ScopedTypeVariables #-}--module Conferer.FromConfig.Basics where--import Control.Monad (join)-import Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Text.Encoding as Text-import Data.ByteString (ByteString)-import Data.Maybe (fromMaybe)-import Control.Exception-import Data.Char (toLower)-import Data.Typeable (Typeable, typeRep, Proxy(..))-import Data.String (IsString, fromString)-import Text.Read (readMaybe)-import GHC.Generics--import Conferer.Types-import Conferer.Core (getKey, (/.), getFromConfig)--updateAllAtOnceUsingFetch :: forall a. (FromConfig a, Typeable a) => Key -> Config -> a -> IO a-updateAllAtOnceUsingFetch key config old = do- fetchFromConfig key config- >>= \case- Just new -> do- evaluate new- Nothing -> do- evaluate old--instance FromConfig () where- updateFromConfig key config _ = do- return ()- fetchFromConfig key config = do- return $ Just ()--instance DefaultConfig () where- configDef = ()--instance FromConfig Int where- updateFromConfig = updateAllAtOnceUsingFetch- fetchFromConfig = fetchFromConfigByRead--instance FromConfig Integer where- updateFromConfig = updateAllAtOnceUsingFetch- fetchFromConfig = fetchFromConfigByRead--instance FromConfig Float where- updateFromConfig = updateAllAtOnceUsingFetch- fetchFromConfig = fetchFromConfigByRead--instance FromConfig ByteString where- updateFromConfig = updateAllAtOnceUsingFetch- fetchFromConfig = fetchFromConfigWith (Just . Text.encodeUtf8)--instance DefaultConfig (Maybe a) where- configDef = Nothing-instance (FromConfig a) => FromConfig (Maybe a) where- updateFromConfig k config (Just a) = do- res <- updateFromConfig k config a- Just <$> evaluate res- updateFromConfig k config Nothing = do- fetchFromConfig k config- fetchFromConfig k config = do- fetchFromConfig @a k config- >>= \case- Just res -> Just <$> Just <$> evaluate res- Nothing -> return $ Just Nothing---instance FromConfig String where- updateFromConfig = updateAllAtOnceUsingFetch- fetchFromConfig = fetchFromConfigWith (Just . Text.unpack)--instance FromConfig Text where- updateFromConfig = updateAllAtOnceUsingFetch- fetchFromConfig = fetchFromConfigWith Just---instance FromConfig Bool where- updateFromConfig = updateAllAtOnceUsingFetch- fetchFromConfig = fetchFromConfigWith parseBool--parseBool text =- case Text.toLower text of- "false" -> Just False- "true" -> Just True- _ -> Nothing--updateFromConfigByRead :: (Typeable a, Read a) => Key -> Config -> a -> IO (a)-updateFromConfigByRead = updateFromConfigWith (readMaybe . Text.unpack)--updateFromConfigByIsString :: (Typeable a, IsString a) => Key -> Config -> a -> IO (a)-updateFromConfigByIsString = updateFromConfigWith (Just . fromString . Text.unpack)--fetchFromConfigByRead :: (Typeable a, Read a) => Key -> Config -> IO (Maybe a)-fetchFromConfigByRead = fetchFromConfigWith (readMaybe . Text.unpack)--fetchFromConfigByIsString :: (Typeable a, IsString a) => Key -> Config -> IO (Maybe a)-fetchFromConfigByIsString = fetchFromConfigWith (Just . fromString . Text.unpack)--fromValueWith :: (Text -> Maybe a) -> Text -> Maybe a-fromValueWith parseValue valueAsText = parseValue valueAsText--fetchFromConfigWith :: forall a. Typeable a => (Text -> Maybe a) -> Key -> Config -> IO (Maybe a)-fetchFromConfigWith parseValue key config = do- getKey key config >>=- \case- Just value ->- return $ Just $- fromMaybe (throw $ ConfigParsingError key value (typeRep (Proxy :: Proxy a))) $- fromValueWith parseValue value- Nothing ->- return Nothing--updateFromConfigWith :: forall a. Typeable a => (Text -> Maybe a) -> Key -> Config -> a -> IO a-updateFromConfigWith parseValue key config a = do- getKey key config >>=- \case- Just value ->- return $- fromMaybe (throw $ ConfigParsingError key value (typeRep (Proxy :: Proxy a))) $- fromValueWith parseValue value- Nothing -> return a---- | Concatenate many transformations to the config based on keys and functions-findKeyAndApplyConfig ::- forall newvalue config.- FromConfig newvalue- => Config -- ^ Complete config- -> Key -- ^ Key that indicates the part of the config that we care about- -> Key -- ^ Key that we use to find the config (usually concatenating with the- -- other key)- -> (config -> newvalue) -- ^ Function that knows how to use the- -- value to update the config- -> (newvalue -> config -> config) -- ^ Function that knows how to use the- -- value to update the config- -> config -- ^ Result of the last config updating- -> IO config -- ^ Updated config-findKeyAndApplyConfig config k relativeKey get set customConfig = do- newValue <- updateFromConfig @newvalue (k /. relativeKey) config (get customConfig)- return $ set newValue customConfig--instance FromConfigG inner =>- FromConfigG (D1 metadata inner) where- updateFromConfigG key config (M1 inner) =- M1 <$> updateFromConfigG key config inner- fetchFromConfigG key config =- fmap M1 <$> fetchFromConfigG key config--instance (FromConfigWithConNameG inner, Constructor constructor) =>- FromConfigG (C1 constructor inner) where- updateFromConfigG key config (M1 inner) =- M1 <$> updateFromConfigWithConNameG @inner (conName @constructor undefined) key config inner- fetchFromConfigG key config =- fmap M1 <$> fetchFromConfigWithConNameG @inner (conName @constructor undefined) key config--class FromConfigWithConNameG f where- updateFromConfigWithConNameG :: String -> Key -> Config -> f a -> IO (f a)- fetchFromConfigWithConNameG :: String -> Key -> Config -> IO (Maybe (f a))--instance (FromConfigWithConNameG left, FromConfigWithConNameG right) =>- FromConfigWithConNameG (left :*: right) where- updateFromConfigWithConNameG s key config (left :*: right) = do- leftValue <- updateFromConfigWithConNameG @left s key config left- rightValue <- updateFromConfigWithConNameG @right s key config right- return (leftValue :*: rightValue)-- fetchFromConfigWithConNameG s key config = do- leftValue <- fetchFromConfigWithConNameG @left s key config- rightValue <- fetchFromConfigWithConNameG @right s key config- case (leftValue, rightValue) of- (Just l, Just r) -> return $ Just (l :*: r)- _ -> return Nothing--instance (FromConfigG inner, Selector selector) =>- FromConfigWithConNameG (S1 selector inner) where- updateFromConfigWithConNameG s key config (M1 inner) =- let- applyFirst :: (Char -> Char) -> Text -> Text- applyFirst f t = case Text.uncons t of- Just (c, ts) -> Text.cons (f c) ts- Nothing -> t-- fieldName = Text.pack $ selName @selector undefined- prefix = applyFirst toLower $ Text.pack s- scopedKey =- case Text.stripPrefix prefix fieldName of- Just stripped -> applyFirst toLower stripped- Nothing -> fieldName- in M1 <$> updateFromConfigG @inner (key /. Path [scopedKey]) config inner-- fetchFromConfigWithConNameG s key config =- let- applyFirst :: (Char -> Char) -> Text -> Text- applyFirst f t = case Text.uncons t of- Just (c, ts) -> Text.cons (f c) ts- Nothing -> t-- fieldName = Text.pack $ selName @selector undefined- prefix = applyFirst toLower $ Text.pack s- scopedKey =- case Text.stripPrefix prefix fieldName of- Just stripped -> applyFirst toLower stripped- Nothing -> fieldName- in fmap M1 <$> fetchFromConfigG @inner (key /. Path [scopedKey]) config---- | Purely 'Generics' machinery, ignore...-instance (FromConfig inner) => FromConfigG (Rec0 inner) where- updateFromConfigG key config (K1 inner) = do- K1 <$> updateFromConfig @inner key config inner- fetchFromConfigG key config = do- fmap K1 <$> fetchFromConfig @inner key config
+ src/Conferer/FromConfig/Internal.hs view
@@ -0,0 +1,486 @@+-- |+-- Copyright: (c) 2019 Lucas David Traverso+-- License: MPL-2.0+-- Maintainer: Lucas David Traverso <lucas6246@gmail.com>+-- Stability: unstable+-- Portability: portable+--+-- Internal module providing FromConfig functionality+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE AllowAmbiguousTypes #-}++module Conferer.FromConfig.Internal where++import Data.Text (Text)+import qualified Data.Text as Text+import qualified Data.Text.Encoding as Text+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as LBS+import Control.Exception+import Data.Typeable+import Text.Read (readMaybe)+import Data.Dynamic+import GHC.Generics+import Data.Function ((&))++import Conferer.Key+import Conferer.Config.Internal.Types+import Conferer.Config.Internal+import qualified Data.Char as Char+import Control.Monad (forM)+import Data.Maybe (fromMaybe, mapMaybe)+import qualified System.FilePath as FilePath+import Data.List (nub, foldl', sort)+import Data.String (IsString(..))+import Data.Foldable (msum)++-- | The typeclass for defining the way to get values from a 'Config', hiding the+-- 'Text' based nature of the 'Conferer.Source.Source's and parse whatever value+-- as the types sees fit+--+-- Some of these instances are provided in different packages to avoid the heavy+-- dependencies.+--+-- It provides a reasonable default using 'Generic's so most of the time user need+-- not to implement this typeclass.+class FromConfig a where+ -- | This function uses a 'Config' and a scoping 'Key' to get a value.+ --+ -- Some conventions:+ --+ -- * When some 'Key' is missing this function should throw 'MissingRequiredKey'+ --+ -- * For any t it should hold that @fetchFromConfig k (config & addDefault k t) == t@+ -- meaning that a default on the same key with the right type should be used as a+ -- default and with no configuration that value should be returned+ --+ -- * Try desconstructing the value in as many keys as possible since is allows easier+ -- partial overriding.+ fromConfig :: Key -> Config -> IO a+ default fromConfig :: (Typeable a, Generic a, IntoDefaultsG (Rep a), FromConfigG (Rep a)) => Key -> Config -> IO a+ fromConfig key config = do+ let configWithDefaults = case fetchFromDefaults @a key config of+ Just d -> config & addDefaults (intoDefaultsG key $ from d)+ Nothing -> config+ to <$> fromConfigG key configWithDefaults++fetchFromConfig :: forall a. (FromConfig a, Typeable a) => Key -> Config -> IO a+fetchFromConfig key config =+ case fetchFromDefaults @(OverrideFromConfig a) key config of+ Just (OverrideFromConfig fetch) ->+ fetch key $ removeDefault @(OverrideFromConfig a) key config+ Nothing ->+ fromConfig key config++-- | Utility only typeclass to smooth the naming differences between default values for+-- external library settings+--+-- This typeclass is not used internally it's only here for convinience for users+class DefaultConfig a where+ configDef :: a++instance {-# OVERLAPPABLE #-} Typeable a => FromConfig a where+ fromConfig = fetchRequiredFromDefaults++instance FromConfig () where+ fromConfig _key _config = return ()++instance FromConfig String where+ fromConfig = fetchFromConfigWith (Just . Text.unpack)++instance {-# OVERLAPPABLE #-} (Typeable a, FromConfig a) =>+ FromConfig [a] where+ fromConfig key config = do+ keysForItems <- getSubkeysForItems+ case keysForItems of+ Nothing -> do+ fetchRequiredFromDefaults @[a] key config+ Just subkeys -> do+ let configWithDefaults :: Config =+ case fetchFromDefaults @[a] key config of+ Just defaults ->+ foldl' (\c (index, value) ->+ c & addDefault (key /. "defaults" /. mkKey (show index)) value) config+ $ zip [0 :: Integer ..] defaults+ Nothing -> config+ forM subkeys $ \k -> do+ fetchFromConfig @a (key /. k)+ (if isKeyPrefixOf (key /. "defaults") (key /. k)+ then+ configWithDefaults+ else+ configWithDefaults & addKeyMappings [(key /. k, key /. "prototype")])+ where+ getSubkeysForItems :: IO (Maybe [Key])+ getSubkeysForItems = do+ fetchFromConfig @(Maybe Text) (key /. "keys") config+ >>= \case+ Just rawKeys -> do+ return $+ Just $+ nub $+ filter (/= "") $+ mkKey .+ Text.unpack <$>+ Text.split (== ',') rawKeys+ Nothing -> do+ subelements <-+ sort+ . nub+ . filter (not . (`elem` ["prototype", "keys", "defaults"]))+ . mapMaybe (\k -> case rawKeyComponents <$> stripKeyPrefix key k of+ Just (subkey:_) -> Just $ fromText subkey+ _ -> Nothing)+ <$> listSubkeys key config+ return $ if null subelements then Nothing else Just subelements++instance FromConfig Int where+ fromConfig = fetchFromConfigByRead++instance FromConfig Integer where+ fromConfig = fetchFromConfigByRead++instance FromConfig Float where+ fromConfig = fetchFromConfigByRead++instance FromConfig BS.ByteString where+ fromConfig = fetchFromConfigWith (Just . Text.encodeUtf8)++instance FromConfig LBS.ByteString where+ fromConfig = fetchFromConfigWith (Just . LBS.fromStrict . Text.encodeUtf8)++instance forall a. (Typeable a, FromConfig a) => FromConfig (Maybe a) where+ fromConfig key config = do+ let+ configWithUnwrappedDefault =+ case fetchFromDefaults @(Maybe a) key config of+ Just (Just defaultThing) -> do+ config & addDefault key defaultThing+ _ -> do+ config+ (Just <$> fetchFromConfig @a key configWithUnwrappedDefault)+ `catch` (\(_e :: MissingRequiredKey) -> return Nothing)++instance FromConfig Text where+ fromConfig = fetchFromConfigWith Just++instance FromConfig Bool where+ fromConfig = fetchFromConfigWith parseBool++-- | A newtype wrapper for a 'FilePath' to allow implementing 'FromConfig'+-- with something better than just a 'String'+newtype File =+ File FilePath+ deriving (Show, Eq, Ord, Read)++unFile :: File -> FilePath+unFile (File f) = f++instance IsString File where+ fromString s = File s++instance FromConfig File where+ fromConfig key config = do+ let defaultPath = fetchFromDefaults @File key config++ filepath <- fetchFromConfig @(Maybe String) key config++ extension <- fetchFromConfig @(Maybe String) (key /. "extension") config+ dirname <- fetchFromConfig @(Maybe String) (key /. "dirname") config+ basename <- fetchFromConfig @(Maybe String) (key /. "basename") config+ filename <- fetchFromConfig @(Maybe String) (key /. "filename") config++ let+ constructedFilePath =+ applyIfPresent FilePath.replaceDirectory dirname+ $ applyIfPresent FilePath.replaceBaseName basename+ $ applyIfPresent FilePath.replaceExtension extension+ $ applyIfPresent FilePath.replaceFileName filename+ $ fromMaybe (unFile $ fromMaybe "" defaultPath) filepath+ if FilePath.isValid constructedFilePath+ then return $ File constructedFilePath+ else throwMissingRequiredKeys @String+ [ key+ , key /. "extension"+ , key /. "dirname"+ , key /. "basename"+ , key /. "filename"+ ]+ where+ applyIfPresent f maybeComponent =+ (\fp -> maybe fp (f fp) maybeComponent)++-- | Helper function to parse a 'Bool' from 'Text'+parseBool :: Text -> Maybe Bool+parseBool text =+ case Text.toLower text of+ "false" -> Just False+ "true" -> Just True+ _ -> Nothing++data OverrideFromConfig a =+ OverrideFromConfig (Key -> Config -> IO a)++-- | Allow the programmer to override this 'FromConfig' instance by providing+-- a special 'OverrideFromConfig' value.+--+-- To avoid infinite recursion we remove the Override before calling the value++-- | Helper function to implement fetchFromConfig using the 'Read' instance+fetchFromConfigByRead :: (Typeable a, Read a) => Key -> Config -> IO a+fetchFromConfigByRead = fetchFromConfigWith (readMaybe . Text.unpack)++-- | Helper function to implement fetchFromConfig using the 'IsString' instance+fetchFromConfigByIsString :: (Typeable a, IsString a) => Key -> Config -> IO a+fetchFromConfigByIsString = fetchFromConfigWith (Just . fromString . Text.unpack)++-- | Helper function to implement fetchFromConfig using some parsing function+fetchFromConfigWith :: forall a. Typeable a => (Text -> Maybe a) -> Key -> Config -> IO a+fetchFromConfigWith parseValue key config = do+ getKey key config >>=+ \case+ MissingKey k -> do+ throwMissingRequiredKeys @a k++ FoundInSources k value ->+ case parseValue value of+ Just a -> do+ return a+ Nothing -> do+ throwConfigParsingError @a k value++ FoundInDefaults k dynamics ->+ case fromDynamics dynamics of+ Just a -> do+ return a+ Nothing -> do+ throwMissingRequiredKeys @a [k]++fromDynamics :: forall a. Typeable a => [Dynamic] -> Maybe a+fromDynamics =+ msum . fmap (fromDynamic @a)++-- | Helper function does the plumbing of desconstructing a default into smaller+-- defaults, which is usefull for nested 'fetchFromConfig'.+addDefaultsAfterDeconstructingToDefaults+ :: forall a.+ Typeable a =>+ -- | Function to deconstruct the value+ (a -> [(Key, Dynamic)]) ->+ -- | Key where to look for the value+ Key ->+ -- | The config+ Config ->+ IO Config+addDefaultsAfterDeconstructingToDefaults destructureValue key config = do+ case fetchFromDefaults @a key config of+ Just value -> do+ let newDefaults =+ ((\(k, d) -> (key /. k, d)) <$> destructureValue value)+ return $+ addDefaults newDefaults config+ Nothing -> do+ return config++-- | Helper function to override the fetching function for a certain key.+--+-- This function creates a 'Dynamic' that when added to the defaults allows+-- overriding the default 'FromConfig' instance.+overrideFetch :: forall a. Typeable a => (Key -> Config -> IO a) -> Dynamic+overrideFetch f =+ toDyn @(OverrideFromConfig a) $ OverrideFromConfig f++-- | Exception to show that a value couldn't be parsed properly+data ConfigParsingError =+ ConfigParsingError Key Text TypeRep+ deriving (Typeable, Eq)++instance Exception ConfigParsingError+instance Show ConfigParsingError where+ show (ConfigParsingError key value aTypeRep) =+ concat+ [ "Couldn't parse value '"+ , Text.unpack value+ , "' from key '"+ , show key+ , "' as "+ , show aTypeRep+ ]++-- | Helper function to throw 'ConfigParsingError'+throwConfigParsingError :: forall a b. (Typeable a) => Key -> Text -> IO b+throwConfigParsingError key text =+ throwIO $ configParsingError @a key text++-- | Helper function to create a 'ConfigParsingError'+configParsingError :: forall a. (Typeable a) => Key -> Text -> ConfigParsingError+configParsingError key text =+ ConfigParsingError key text $ typeRep (Proxy :: Proxy a)++-- | Exception to show that some non optional 'Key' was missing while trying+-- to 'fetchFromConfig'+data MissingRequiredKey =+ MissingRequiredKey [Key] TypeRep+ deriving (Typeable, Eq)++instance Exception MissingRequiredKey+instance Show MissingRequiredKey where+ show (MissingRequiredKey keys aTypeRep) =+ concat+ [ "Failed to get a '"+ , show aTypeRep+ , "' from keys: "+ , Text.unpack+ $ Text.intercalate ", "+ $ fmap (Text.pack . show)+ $ keys++ ]++-- | Simplified helper function to throw a 'MissingRequiredKey'+throwMissingRequiredKey :: forall t a. Typeable t => Key -> IO a+throwMissingRequiredKey key =+ throwMissingRequiredKeys @t [key]++-- | Simplified helper function to create a 'MissingRequiredKey'+missingRequiredKey :: forall t. Typeable t => Key -> MissingRequiredKey+missingRequiredKey key =+ missingRequiredKeys @t [key]++-- | Helper function to throw a 'MissingRequiredKey'+throwMissingRequiredKeys :: forall t a. Typeable t => [Key] -> IO a+throwMissingRequiredKeys keys =+ throwIO $ missingRequiredKeys @t keys++-- | Helper function to create a 'MissingRequiredKey'+missingRequiredKeys :: forall a. (Typeable a) => [Key] -> MissingRequiredKey+missingRequiredKeys keys =+ MissingRequiredKey keys (typeRep (Proxy :: Proxy a))++-- | Fetch from value from the defaults map of a 'Config' or else throw+fetchRequiredFromDefaults :: forall a. (Typeable a) => Key -> Config -> IO a+fetchRequiredFromDefaults key config =+ case fetchFromDefaults key config of+ Nothing -> do+ throwMissingRequiredKey @a key+ Just a ->+ return a++-- | Fetch from value from the defaults map of a 'Config' or else return a 'Nothing'+fetchFromDefaults :: forall a. (Typeable a) => Key -> Config -> Maybe a+fetchFromDefaults key config =+ getKeyFromDefaults key config+ >>= fromDynamics @a++-- | Same as 'fetchFromConfig' using the root key+fetchFromRootConfig :: forall a. (FromConfig a, Typeable a) => Config -> IO a+fetchFromRootConfig =+ fetchFromConfig ""++-- | Same as 'fetchFromConfig' but adding a user defined default before 'fetchFromConfig'ing+-- so it doesn't throw a MissingKeyError+fetchFromConfigWithDefault :: forall a. (Typeable a, FromConfig a) => Config -> Key -> a -> IO a+fetchFromConfigWithDefault config key configDefault =+ fetchFromConfig key (config & addDefault key configDefault)++-- | Same as 'fetchFromConfigWithDefault' using the root key+fetchFromRootConfigWithDefault :: forall a. (Typeable a, FromConfig a) => Config -> a -> IO a+fetchFromRootConfigWithDefault config configDefault =+ fetchFromRootConfig (config & addDefault "" configDefault)++-- | Purely 'Generic's machinery, ignore...+class FromConfigG f where+ fromConfigG :: Key -> Config -> IO (f a)++instance FromConfigG inner =>+ FromConfigG (D1 metadata inner) where+ fromConfigG key config = do+ M1 <$> fromConfigG key config++instance (FromConfigWithConNameG inner, Constructor constructor) =>+ FromConfigG (C1 constructor inner) where+ fromConfigG key config =+ M1 <$> fromConfigWithConNameG @inner (conName @constructor undefined) key config++-- | Purely 'Generic's machinery, ignore...+class FromConfigWithConNameG f where+ fromConfigWithConNameG :: String -> Key -> Config -> IO (f a)++instance (FromConfigWithConNameG left, FromConfigWithConNameG right) =>+ FromConfigWithConNameG (left :*: right) where+ fromConfigWithConNameG s key config = do+ leftValue <- fromConfigWithConNameG @left s key config+ rightValue <- fromConfigWithConNameG @right s key config+ return (leftValue :*: rightValue)++instance (FromConfigG inner, Selector selector) =>+ FromConfigWithConNameG (S1 selector inner) where+ fromConfigWithConNameG s key config =+ let+ applyFirst :: (Char -> Char) -> Text -> Text+ applyFirst f t = case Text.uncons t of+ Just (c, ts) -> Text.cons (f c) ts+ Nothing -> t++ fieldName = Text.pack $ selName @selector undefined+ prefix = applyFirst Char.toLower $ Text.pack s+ scopedKey =+ case Text.stripPrefix prefix fieldName of+ Just stripped -> applyFirst Char.toLower stripped+ Nothing -> fieldName+ in M1 <$> fromConfigG @inner (key /. fromText scopedKey) config++instance (FromConfig inner, Typeable inner) => FromConfigG (Rec0 inner) where+ fromConfigG key config = do+ K1 <$> fetchFromConfig @inner key config++-- | Purely 'Generic's machinery, ignore...+class IntoDefaultsG f where+ intoDefaultsG :: Key -> f a -> [(Key, Dynamic)]++instance IntoDefaultsG inner =>+ IntoDefaultsG (D1 metadata inner) where+ intoDefaultsG key (M1 inner) =+ intoDefaultsG key inner++instance (IntoDefaultsWithConNameG inner, Constructor constructor) =>+ IntoDefaultsG (C1 constructor inner) where+ intoDefaultsG key (M1 inner) =+ intoDefaultsWithConNameG @inner (conName @constructor undefined) key inner++-- | Purely 'Generic's machinery, ignore...+class IntoDefaultsWithConNameG f where+ intoDefaultsWithConNameG :: String -> Key -> f a -> [(Key, Dynamic)]++instance (IntoDefaultsWithConNameG left, IntoDefaultsWithConNameG right) =>+ IntoDefaultsWithConNameG (left :*: right) where+ intoDefaultsWithConNameG s key (left :*: right) = do+ intoDefaultsWithConNameG @left s key left+ +++ intoDefaultsWithConNameG @right s key right++instance (IntoDefaultsG inner, Selector selector) =>+ IntoDefaultsWithConNameG (S1 selector inner) where+ intoDefaultsWithConNameG s key (M1 inner) =+ let+ applyFirst :: (Char -> Char) -> Text -> Text+ applyFirst f t = case Text.uncons t of+ Just (c, ts) -> Text.cons (f c) ts+ Nothing -> t++ fieldName = Text.pack $ selName @selector undefined+ prefix = applyFirst Char.toLower $ Text.pack s+ scopedKey =+ case Text.stripPrefix prefix fieldName of+ Just stripped -> applyFirst Char.toLower stripped+ Nothing -> fieldName+ in intoDefaultsG @inner (key /. fromText scopedKey) inner++instance (Typeable inner) => IntoDefaultsG (Rec0 inner) where+ intoDefaultsG key (K1 inner) = do+ [(key, toDyn inner)]
+ src/Conferer/Key.hs view
@@ -0,0 +1,25 @@+-- |+-- Copyright: (c) 2019 Lucas David Traverso+-- License: MPL-2.0+-- Maintainer: Lucas David Traverso <lucas6246@gmail.com>+-- Stability: stable+-- Portability: portable+--+-- Public API for Key related features+module Conferer.Key+ ( Key+ -- * Creating 'Key's+ , mkKey+ , fromText+ -- * Maniputaing 'Key's+ , (/.)+ , stripKeyPrefix+ , isKeyPrefixOf+ , isValidKeyFragment+ , isKeyCharacter+ -- * Introspecting 'Key's+ , rawKeyComponents+ , unconsKey+ ) where++import Conferer.Key.Internal
+ src/Conferer/Key/Internal.hs view
@@ -0,0 +1,103 @@+-- |+-- Copyright: (c) 2019 Lucas David Traverso+-- License: MPL-2.0+-- Maintainer: Lucas David Traverso <lucas6246@gmail.com>+-- Stability: unstable+-- Portability: portable+--+-- Internal module for Key related features+module Conferer.Key.Internal where++import Data.String (IsString(..))+import Data.Text (Text)+import qualified Data.Text as Text+import Data.Function (on)+import Data.List (stripPrefix, isPrefixOf)+import qualified Data.Char as Char+-- | This type is used extensivelly as a way to point into a 'Conferer.Source.Source'+-- and in turn into a 'Conferer.Config.Config'. The intended way to create them is+-- is using 'mkKey'.+--+-- It's a list of alphanumeric words and each 'Conferer.Source.Source' can interpret+-- it as it sees fit.+newtype Key = Key+ { unKey :: [Text]+ } deriving (Eq, Ord)++instance Show Key where+ show key = "\"" ++ Text.unpack (keyName key) ++ "\""++instance IsString Key where+ fromString = mkKey++-- | Helper function to create 'Key's, this function always works, but+-- since 'Key's reject some string this function transforms the input+-- to provide lawful 'Key's instead of throwing.+--+-- For example:+--+-- > 'mkKey' "sOmE.KEy" == "some.key"+-- > 'mkKey' "1.key" == "1.key"+-- > 'mkKey' "1_thing.key" == "1thing.key"+-- > 'mkKey' "some....key" == "some.key"+-- > 'mkKey' ".." == ""+mkKey :: String -> Key+mkKey s =+ Key .+ filter (/= mempty) .+ fmap (Text.filter isKeyCharacter . Text.toLower) .+ Text.split (== '.') .+ fromString+ $ s++-- | Same as 'mkKey' but for 'Text'+fromText :: Text -> Key+fromText = mkKey . Text.unpack++-- | Collapse a key into a textual representation+keyName :: Key -> Text+keyName = Text.intercalate "." . unKey++-- | This function tells if a key is a subkey of another key based+-- using key fragments instead of letters as units+--+-- > 'isKeyPrefixOf' "foo" "foo.bar" == True+-- > 'isKeyPrefixOf' "foo" "foo" == True+-- > 'isKeyPrefixOf' "foo" "fooa" == False+isKeyPrefixOf :: Key -> Key -> Bool+isKeyPrefixOf = isPrefixOf `on` unKey++-- | Given k1 and k2 this function drops k1 as a prefix from k2, if+-- k1 is not a prefix of k2 it returns 'Nothing'+--+-- > 'keyPrefixOf' "foo" "foo.bar" == Just "bar"+-- > 'keyPrefixOf' "foo" "foo" == Just ""+-- > 'keyPrefixOf' "foo" "fooa" == Nothing+-- > 'keyPrefixOf' "" k == Just k+stripKeyPrefix :: Key -> Key -> Maybe Key+stripKeyPrefix key1 key2 = Key <$> (stripPrefix `on` unKey) key1 key2++-- | Concatenate two keys+(/.) :: Key -> Key -> Key+parent /. child = Key (unKey parent ++ unKey child)++-- | Get raw components from a key, usually to do some manipulation+rawKeyComponents :: Key -> [Text]+rawKeyComponents = unKey++-- | Get first component of a key and the rest of the key+unconsKey :: Key -> Maybe (Text, Key)+unconsKey (Key []) = Nothing+unconsKey (Key (k:ks)) = Just (k, Key ks)++-- | Check if a 'Text' is a valid fragment of a 'Key', meaning+-- that each character 'isKeyCharacter'+isValidKeyFragment :: Text -> Bool+isValidKeyFragment t =+ (not $ Text.null t) && Text.all isKeyCharacter t++-- | Checks if the given 'Char' is a valid for a 'Key'.+-- Meaning it is a lower case ascii letter or a number.+isKeyCharacter :: Char -> Bool+isKeyCharacter c =+ Char.isAsciiLower c || Char.isDigit c
+ src/Conferer/Source.hs view
@@ -0,0 +1,18 @@+-- |+-- Copyright: (c) 2019 Lucas David Traverso+-- License: MPL-2.0+-- Maintainer: Lucas David Traverso <lucas6246@gmail.com>+-- Stability: stable+-- Portability: portable+--+-- Public API module for Source related features+module Conferer.Source+ ( Source(..)+ , IsSource(..)+ , SourceCreator+ , module Conferer.Key+ ) where++import Conferer.Key+import Conferer.Config.Internal.Types+import Conferer.Source.Internal
src/Conferer/Source/CLIArgs.hs view
@@ -1,43 +1,76 @@-module Conferer.Source.CLIArgs- (- -- * Command line arguments Source- -- | This source provides keys from the command line arguments passed into- -- the program. It only accepts arguments with @--@ and an equals, for- -- example: @./awesomeapp --warp.port=5000@- mkCLIArgsSource- , mkCLIArgsSource'- , parseArgsIntoKeyValue- )-where+-- |+-- Copyright: (c) 2019 Lucas David Traverso+-- License: MPL-2.0+-- Maintainer: Lucas David Traverso <lucas6246@gmail.com>+-- Stability: stable+-- Portability: portable+--+-- Command line arguments based source+module Conferer.Source.CLIArgs where -import Data.Text (Text)+import Data.Text (Text) import qualified Data.Text as Text-import Data.Maybe (mapMaybe)-import Data.String (fromString)-import System.Environment (getArgs)+import Data.Maybe (mapMaybe)+import System.Environment (getArgs) -import Conferer.Types-import Conferer.Source.Simple+import Conferer.Source+import qualified Conferer.Source.InMemory as InMemory --- | Create a 'SourceCreator' for CLIArgs from a argument list-mkCLIArgsSource' :: [String] -> SourceCreator-mkCLIArgsSource' args = \config -> do- let configMap = parseArgsIntoKeyValue args- mkMapSource configMap config+-- | This source provides keys from the command line arguments passed into+-- the program. It only accepts arguments with @--@ and an equals, for+-- example: @./awesomeapp --warp.port=5000@+data CLIArgsSource =+ CLIArgsSource+ { originalCliArgs :: RawCLIArgs+ , innerSource :: Source+ } deriving (Show) --- | Same as 'mkCLIArgsSource'' but using 'getArgs' to provide the argument--- list-mkCLIArgsSource :: SourceCreator-mkCLIArgsSource = \config -> do+-- | Type alias for cli args+type RawCLIArgs = [String]++instance IsSource CLIArgsSource where+ getKeyInSource CLIArgsSource{..} key = do+ getKeyInSource innerSource key+ getSubkeysInSource CLIArgsSource{..} key = do+ getSubkeysInSource innerSource key+++-- | Create a 'SourceCreator' using 'fromEnv'+fromConfig :: SourceCreator+fromConfig = \_config ->+ fromEnv++-- | Create a 'Source' using 'fromArgs' but using real cli args+fromEnv :: IO Source+fromEnv = do args <- getArgs- mkCLIArgsSource' args config+ return $ fromArgs args +-- | Create a 'Source' using cli args passed as parameter+fromArgs :: RawCLIArgs -> Source+fromArgs originalCliArgs =+ let configMap = parseArgsIntoKeyValue originalCliArgs+ innerSource = InMemory.fromAssociations configMap+ in Source $ CLIArgsSource {..}+ -- | Parse an argument list into a dictionary suitable for a 'Source' parseArgsIntoKeyValue :: [String] -> [(Key, Text)] parseArgsIntoKeyValue =- fmap (\(k, s) -> (fromString $ Text.unpack k, s)) .- fmap (\s -> fmap (Text.drop 1) $ Text.breakOn "=" s).+ fmap (\s ->+ let (k, rawValue) = Text.breakOn "=" s+ in case Text.uncons rawValue of+ Nothing -> (fromText k, "true")+ (Just ('=', v)) -> (fromText k, v)+ -- Since rawValue comes from breaking on "=" the first character+ -- should always be '=' or none at all+ (Just (_, _)) ->+ error $ unlines+ [ "'"+ , Text.unpack rawValue+ , "' should always start with '='"+ ]+ ) . mapMaybe (Text.stripPrefix "--") . takeWhile (/= "--") . fmap Text.pack
src/Conferer/Source/Env.hs view
@@ -1,67 +1,84 @@-module Conferer.Source.Env- (--- * Env Source--- | This 'Source' provides config values from env vars given a prefix that's--- used to avoid colliding with different system configuration------ For example if you use the 'Prefix' "awesomeapp" and get the 'Key'--- "warp.port" this source will try to lookup the env var called--- @AWESOMEAPP_WARP_PORT@.---- * Usage--- | To use this source simply choose a prefix and add it using the--- 'addSource' function like:+-- |+-- Copyright: (c) 2019 Lucas David Traverso+-- License: MPL-2.0+-- Maintainer: Lucas David Traverso <lucas6246@gmail.com>+-- Stability: stable+-- Portability: portable ----- @--- config & 'addSource' ('mkEnvSource' "awesomeapp")--- @- mkEnvSource- , mkEnvSource'- , Prefix- , LookupEnvFunc- , keyToEnvVar- ) where-+-- Environment based source+{-# LANGUAGE RecordWildCards #-}+module Conferer.Source.Env where -import Data.Text (Text)+import Data.Text (Text) import qualified Data.Text as Text import qualified System.Environment as System+import Data.Maybe (mapMaybe) -import Conferer.Types+import Conferer.Source+import qualified Conferer.Source.InMemory as InMemory --- | 'SourceCreator' for env 'Source' that uses the real 'System.lookupEnv'--- function-mkEnvSource :: Prefix -> SourceCreator-mkEnvSource prefix =- mkEnvSource' System.lookupEnv prefix+-- | Source that interfaces with the environment transforming keys+-- by uppercasing and interspersing underscores, and using a prefix+-- to avoid clashing with system env vars+--+-- so with "app" prefix, @"some.key"@ turns into @APP_SOME_KEY@+data EnvSource =+ EnvSource+ { environment :: RawEnvironment+ , keyPrefix :: Prefix+ , innerSource :: Source+ } deriving (Show) --- | 'SourceCreator' for env 'Source' that allows parameterizing the--- function used to lookup for testing-mkEnvSource' :: LookupEnvFunc -> Prefix -> SourceCreator-mkEnvSource' lookupEnv prefix = \_config ->- return $- Source- { getKeyInSource = \k -> do- let envVarName = Text.unpack $ keyToEnvVar prefix k- fmap Text.pack <$> lookupEnv envVarName- }+-- | Type alias for the environment+type RawEnvironment = [(String, String)] --- | Type alias for the function to lookup env vars-type LookupEnvFunc = String -> IO (Maybe String)+-- | Type alias for the env vars prefix+type Prefix = Text +instance IsSource EnvSource where+ getKeyInSource EnvSource{..} key = do+ getKeyInSource innerSource key+ getSubkeysInSource EnvSource{..} key = do+ getSubkeysInSource innerSource key --- | A text to namespace env vars-type Prefix = Text+-- | Create a 'SourceCreator' using 'fromEnv'+fromConfig :: Prefix -> SourceCreator+fromConfig prefix _config = do+ fromEnv prefix +-- | Create a 'Source' using the real environment+fromEnv :: Prefix -> IO Source+fromEnv prefix = do+ rawEnvironment <- System.getEnvironment+ return $ fromEnvList rawEnvironment prefix++-- | Create a 'Source' using a hardcoded list of env vars+fromEnvList :: RawEnvironment -> Prefix -> Source+fromEnvList environment keyPrefix =+ let+ mappings =+ mapMaybe (\(key, v) -> do+ k <- envVarToKey keyPrefix $ Text.pack key+ return (k, Text.pack v)+ )+ environment+ innerSource = InMemory.fromAssociations mappings+ in Source EnvSource {..}+ -- | Get the env name from a prefix and a key by uppercasing and -- intercalating underscores------ >>> keyToEnVar "awesomeapp" "warp.port"--- "AWESOMEAPP_WARP_PORT" keyToEnvVar :: Prefix -> Key -> Text-keyToEnvVar prefix (Path keys) =+keyToEnvVar prefix keys = Text.toUpper $ Text.intercalate "_" $ filter (/= mempty)- $ prefix : keys+ $ prefix : rawKeyComponents keys++-- | The opossite of 'keyToEnvVar'+envVarToKey :: Prefix -> Text -> Maybe Key+envVarToKey prefix envVar =+ let+ splitEnvVar = fromText $ Text.replace "_" "." envVar+ in+ stripKeyPrefix (fromText prefix) splitEnvVar
src/Conferer/Source/Files.hs view
@@ -1,21 +1,25 @@+-- |+-- Copyright: (c) 2019 Lucas David Traverso+-- License: MPL-2.0+-- Maintainer: Lucas David Traverso <lucas6246@gmail.com>+-- Stability: stable+-- Portability: portable+--+-- Public API module providing some helper functions related to files+{-# LANGUAGE TypeApplications #-} module Conferer.Source.Files where -import qualified Data.Text as Text-import Data.Maybe (fromMaybe)--import Conferer.Types-import Conferer.FromConfig.Basics ()+import Data.Maybe (fromMaybe)+import System.FilePath -fromRight :: a -> Either e a -> a-fromRight a (Left _) = a-fromRight _ (Right a) = a+import Conferer.Config+import Conferer.FromConfig -getFilePathFromEnv :: Config -> String -> IO FilePath-getFilePathFromEnv config extension = do- env <- updateFromConfig "env" config "development"- return $ mconcat- [ "config/"- , Text.unpack env- , "."- , extension- ]+-- | Helper function to get a file from the config specifying the extension and using+-- current env+getFilePathFromEnv :: Key -> String -> Config -> IO FilePath+getFilePathFromEnv key extension config = do+ env <- fromMaybe "development" <$> fetchFromConfig @(Maybe String) "env" config+ let defaultPath = "config" </> env <.> extension+ File filepath <- fetchFromConfigWithDefault @File config key $ File defaultPath+ return filepath
+ src/Conferer/Source/InMemory.hs view
@@ -0,0 +1,44 @@+-- |+-- Copyright: (c) 2019 Lucas David Traverso+-- License: MPL-2.0+-- Maintainer: Lucas David Traverso <lucas6246@gmail.com>+-- Stability: stable+-- Portability: portable+--+-- In memory source mostly used for testing+{-# LANGUAGE RecordWildCards #-}+module Conferer.Source.InMemory where++import Data.Map (Map)+import qualified Data.Map as Map+import Data.Text (Text)++import Conferer.Source++-- | A 'Source' mostly use for mocking which is configured directly using a+-- 'Map'+newtype InMemorySource =+ InMemorySource+ { configMap :: Map Key Text+ } deriving (Show, Eq)++instance IsSource InMemorySource where+ getKeyInSource InMemorySource {..} key =+ return $ Map.lookup key configMap+ getSubkeysInSource InMemorySource {..} key = do+ return $ filter (\k -> key `isKeyPrefixOf` k && key /= k) $ Map.keys configMap++-- | Create a 'SourceCreator' from a list of associations+fromConfig :: [(Key, Text)] -> SourceCreator+fromConfig configMap _config =+ return $ fromAssociations configMap++-- | Create a 'Source' from a 'Map'+fromMap :: Map Key Text -> Source+fromMap configMap =+ Source . InMemorySource $ configMap++-- | Create a 'Source' from a list of associations+fromAssociations :: [(Key, Text)] -> Source+fromAssociations =+ fromMap . Map.fromList
+ src/Conferer/Source/Internal.hs view
@@ -0,0 +1,35 @@+-- |+-- Copyright: (c) 2019 Lucas David Traverso+-- License: MPL-2.0+-- Maintainer: Lucas David Traverso <lucas6246@gmail.com>+-- Stability: unstable+-- Portability: portable+--+-- Internal module for Key related features+{-# LANGUAGE ExistentialQuantification #-}+module Conferer.Source.Internal where++import Data.Text (Text)+import Conferer.Key (Key)++-- | Concrete type for 'IsSource'+data Source = forall s. (IsSource s, Show s) => Source s++instance Show Source where+ show (Source s) = "Source " ++ show s++-- | Main interface for interacting with external systems that provide configuration+-- which will be used by 'Conferer.FromConfig.FromConfig' to fetch values.+class IsSource s where+ -- | This function is used by the 'Conferer.Config.Config' to get values from this+ -- 'Source'.+ getKeyInSource :: s -> Key -> IO (Maybe Text)+ -- | This function is used by the 'Conferer.Config.Config' to list possible values+ -- from the 'Source' that if the user 'getKeyInSource', it will be found.+ getSubkeysInSource :: s -> Key -> IO [Key]++instance IsSource Source where+ getKeyInSource (Source source) =+ getKeyInSource source+ getSubkeysInSource (Source source) =+ getSubkeysInSource source
− src/Conferer/Source/Mapping.hs
@@ -1,34 +0,0 @@-module Conferer.Source.Mapping- (- -- * Namespaced higher-order source- -- | This source takes a source and returns a new source that- -- always transforms the key according to either a function for- -- 'mkMappingSource'' or a 'Map' for 'mkMappingSource'- mkMappingSource- , mkMappingSource'- )-where--import Data.Map (Map)-import qualified Data.Map as Map--import Conferer.Types---- | Create a 'SourceCreator' using a function to transform the supplied keys--- and another 'SourceCreator'-mkMappingSource' :: (Key -> Maybe Key) -> SourceCreator -> SourceCreator-mkMappingSource' mapper sourceCreator config = do- configSource <- sourceCreator config-- return $ Source- { getKeyInSource = \k -> do- case mapper k of- Just newKey -> getKeyInSource configSource newKey- Nothing -> return Nothing- }---- | Create a 'SourceCreator' using a 'Map' 'Key' 'Key' to transform the supplied keys--- and another 'SourceCreator'-mkMappingSource :: Map Key Key -> SourceCreator -> SourceCreator-mkMappingSource configMap configSource =- mkMappingSource' (`Map.lookup` configMap) configSource
src/Conferer/Source/Namespaced.hs view
@@ -1,23 +1,43 @@-module Conferer.Source.Namespaced- (- -- * Namespaced higher-order source- -- | This source takes a source and returns a new source that- -- always checks that the 'Key' given always starts with certain 'Key'- -- and then strips that prefix before consulting its inner Source- mkNamespacedSource- ) where+-- |+-- Copyright: (c) 2019 Lucas David Traverso+-- License: MPL-2.0+-- Maintainer: Lucas David Traverso <lucas6246@gmail.com>+-- Stability: stable+-- Portability: portable+--+-- Source that namespaces an inner source+{-# LANGUAGE RecordWildCards #-}+module Conferer.Source.Namespaced where -import Data.List (stripPrefix)+import Conferer.Source -import Conferer.Types+-- | This source takes a source and returns a new source that+-- always checks that the 'Key' given always starts with certain 'Key'+-- and then strips that prefix before consulting its inner Source+data NamespacedSource =+ NamespacedSource+ { scopeKey :: Key+ , innerSource :: Source+ } deriving (Show) +instance IsSource NamespacedSource where+ getKeyInSource NamespacedSource{..} key = do+ case stripKeyPrefix scopeKey key of+ Just innerKey -> getKeyInSource innerSource innerKey+ Nothing -> return Nothing+ getSubkeysInSource NamespacedSource{..} key = do+ case stripKeyPrefix scopeKey key of+ Just innerKey -> do+ fmap (scopeKey /.) <$> getSubkeysInSource innerSource innerKey+ Nothing -> return []+ -- | Create a 'SourceCreator' from a prefix and another 'SourceCreator'-mkNamespacedSource :: Key -> SourceCreator -> SourceCreator-mkNamespacedSource (Path key) configCreator = \config -> do- configSource <- configCreator config- return $ Source- { getKeyInSource = \(Path k) -> do- case stripPrefix key k of- Just newKey -> getKeyInSource configSource (Path newKey)- Nothing -> return Nothing- }+fromConfig :: Key -> SourceCreator -> SourceCreator+fromConfig scopeKey configCreator = \config -> do+ innerSource <- configCreator config+ return $ fromInner scopeKey innerSource++-- | Create a 'Source' from a prefix and another 'Source'+fromInner :: Key -> Source -> Source+fromInner scopeKey innerSource = do+ Source $ NamespacedSource{..}
src/Conferer/Source/Null.hs view
@@ -1,18 +1,32 @@-module Conferer.Source.Null- (- -- * Does Nothing Source- -- | The stub source that never has a key- mkNullSource- )-where+-- |+-- Copyright: (c) 2019 Lucas David Traverso+-- License: MPL-2.0+-- Maintainer: Lucas David Traverso <lucas6246@gmail.com>+-- Stability: stable+-- Portability: portable+--+-- Do nothing source+module Conferer.Source.Null where -import Conferer.Types+import Conferer.Source --- | Create a null 'Source'-mkNullSource :: SourceCreator-mkNullSource _config =- return $ Source- { getKeyInSource =- \_k -> do- return Nothing- }+-- | Stub source that has no keys+data NullSource =+ NullSource+ deriving (Show, Eq)++instance IsSource NullSource where+ getKeyInSource _source _key =+ return Nothing+ getSubkeysInSource _source _key =+ return []++-- | Create 'SourceCreator'+fromConfig :: SourceCreator+fromConfig _config =+ return empty++-- | Create a 'Source'+empty :: Source+empty =+ Source $ NullSource
src/Conferer/Source/PropertiesFile.hs view
@@ -1,44 +1,74 @@+-- |+-- Copyright: (c) 2019 Lucas David Traverso+-- License: MPL-2.0+-- Maintainer: Lucas David Traverso <lucas6246@gmail.com>+-- Stability: stable+-- Portability: portable+--+-- Properties file source module Conferer.Source.PropertiesFile where -import Data.Text (Text)-import Data.Function ((&))-import System.Directory (doesFileExist)-import Data.Maybe (catMaybes)+import Data.Text (Text)+import Data.Function ((&))+import System.Directory (doesFileExist)+import Data.Maybe (catMaybes) import qualified Data.Text as Text import qualified Data.Text.IO as Text -import Conferer.Types-import Conferer.Source.Files-import Conferer.Source.Null-import Conferer.Source.Simple+import Conferer.Source+import Conferer.Source.Files+import qualified Conferer.Source.Null as Null+import qualified Conferer.Source.InMemory as InMemory --- | 'SourceCreator' for properties file 'Source' that read from a--- config file in @config/{env}.properties@ and parses it as a properties--- file with @some.key=a value@ lines-mkPropertiesFileSource :: SourceCreator-mkPropertiesFileSource = \config -> do- filePath <- getFilePathFromEnv config "properties"+-- | 'Source' that uses a config file in @config/{env}.properties@ and+-- parses it as a properties file with @some.key=a value@ lines+data PropertiesFileSource =+ PropertiesFileSource+ { originalFilePath :: FilePath+ , innerSource :: Source+ } deriving (Show)++instance IsSource PropertiesFileSource where+ getKeyInSource PropertiesFileSource{..} key = do+ getKeyInSource innerSource key+ getSubkeysInSource PropertiesFileSource{..} key = do+ getSubkeysInSource innerSource key++-- | Create a 'SourceCreator' using 'getFilePathFromEnv' to get the path to file+-- and 'fromFilePath'+fromConfig :: Key -> SourceCreator+fromConfig key config = do+ filePath <- getFilePathFromEnv key "properties" config+ fromFilePath' filePath++-- | Create a 'SourceCreator' reading the file and using that as a properties file, but+-- if the file doesn't exist do nothing.+fromFilePath :: FilePath -> SourceCreator+fromFilePath filepath _config =+ fromFilePath' filepath++-- | Create a 'Source' reading the file and using that as a properties file, but+-- if the file doesn't exist do nothing.+fromFilePath' :: FilePath -> IO Source+fromFilePath' filePath = do fileExists <- doesFileExist filePath if fileExists then do fileContent <- Text.readFile filePath- mkPropertiesFileSource' fileContent config- else do- mkNullSource config----- | 'SourceCreator' for properties file 'Source' that only parses a--- given 'Text' as a properties file-mkPropertiesFileSource' :: Text -> SourceCreator-mkPropertiesFileSource' fileContent =- \config -> do- let keyValues =- fileContent- & Text.lines- & fmap lineToKeyValue- & catMaybes- mkMapSource keyValues config+ return $ fromFileContent filePath fileContent+ else+ return Null.empty +-- | Create a 'Source' using some content as a properties file+fromFileContent :: FilePath -> Text -> Source+fromFileContent originalFilePath fileContent =+ let keyValues =+ fileContent+ & Text.lines+ & fmap lineToKeyValue+ & catMaybes+ innerSource = InMemory.fromAssociations keyValues+ in Source $ PropertiesFileSource {..} -- | Transform a line into a key/value pair (or not) lineToKeyValue :: Text -> Maybe (Key, Text)@@ -47,7 +77,7 @@ & (\(rawKey, rawValue) -> case Text.stripPrefix "=" rawValue of Just value ->- Just (Path $ Text.splitOn "." rawKey, value)+ Just (fromText rawKey, value) Nothing -> Nothing )
− src/Conferer/Source/Simple.hs
@@ -1,34 +0,0 @@-module Conferer.Source.Simple- (- -- * Simple Source- -- | This source provides values from a hardcoded Map passed at creation- -- time that can not be changed afterwards, it's mostly used as a necessary- -- utility- mkMapSource- , mkMapSource'- , mkPureMapSource- ) where--import Data.Map (Map)-import qualified Data.Map as Map-import Data.Text (Text)--import Conferer.Types---- | Make a 'SourceCreator' from a 'Map'-mkMapSource' :: Map Key Text -> SourceCreator-mkMapSource' configMap _config =- return $ mkPureMapSource configMap---- | Make a 'Source' from a 'Map'-mkPureMapSource :: Map Key Text -> Source-mkPureMapSource configMap =- Source- { getKeyInSource =- \k -> do- return $ Map.lookup k configMap- }---- | Make a 'Source' from 'List' of 'Key', 'Text' pairs-mkMapSource :: [(Key, Text)] -> SourceCreator-mkMapSource = mkMapSource' . Map.fromList
+ src/Conferer/Test.hs view
@@ -0,0 +1,24 @@+-- |+-- Copyright: (c) 2019 Lucas David Traverso+-- License: MPL-2.0+-- Maintainer: Lucas David Traverso <lucas6246@gmail.com>+-- Stability: stable+-- Portability: portable+--+-- Some testing utilities+module Conferer.Test+ ( configWith+ ) where++import Conferer.Config+import qualified Conferer.Source.InMemory as InMemory+import Data.Text (Text)+import Data.Dynamic++-- | Create a Config mostly used for testing 'Conferer.FromConfig.FromConfig'+-- instances without having to create a full config+configWith :: [(Key, Dynamic)] -> [(Key, Text)] -> IO Config+configWith defaults keyValues =+ emptyConfig + & addDefaults defaults+ & addSource (InMemory.fromConfig keyValues)
− src/Conferer/Types.hs
@@ -1,118 +0,0 @@-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE TypeSynonymInstances #-}-{-# LANGUAGE DefaultSignatures #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeOperators #-}-module Conferer.Types where--import Data.String-import Data.Text (Text)-import qualified Data.Text as Text-import Data.Map (Map)-import qualified Data.Map as Map-import Control.Exception-import Data.Typeable-import GHC.Generics---- | Core interface for library provided configuration, basically consists of--- getting a 'Key' and informing returning a maybe signaling the value and--- if it's present in that specific source-data Source =- Source- { getKeyInSource :: Key -> IO (Maybe Text)- }---- | The way to index 'Source's, basically list of names that will be adapted--- to whatever the source needs-newtype Key- = Path { unKey :: [Text] }- deriving (Show, Eq, Ord)--instance IsString Key where- fromString s = Path $ filter (/= mempty) $ Text.split (== '.') $ fromString s---- | Collapse a key into a textual representation-keyName :: Key -> Text-keyName = Text.intercalate "." . unKey---- | Core type that the user of this library interact with, in the future it may--- contain more this besides a list of sources-data Config =- Config- { sources :: [Source]- , defaults :: Map Key Text- }---- | The type for creating a source given a 'Config', some sources require a--- certain configuration to be initialized (for example: the redis source--- needs connection info to connect to the server)-type SourceCreator = Config -> IO Source--keyNotPresentError :: forall a. (Typeable a) => Key -> Proxy a -> FailedToFetchError-keyNotPresentError key =- throw $ FailedToFetchError key $ typeRep (Proxy :: Proxy a)---- | Default defining instance------ Here a 'Nothing' means that the value didn't appear in the config, some--- instances never return a value since they have defaults that can never--- fail-class DefaultConfig a where- configDef :: a---- | Main typeclass for defining the way to get values from config, hiding the--- 'Text' based nature of the 'Source's.--- updated using a config, so for example a Warp.Settings can get updated from a config,--- but that doesn't make much sense for something like an 'Int'------ You'd normally would never implement this typeclass, if you want to implement--- 'FromConfig' you should implement that directly, and if you want to use--- 'DefaultConfig' and 'FromConfig' to implement 'FromConfig' you should let--- the default 'Generics' based implementation do it's thing-class FromConfig a where- updateFromConfig :: Key -> Config -> a -> IO a- default updateFromConfig :: (Generic a, Typeable a, FromConfigG (Rep a)) => Key -> Config -> a -> IO a- updateFromConfig k c a = to <$> updateFromConfigG k c (from a)-- fetchFromConfig :: Key -> Config -> IO (Maybe a)- default fetchFromConfig :: (Generic a, FromConfigG (Rep a)) => Key -> Config -> IO (Maybe a)- fetchFromConfig k c = fmap to <$> fetchFromConfigG k c---- | Purely 'Generics' machinery, ignore...-class FromConfigG f where- updateFromConfigG :: Key -> Config -> f a -> IO (f a)- fetchFromConfigG :: Key -> Config -> IO (Maybe (f a))--data ConfigParsingError =- ConfigParsingError Key Text TypeRep- deriving (Typeable, Eq)--instance Show ConfigParsingError where- show (ConfigParsingError key value typeRep) =- concat- [ "Couldn't parse value '"- , Text.unpack value- , "' from key '"- , Text.unpack (keyName key)- , "' as "- , show typeRep- ]--instance Exception ConfigParsingError--data FailedToFetchError =- FailedToFetchError Key TypeRep- deriving (Typeable, Eq)--instance Show FailedToFetchError where- show (FailedToFetchError key typeRep) =- concat- [ "Couldn't get a "- , show typeRep- , " from key '"- , Text.unpack (keyName key)- , "'"- ]--instance Exception FailedToFetchError
+ test/Conferer/ConfigSpec.hs view
@@ -0,0 +1,211 @@+{-# LANGUAGE TypeApplications #-}+module Conferer.ConfigSpec where++import Test.Hspec+import Data.Text (Text)+import Data.Dynamic++import Conferer.Config+import Conferer.Source.InMemory+import Conferer.FromConfig.Internal (fromDynamics)++missingKey :: Key -> KeyLookupResult -> Bool +missingKey expectedKey (MissingKey k) = [expectedKey] == k+missingKey _ _ = False++foundInSources :: Key -> Text -> KeyLookupResult -> Bool +foundInSources expectedKey expected (FoundInSources k t) = + expected == t && expectedKey == k+foundInSources _keyExpected _expected _ = + False++foundInDefaults :: forall expected. (Eq expected, Typeable expected)+ => Key -> expected -> KeyLookupResult -> Bool +foundInDefaults expectedKey expected (FoundInDefaults k d) = + Just expected == fromDynamics @expected d && expectedKey == k+foundInDefaults _expctedKey _expected _ = + False++spec :: Spec+spec = do+ describe "Config" $ do+ let mkConfig keyMappings defaults content = + pure (emptyConfig+ & addKeyMappings keyMappings+ & addDefaults defaults)+ >>= addSource (fromConfig content)+ describe "#getKey" $ do+ it "getting a non existent key returns missing key" $ do+ c <- mkConfig [] [] []+ res <- getKey "aaa" c+ res `shouldSatisfy` missingKey "aaa"++ it "getting a key only present in the defaults" $ do+ c <- mkConfig [] [("some.key", toDyn @Int 1)] []+ res <- getKey "some.key" c+ res `shouldSatisfy` foundInDefaults "some.key" (1 :: Int)++ context "with multiple sources and defaults" $ do+ let mkConfig' defaults sourceWithPriority otherSource =+ pure (emptyConfig+ & addDefaults defaults)+ >>= addSource (fromConfig sourceWithPriority)+ >>= addSource (fromConfig otherSource)++ it "getting an key returns unwraps the original map" $ do+ c <- mkConfig' [] [] [("some.key", "1")]+ res <- getKey "some.key" c+ res `shouldSatisfy` foundInSources "some.key" "1"++ it "getting an existent key returns in the bottom maps gets it" $ do+ c <- mkConfig' [] [("some.key", "2")] [("some.key", "1")]+ res <- getKey "some.key" c+ res `shouldSatisfy` foundInSources "some.key" "2"+ describe "with some key mapping" $ do+ context "with basic one to one mapping" $ + it "gets the value through a mapping" $ do+ c <- mkConfig+ [ ("something", "server") ]+ []+ [ ("server", "aaa") ]+ res <- getKey "something" c+ res `shouldSatisfy` foundInSources "server" "aaa"+ context "with a nested key mapping" $ do+ it "goes through all the mappings and gets the right value" $ do+ c <- mkConfig+ [ ("something", "else")+ , ("else", "server")+ ]+ []+ [ ("server", "aaa")+ ]+ res <- getKey "something" c+ res `shouldSatisfy` foundInSources "server" "aaa"+ context "with circular mappings" $ do+ it "gets the first key" $ do+ c <- mkConfig+ [ ("a", "b")+ , ("b", "a")+ , ("b", "c")+ ]+ []+ [ ("c", "aaa")+ ]+ res <- getKey "a" c+ res `shouldSatisfy` foundInSources "c" "aaa"+ context "with nested key" $ do+ it "maps the right upper key" $ do+ c <- mkConfig+ [ ("a", "b")+ ]+ []+ [ ("b.k", "aaa")+ ]+ res <- getKey "a.k" c+ res `shouldSatisfy` foundInSources "b.k" "aaa"+ context "with some defaults" $ do+ it "maps and allows getting the default" $ do+ c <- mkConfig+ [ ("a", "b")+ ]+ [ ("b", toDyn False)+ ]+ []+ res <- getKey "a" c+ res `shouldSatisfy` foundInDefaults "a" False+ describe "#listSubkeys" $ do+ it "return an empty list when nothing is configured" $ do+ c <- mkConfig [] [] []+ res <- listSubkeys "some" c+ res `shouldBe` []+ context "with some values in the config" $ do+ it "when nothing is configured" $ do+ c <- mkConfig [] [] [("some.key", "")]+ res <- listSubkeys "some" c+ res `shouldBe` ["some.key"]+ context "with a config that has a value configured and \+ \we get that same value" $ do+ it "returns that key in the list of subkeys" $ do+ c <- mkConfig [] [] [("some", "")]+ res <- listSubkeys "some" c+ res `shouldBe` []+ context "with a config that has a value configured but \+ \we get a different value" $ do+ it "returns an empty list" $ do+ c <- mkConfig [] [] [("some", "")]+ res <- listSubkeys "other" c+ res `shouldBe` []+ context "with a config with defaults" $ do+ it "returns those defaults in the list" $ do+ c <- mkConfig [] [("some.key", toDyn @Int 7)] []+ res <- listSubkeys "some" c+ res `shouldBe` ["some.key"]+ context "with configs in both defaults and sources" $ do+ it "returns both results combined" $ do+ c <- mkConfig [] [("some.key", toDyn @Int 7)] [("some.other", "")]+ res <- listSubkeys "some" c+ res `shouldBe` ["some.key", "some.other"]+ context "mappings" $ do+ context "with a simple mapping and a configured value" $ do+ it "returns the keys that are present based on the mapping" $ do+ c <- mkConfig+ [ ("a", "b")+ ]+ []+ [ ("b.k", "aaa")+ ]+ res <- listSubkeys "a" c+ res `shouldBe` ["a.k"]+ context "with a multiple mappings and a configured value" $ do+ it "returns the right keys" $ do+ c <- mkConfig+ [ ("a", "b")+ , ("b", "c")+ ]+ []+ [ ("c.k", "aaa")+ ]+ res <- listSubkeys "a" c+ res `shouldBe` ["a.k"]+ context "with circular mappings" $ do+ it "returns the right keys" $ do+ c <- mkConfig+ [ ("a", "b")+ , ("b", "a")+ ]+ []+ [ ("a.k", "aaa")+ ]+ res <- listSubkeys "a" c+ res `shouldBe` ["a.k"]+ context "with reducing mappings" $ do+ xit "returns the right keys" $ do+ c <- mkConfig+ [ ("a.k", "a")+ ]+ []+ [ ("a.k", "aaa")+ ]+ res <- listSubkeys "a" c+ -- getKey "a.k.k" c >>= print+ res `shouldBe` ["a.k", "a.k.k"]+ it "keys that are present based on the mapping" $ do+ c <- mkConfig+ [ ("a", "b")+ ]+ [ ("b.k", toDyn @String "aaa")+ ]+ [ ]+ res <- listSubkeys "a" c+ res `shouldBe` ["a.k"]++ describe "#addDefaults" $ do+ context "with multiple defaults on the same key" $ do+ it "the last one has more priority" $ do+ let c = emptyConfig+ & addDefaults+ [ ("some.key", toDyn @Int 1)+ , ("some.key", toDyn @Int 2)+ ]+ res <- getKey "some.key" c+ res `shouldSatisfy` foundInDefaults "some.key" (2 :: Int)
− test/Conferer/FromConfig/BasicsSpec.hs
@@ -1,88 +0,0 @@-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}--module Conferer.FromConfig.BasicsSpec where--import Test.Hspec-import Conferer.Types-import Data.Text-import Conferer-import Control.Exception (evaluate)-import Data.Typeable-import Control.DeepSeq--configWith :: [(Key, Text)] -> IO Config-configWith keyValues = emptyConfig & addSource (mkMapSource keyValues)--configParserError_ :: ConfigParsingError -> Bool-configParserError_ = const True--configParserError :: Key -> Text -> ConfigParsingError -> Bool-configParserError key txt (ConfigParsingError k t _) =- key == k && t == txt--fetch :: (FromConfig a, Show a, Typeable a) => Key -> Config -> IO (Maybe a)-fetch = getFromConfig--spec :: Spec-spec = context "Basics" $ do- describe "fetching an Int from config" $ do- it "getting a value that can't be parsed as an int returns an error message" $ do- config <- configWith [ ("anInt", "50A") ]- fetch @Int "anInt" config `shouldThrow` configParserError "anInt" "50A"-- it "getting a value that can be parsed correctly returns the int" $ do- config <- configWith [ ("anInt", "50") ]- fetchedValue <- fetch @Int "anInt" config- fetchedValue `shouldBe` Just 50-- describe "fetching a Bool from config" $ do- it "getting a value that can't be parsed as a bool returns an error message" $ do- config <- configWith [ ("aBool", "nope") ]- fetch @Bool "aBool" config `shouldThrow` configParserError_-- it "getting a value that can be parsed as a bool returns the bool" $ do- config <- configWith [ ("aBool", "True"), ("anotherBool", "False") ]- fetchedValue <- fetch "aBool" config- fetchedValue `shouldBe` Just True- anotherFetchedValue <- fetch "anotherBool" config- anotherFetchedValue `shouldBe` Just False-- it "the parsing of the bool value is case insensitive" $ do- config <- configWith [ ("aBool", "TRUE"), ("anotherBool", "fAlSe") ]- fetchedValue <- fetch "aBool" config- fetchedValue `shouldBe` Just True- anotherFetchedValue <- fetch "anotherBool" config- anotherFetchedValue `shouldBe` Just False-- describe "fetching a String from config" $ do- it "getting a value returns the value as a string" $ do- config <- configWith [ ("aString", "Bleh") ]- fetchedValue <- fetch @String "aString" config- fetchedValue `shouldBe` Just "Bleh"-- describe "fetching a Float from config" $ do- it "if the value can be parsed as float, it returns that float" $ do- config <- configWith [ ("aFloat", "9.5") ]- fetchedValue <- fetch @Float "aFloat" config- fetchedValue `shouldBe` Just 9.5-- it "if the value cannot be parsed as float, it fails" $ do- config <- configWith [ ("aFloat", "ASD") ]- fetch @Float "aFloat" config `shouldThrow` configParserError_-- describe "fetching a String from config" $ do- context "when the key is there but has a wrong value" $ do- it "returns Nothing" $ do- config <- configWith [ ("anInt", "Bleh") ]- fetch @Int "anInt" config `shouldThrow` configParserError_- context "when the key is not there" $ do- it "returns Nothing" $ do- config <- configWith [ ]- fetchedValue <- fetch @Int "anInt" config- fetchedValue `shouldBe` Nothing- it "returns Nothing" $ do- config <- configWith [ ]- fetchedValue <- fetch @(Maybe Int) "anInt" config- fetchedValue `shouldBe` Just Nothing
+ test/Conferer/FromConfig/BoolSpec.hs view
@@ -0,0 +1,26 @@+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Conferer.FromConfig.BoolSpec (spec) where++import Test.Hspec+import Conferer.FromConfig.Extended++spec :: Spec+spec = do+ context "Basic fetching" $ do+ describe "fetching a Bool from config" $ do+ ensureEmptyConfigThrows @Bool++ ensureUsingDefaultReturnsSameValue @Bool True+ ensureUsingDefaultReturnsSameValue @Bool False++ ensureSingleConfigParsesTheRightThing @Bool "true" True+ ensureSingleConfigParsesTheRightThing @Bool "false" False++ context "parsing is case insensitive" $ do+ ensureSingleConfigParsesTheRightThing @Bool "faLSe" False+ ensureSingleConfigParsesTheRightThing @Bool "TRUE" True++ ensureSingleConfigThrowsParserError @Bool "nope"
+ test/Conferer/FromConfig/Extended.hs view
@@ -0,0 +1,148 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Conferer.FromConfig.Extended+ ( module Conferer.FromConfig+ , module GHC.Generics+ , configWith+ , anyConfigParserError+ , aConfigParserError+ , aMissingRequiredKey+ , aMissingRequiredKeys+ , ensureEmptyConfigThrows+ , ensureUsingDefaultReturnsSameValue+ , ensureSingleConfigParsesTheRightThing+ , ensureSingleConfigThrowsParserError+ , ensureSingleConfigThrows+ , ensureFetchParses+ , ensureFetchThrows+ , toDyn+ , module Conferer.Config+ ) where++import Data.Text+import Data.Typeable+import Test.Hspec+import GHC.Generics+import Control.Exception+import Data.Dynamic+import Data.Function++import Conferer.Config+import Conferer.FromConfig.Internal+ ( MissingRequiredKey(..)+ , ConfigParsingError(..)+ )+import Conferer.FromConfig+import qualified Conferer.Source.InMemory as InMemory++configWith :: [(Key, Text)] -> IO Config+configWith keyValues =+ emptyConfig+ & addSource (InMemory.fromConfig keyValues)++anyConfigParserError :: ConfigParsingError -> Bool+anyConfigParserError _ = True++aConfigParserError :: Key -> Text -> ConfigParsingError -> Bool+aConfigParserError key txt (ConfigParsingError k t _) =+ key == k && t == txt++aMissingRequiredKey :: forall t. Typeable t => Key -> MissingRequiredKey -> Bool+aMissingRequiredKey key (MissingRequiredKey k t) =+ [key] == k && typeRep (Proxy :: Proxy t) == t++aMissingRequiredKeys :: forall t. Typeable t => [Key] -> MissingRequiredKey -> Bool+aMissingRequiredKeys keys (MissingRequiredKey k t) =+ keys == k && typeRep (Proxy :: Proxy t) == t++data InvalidThing = InvalidThing deriving (Show, Eq)++ensureEmptyConfigThrows :: forall a. (Typeable a, FromConfig a) => SpecWith ()+ensureEmptyConfigThrows =+ context "with empty config" $ do+ it "throws an exception" $ do+ config <- configWith []+ fetchFromConfig @a "some.key" config+ `shouldThrow` aMissingRequiredKey @a "some.key"++ensureSingleConfigThrowsParserError ::+ forall a. (FromConfig a, Typeable a) =>+ Text -> SpecWith ()+ensureSingleConfigThrowsParserError keyContent =+ context "with invalid types in the defaults" $ do+ it "throws an exception" $ do+ config <- configWith [("some.key", keyContent)]+ fetchFromConfig @a "some.key" config+ `shouldThrow` aConfigParserError "some.key" keyContent++ensureUsingDefaultReturnsSameValue ::+ forall a. (Typeable a, Eq a, Show a, FromConfig a) =>+ a -> SpecWith ()+ensureUsingDefaultReturnsSameValue value =+ context ("with a default of '" ++ show value ++ "'") $ do+ it "gets that same value" $ do+ config <- configWith []+ fetchedValue <- fetchFromConfig @a "some.key"+ (config & addDefault "some.key" value)+ fetchedValue `shouldBe` value++ensureSingleConfigParsesTheRightThing ::+ forall a. (Eq a, Show a, FromConfig a, Typeable a) =>+ Text -> a -> SpecWith ()+ensureSingleConfigParsesTheRightThing keyContent value =+ context ("with a config value of '" ++ unpack keyContent ++ "'" ) $ do+ it "gets the right value" $ do+ config <- configWith [("some.key", keyContent)]+ fetchedValue <- fetchFromConfig @a "some.key" config+ fetchedValue `shouldBe` value++ensureSingleConfigThrows ::+ forall a e. (FromConfig a, Typeable a, Exception e) =>+ Text -> (e -> Bool) -> SpecWith ()+ensureSingleConfigThrows keyContent checkException =+ it "gets the right value" $ do+ config <- configWith [("some.key", keyContent)]+ fetchFromConfig @a "some.key" config+ `shouldThrow` checkException++ensureFetchParses ::+ forall a.+ ( FromConfig a+ , Typeable a+ , Eq a+ , Show a+ )+ => [(Key, Text)]+ -> [(Key, Dynamic)]+ -> a+ -> SpecWith ()+ensureFetchParses configs defaults expectedValue =+ it "gets the right value" $ do+ config <- addDefaults (mapKeys defaults) <$> configWith (mapKeys configs)+ fetchedValue <- fetchFromConfig @a "some.key" config+ fetchedValue `shouldBe` expectedValue+ where+ mapKeys :: [(Key, any)] -> [(Key, any)]+ mapKeys = fmap (\(k, x) -> ("some.key" /. k, x))++ensureFetchThrows ::+ forall a e.+ ( FromConfig a+ , Typeable a+ , Exception e+ )+ => [(Key, Text)]+ -> [(Key, Dynamic)]+ -> (e -> Bool)+ -> SpecWith ()+ensureFetchThrows configs defaults exceptionCheck =+ it "throws an exception" $ do+ config <- addDefaults (mapKeys defaults) <$> configWith (mapKeys configs)+ fetchFromConfig @a "some.key" config+ `shouldThrow` exceptionCheck+ where+ mapKeys :: forall x. [(Key, x)] -> [(Key, x)]+ mapKeys = fmap (\(k, x) -> ("some.key" /. k, x))
+ test/Conferer/FromConfig/FileSpec.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE TypeApplications #-}+module Conferer.FromConfig.FileSpec (spec) where++import Test.Hspec+import Conferer.FromConfig.Extended+import Conferer.FromConfig ()+import System.FilePath ((</>))+import Data.Text (pack)++spec :: Spec+spec = do+ context "File fromConfig" $ do+ describe "fetching a File from config" $ do+ ensureFetchParses @File [] [("", toDyn $ File "file.png")] $+ File "file.png"+ ensureFetchThrows @File [] [] $+ aMissingRequiredKeys @String+ [ "some.key"+ , "some.key.extension"+ , "some.key.dirname"+ , "some.key.basename"+ , "some.key.filename"+ ]++ context "with whole path in root" $ do+ ensureFetchParses @File+ [ ("", pack $ "some" </> "file.png")+ ]+ []+ $ File $ "some" </> "file.png"++ context "with whole path in root and overriding extension" $ do+ ensureFetchParses @File+ [ ("", pack $ "some" </> "file.png")+ , ("extension", "jpg")+ ]+ []+ $ File $ "some" </> "file.jpg"++ context "with whole path in root and overriding extension and basename" $ do+ ensureFetchParses @File+ [ ("", pack $ "some" </> "file.png")+ , ("extension", "jpg")+ , ("basename", "somefile")+ ]+ []+ $ File $ "some" </> "somefile.jpg"++ context "with whole path in root and overriding dirname" $ do+ ensureFetchParses @File+ [ ("", pack $ "some" </> "file.png")+ , ("dirname", "dir")+ ]+ []+ $ File $ "dir" </> "file.png"++ context "with both filename and basename the most specific takes priority" $ do+ ensureFetchParses @File+ [ ("", pack $ "some" </> "file.png")+ , ("filename", "somefile.jpg")+ , ("basename", "coolfile")+ ]+ []+ $ File $ "some" </> "coolfile.jpg"++ context "with both filename and extension the most specific takes priority" $ do+ ensureFetchParses @File+ [ ("", pack $ "some" </> "file.png")+ , ("filename", "somefile.jpg")+ , ("extension", "gif")+ ]+ []+ $ File $ "some" </> "somefile.gif"++ context "without root path specificied, with subcomponents that complete a path" $ do+ ensureFetchParses @File+ [ ("filename", "somefile.jpg")+ , ("dirname", pack $ "some" </> "path")+ ]+ []+ $ File $ "some" </> "path" </> "somefile.jpg"
+ test/Conferer/FromConfig/ListSpec.hs view
@@ -0,0 +1,171 @@++{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Conferer.FromConfig.ListSpec (spec) where++import Test.Hspec+import Conferer.FromConfig.Extended++data Thing = Thing+ { thingA :: Int+ , thingB :: String+ } deriving (Generic, Show, Eq)++instance FromConfig Thing++spec :: Spec+spec = do+ describe "list parsing" $ do+ ensureEmptyConfigThrows @[Int]+ ensureUsingDefaultReturnsSameValue @[Int] [7]++ context "with an empty keys it always gets an empty list" $ do+ ensureFetchParses+ @[Int]+ [ ("keys", "")+ ]+ [ ("", toDyn @[Int] [1, 2, 3]) ]+ []+ context "with a fully defined list without a default" $ do+ ensureFetchParses+ @[Int]+ [ ("0", "2")+ ]+ []+ [2]++ context "with a non number key" $ do+ ensureFetchParses+ @[Int]+ [ ("a", "0")+ , ("b", "1")+ ]+ []+ [0, 1]++ context "orders the values lexicographically" $ do+ ensureFetchParses+ @[Int]+ [ ("09", "0")+ , ("10", "1")+ ]+ []+ [0, 1]+ context "with a prototype as default" $ do+ ensureFetchParses+ @[Thing]+ [ ("0.a", "0")+ , ("1.b", "aaa")+ ]+ [ ("prototype", toDyn (Thing 7 "lala"))+ ]+ [ Thing { thingA = 0, thingB = "lala"}+ , Thing { thingA = 7, thingB = "aaa"}+ ]++ context "with a partial prototype that gets completed later" $ do+ ensureFetchParses+ @[Thing]+ [ ("0.a", "0")+ , ("prototype.b", "lala")+ ]+ []+ [ Thing { thingA = 0, thingB = "lala"}+ ]++ context "when the user updates the prototype via config" $ do+ ensureFetchParses+ @[Thing]+ [ ("0.a", "0")+ , ("prototype.b", "aaa")+ ]+ [("prototype", toDyn Thing {thingA = 7, thingB = "lala"})+ ]+ [ Thing { thingA = 0, thingB = "aaa"}+ ]+ context "listing keys explicitly uses keys in that order" $ do+ ensureFetchParses+ @[Thing]+ [ ("1.a", "0")+ , ("1.b", "a")+ , ("a.a", "1")+ , ("a.b", "b")+ , ("keys", "1,a")+ ]+ [ ]+ [ Thing 0 "a"+ , Thing 1 "b"+ ]+ context "listing keys explicitly uses the prototype if the key is not complete" $ do+ ensureFetchParses+ @[Thing]+ [ ("1.a", "0")+ , ("keys", "1")+ ]+ [ ("prototype", toDyn $ Thing 7 "lele")+ ]+ [ Thing 0 "lele"+ ]+ context "listing keys explicitly I can use values from the default" $ do+ ensureFetchParses+ @[Thing]+ [ ("keys", "defaults.0")+ ]+ [ ("", toDyn+ [ Thing {thingA = 0, thingB = "a"}+ , Thing {thingA = 1, thingB = "b"}+ ])+ ]+ [ Thing 0 "a"+ ]+ context "listing keys explicitly I can use values from the default \+ \ and override some of the values" $ do+ ensureFetchParses+ @[Thing]+ [ ("keys", "defaults.0")+ , ("defaults.0.a", "7")+ ]+ [ ("", toDyn+ [ Thing {thingA = 0, thingB = "a"}+ , Thing {thingA = 1, thingB = "b"}+ ])+ ]+ [ Thing 7 "a"+ ]+ context "listing keys explicitly and using a default from an index \+ \that's not present" $ do+ ensureFetchThrows+ @[Thing]+ [ ("keys", "defaults.5")+ ]+ [ ("", toDyn+ [ Thing {thingA = 0, thingB = "a"}+ , Thing {thingA = 1, thingB = "b"}+ ])+ ]+ $ aMissingRequiredKey @Int "some.key.defaults.5.a"+ context "listing a default keys that's not present ignores the \+ \prototype" $ do+ ensureFetchThrows+ @[Thing]+ [ ("keys", "defaults.5")+ ]+ [ ("prototype", toDyn $ Thing 7 "lele")+ , ("", toDyn+ [ Thing {thingA = 0, thingB = "a"}+ , Thing {thingA = 1, thingB = "b"}+ ])+ ]+ $ aMissingRequiredKey @Int "some.key.defaults.5.a"+ context "fetching maps every key (and they can be used if the lower \+ \fromconfig uses listSubkeys" $ do+ ensureFetchParses+ @[[Int]]+ [ ("1.0", "0")+ ]+ [ ("prototype.1", toDyn @Int 8)+ ]+ [[0, 8]]
+ test/Conferer/FromConfig/MaybeSpec.hs view
@@ -0,0 +1,33 @@++{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Conferer.FromConfig.MaybeSpec (spec) where++import Test.Hspec+import Conferer.FromConfig.Extended++data Thing = Thing+ { thingA :: Int+ , thingB :: String+ } deriving (Generic, Show, Eq)++instance FromConfig Thing++spec :: Spec+spec = do+ describe "fetching a Maybe from config" $ do+ context "when the key is there and has a valid value" $ do+ ensureFetchParses @(Maybe Int) [("", "7")] [] $ Just 7+ context "when the key is missing" $ do+ ensureFetchParses @(Maybe Int) [] [] Nothing+ context "when the key is there but has a wrong value" $ do+ ensureFetchThrows @(Maybe Int) [("", "Bleh")] [] anyConfigParserError+ context "with default of the inner type" $ do+ ensureFetchParses @(Maybe Int) [] [("", toDyn @Int 7)] $ Just 7+ context "with default of a Just inner type" $ do+ ensureFetchParses @(Maybe Int) [] [("", toDyn @(Maybe Int) $ Just 7)] $ Just 7+ context "with default of a Nothing and no configuration" $ do+ ensureFetchParses @(Maybe Int) [] [("", toDyn @(Maybe Int) $ Nothing)] $ Nothing
+ test/Conferer/FromConfig/NumbersSpec.hs view
@@ -0,0 +1,29 @@++{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Conferer.FromConfig.NumbersSpec (spec) where++import Test.Hspec+import Conferer.FromConfig.Extended++spec :: Spec+spec = do+ context "Numbers fetching" $ do+ describe "fetching an Int from config" $ do+ ensureEmptyConfigThrows @Int+ ensureUsingDefaultReturnsSameValue @Int 7+ ensureSingleConfigParsesTheRightThing @Int "7" 7+ ensureSingleConfigParsesTheRightThing @Int "-7" (-7)+ ensureSingleConfigThrowsParserError @Int "50A"++ describe "fetching a Float from config" $ do+ ensureEmptyConfigThrows @Float+ ensureUsingDefaultReturnsSameValue @Float 7.5+ ensureSingleConfigParsesTheRightThing @Float "9.5" 9.5+ ensureSingleConfigParsesTheRightThing @Float "-9.5" (-9.5)+ ensureSingleConfigThrowsParserError @Float "9.50J"+ ensureSingleConfigThrowsParserError @Float ".5"+ ensureSingleConfigThrowsParserError @Float "9."
+ test/Conferer/FromConfig/StringLikeSpec.hs view
@@ -0,0 +1,28 @@+{-# LANGUAGE TypeApplications #-}++module Conferer.FromConfig.StringLikeSpec (spec) where++import Test.Hspec+import Data.Text (Text)+import Conferer.FromConfig.Extended+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as LBS++spec :: Spec+spec = do+ describe "fetching a String from config" $ do+ ensureEmptyConfigThrows @String+ ensureUsingDefaultReturnsSameValue @String "aaa"+ ensureSingleConfigParsesTheRightThing @String "thing" "thing"+ describe "fetching text from config" $ do+ ensureEmptyConfigThrows @Text+ ensureUsingDefaultReturnsSameValue @Text "aaa"+ ensureSingleConfigParsesTheRightThing @Text "thing" "thing"+ describe "fetching bytestring from config" $ do+ ensureEmptyConfigThrows @BS.ByteString+ ensureUsingDefaultReturnsSameValue @BS.ByteString "aaa"+ ensureSingleConfigParsesTheRightThing @BS.ByteString "thing" "thing"+ describe "fetching lazy bytestring from config" $ do+ ensureEmptyConfigThrows @LBS.ByteString+ ensureUsingDefaultReturnsSameValue @LBS.ByteString "aaa"+ ensureSingleConfigParsesTheRightThing @LBS.ByteString "thing" "thing"
+ test/Conferer/FromConfigSpec.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE TypeApplications #-}+module Conferer.FromConfigSpec (spec) where++import Test.Hspec+import Conferer.FromConfig+import Conferer.FromConfig.Extended++spec :: Spec+spec = do+ describe "Override FromConfig" $ do+ ensureFetchParses @Int+ []+ [ ("", overrideFetch @Int $ \_k _c -> pure 42) ]+ 42
test/Conferer/GenericsSpec.hs view
@@ -4,14 +4,10 @@ import Test.Hspec -import Conferer-import Conferer.Types (FromConfig, DefaultConfig, configDef)+import Conferer.FromConfig -import Data.Typeable import GHC.Generics--fetch :: (FromConfig a, Show a, Typeable a, DefaultConfig a) => Key -> Config -> IO a-fetch k c = getFromConfig k c+import Conferer.FromConfig.Extended data Thing = Thing { thingA :: Int@@ -19,6 +15,7 @@ } deriving (Generic, Show, Eq) instance FromConfig Thing+ instance DefaultConfig Thing where configDef = Thing 0 0 @@ -28,79 +25,74 @@ } deriving (Generic, Show, Eq) instance FromConfig Bigger+ instance DefaultConfig Bigger where- configDef = Bigger (configDef { thingA = 27}) 1+ configDef = Bigger (configDef {thingA = 27}) 1 spec :: Spec spec = do describe "Generics" $ do context "with a simple record" $ do+ context "without a default but with all of the keys defined" $ do+ ensureFetchParses+ [ ("a", "0")+ , ("b", "0")+ ]+ []+ Thing { thingA = 0, thingB = 0 } context "when no keys are set" $ do- it "returns the default" $ do- c <- emptyConfig- & addSource (mkMapSource [ ])-- res <- fetch @Thing "somekey" c- res `shouldBe` Thing { thingA = 0, thingB = 0 }+ ensureFetchParses+ []+ [ ("", toDyn $ configDef @Thing)+ ]+ Thing { thingA = 0, thingB = 0 } context "when all keys are set" $ do- it "return the keys set" $ do- c <- emptyConfig- & addSource (mkMapSource- [ ("somekey.a", "1")- , ("somekey.b", "2")- ])-- res <- fetch @Thing "somekey" c- res `shouldBe` Thing { thingA = 1, thingB = 2 }+ ensureFetchParses+ [ ("a", "1")+ , ("b", "2")+ ]+ [ ("", toDyn $ configDef @Thing)+ ]+ Thing { thingA = 1, thingB = 2 } context "when some keys are set" $ do- it "uses the default and returns the keys set" $ do- c <- emptyConfig- & addSource (mkMapSource- [ ("somekey.b", "2")- ])-- res <- fetch @Thing "somekey" c- res `shouldBe` Thing { thingA = 0, thingB = 2 }+ ensureFetchParses+ [ ("b", "2")+ ]+ [ ("", toDyn $ configDef @Thing)+ ]+ Thing { thingA = 0, thingB = 2 } context "with a nested record" $ do context "when none of the keys are set" $ do- it "returns the default of both records" $ do- c <- emptyConfig- & addSource (mkMapSource- [ ])-- res <- fetch @Bigger "somekey" c- res `shouldBe` Bigger { biggerThing = Thing { thingA = 27, thingB = 0 }, biggerB = 1}+ ensureFetchParses+ [ ]+ [ ("", toDyn @Bigger configDef )+ ]+ Bigger { biggerThing = Thing { thingA = 27, thingB = 0 }, biggerB = 1} context "when some keys of the top record are set" $ do- it "returns the default for the inner record" $ do- c <- emptyConfig- & addSource (mkMapSource- [ ("somekey.b", "30")- ])-- res <- fetch @Bigger "somekey" c- res `shouldBe` Bigger { biggerThing = Thing { thingA = 27, thingB = 0 }, biggerB = 30}+ ensureFetchParses+ [ ("b", "30")+ ]+ [ ("", toDyn @Bigger configDef)+ ]+ Bigger { biggerThing = Thing { thingA = 27, thingB = 0 }, biggerB = 30} context "when some keys of the inner record are set" $ do- it "returns the inner record updated" $ do- c <- emptyConfig- & addSource (mkMapSource- [ ("somekey.thing.a", "30")- ])-- res <- fetch @Bigger "somekey" c- res `shouldBe` Bigger { biggerThing = Thing { thingA = 30, thingB = 0 }, biggerB = 1}+ ensureFetchParses+ [ ("thing.a", "30")+ ]+ [ ("", toDyn @Bigger configDef)+ ]+ Bigger { biggerThing = Thing { thingA = 30, thingB = 0 }, biggerB = 1} context "when every key is set" $ do- it "returns everything with the right values" $ do- c <- emptyConfig- & addSource (mkMapSource- [ ("somekey.thing.a", "10")- , ("somekey.thing.b", "20")- , ("somekey.b", "30")- ])-- res <- fetch @Bigger "somekey" c- res `shouldBe` Bigger { biggerThing = Thing { thingA = 10, thingB = 20 }, biggerB = 30}+ ensureFetchParses+ [ ("thing.a", "10")+ , ("thing.b", "20")+ , ("b", "30")+ ]+ [ ("", toDyn @Bigger configDef)+ ]+ Bigger { biggerThing = Thing { thingA = 10, thingB = 20 }, biggerB = 30}
+ test/Conferer/KeySpec.hs view
@@ -0,0 +1,39 @@+module Conferer.KeySpec where++import Test.Hspec++import Conferer.Key+import Conferer.Key.Internal++spec :: Spec+spec = do+ describe "#fromString" $ do+ it "parsing keys does the right thing" $ do+ "some.key" `shouldBe` Key ["some", "key"]+ it "an empty string is the empty list" $ do+ "" `shouldBe` Key []+ it "empty key fragments are removed" $ do+ "some..key" `shouldBe` Key ["some", "key"]+ it "are matched in a case insensitive way" $ do+ "some.key" `shouldBe` ("SoME.KeY" :: Key)+ it "'s true representation is case insensitive" $ do+ "somE.Key" `shouldBe` Key ["some", "key"]+ describe "#isValidKeyFragment" $ do+ context "with a all leters" $ do+ it "is valid" $+ isValidKeyFragment "fragment" `shouldBe` True+ context "with only numbers" $ do+ it "is valid" $+ isValidKeyFragment "000" `shouldBe` True+ context "with both numbers and letters" $ do+ it "is valid" $+ isValidKeyFragment "000" `shouldBe` True+ context "with a dot" $ do+ it "is not valid" $ do+ isValidKeyFragment "some.fragment" `shouldBe` False+ context "with an uppercase letter" $ do+ it "is not valid" $ do+ isValidKeyFragment "FRAGMENT" `shouldBe` False+ context "with an empty string" $ do+ it "is not valid" $ do+ isValidKeyFragment "" `shouldBe` False
− test/Conferer/Source/ArgsSpec.hs
@@ -1,38 +0,0 @@-module Conferer.Source.ArgsSpec where--import Test.Hspec--import Conferer--spec :: Spec-spec = do- describe "with a mapping source" $ do- let mkConf args =- emptyConfig- & addSource- (mkCLIArgsSource' args)-- it "gets a parameters with it's value if it starts with the right prefix" $ do- c <- mkConf []- res <- getKey "some.key" c- res `shouldBe` Nothing-- it "with a value that begins with the right prefix it uses it" $ do- c <- mkConf ["--some.key=value"]- res <- getKey "some.key" c- res `shouldBe` Just "value"-- it "with a reapeated value it uses the last one" $ do- c <- mkConf ["--some.key=value", "--some.key=different value"]- res <- getKey "some.key" c- res `shouldBe` Just "different value"-- it "ignores values that don't start with the right prefix" $ do- c <- mkConf ["some.key=value", "-some.key=value", "-Xsome.key=value", "some.key"]- res <- getKey "some.key" c- res `shouldBe` Nothing-- it "after encountering a -- it stops parsing parameters" $ do- c <- mkConf ["--", "--some.key=value"]- res <- getKey "some.key" c- res `shouldBe` Nothing
+ test/Conferer/Source/CLIArgsSpec.hs view
@@ -0,0 +1,42 @@+module Conferer.Source.CLIArgsSpec where++import Test.Hspec++import Conferer.Source+import Conferer.Source.CLIArgs++spec :: Spec+spec = do+ describe "with a mapping source" $ do+ let mkConf args =+ return $ fromArgs args++ it "gets a parameters with it's value if it starts with the right prefix" $ do+ c <- mkConf []+ res <- getKeyInSource c "some.key"+ res `shouldBe` Nothing++ it "with a value that begins with the right prefix it uses it" $ do+ c <- mkConf ["--some.key=value"]+ res <- getKeyInSource c "some.key"+ res `shouldBe` Just "value"++ it "with a reapeated value it uses the last one" $ do+ c <- mkConf ["--some.key=value", "--some.key=different value"]+ res <- getKeyInSource c "some.key"+ res `shouldBe` Just "different value"++ it "ignores values that don't start with the right prefix" $ do+ c <- mkConf ["some.key=value", "-some.key=value", "-Xsome.key=value", "some.key"]+ res <- getKeyInSource c "some.key"+ res `shouldBe` Nothing++ it "after encountering a -- it stops parsing parameters" $ do+ c <- mkConf ["--", "--some.key=value"]+ res <- getKeyInSource c "some.key"+ res `shouldBe` Nothing++ it "with passed flag without a value it returns true" $ do+ c <- mkConf ["--some.key"]+ res <- getKeyInSource c "some.key"+ res `shouldBe` Just "true"
test/Conferer/Source/EnvSpec.hs view
@@ -1,41 +1,58 @@ module Conferer.Source.EnvSpec where -import Test.Hspec-import qualified Data.Map as Map--import Conferer+import Test.Hspec+import Test.QuickCheck -fakeLookupEnv :: [(String, String)] -> LookupEnvFunc-fakeLookupEnv fakeEnv = \envName ->- return $ Map.lookup envName $ Map.fromList fakeEnv+import Conferer.Source+import Conferer.Source.Env spec :: Spec spec = do- describe "with an env config" $ do+ describe "with an env source" $ do let- mkEnvConfig =- emptyConfig- & addSource- (mkEnvSource'- (fakeLookupEnv- [ ("TMUX","/tmp/tmux-1000/default,2822,0")- , ("TMUX_PANE","%1")- , ("TMUX_PLUGIN_MANAGER_PATH","/home/user/.tmux/plugins/")- ])- "TMUX"- )- it "getting an existent key returns unwraps top level value (wihtout \+ mkEnvConfig envVars =+ return $ fromEnvList envVars "APP"++ it "getting an existent key returns unwraps top level value (without \ \children)" $ do c <- mkEnvConfig- res <- getKey "." c- res `shouldBe` Just "/tmp/tmux-1000/default,2822,0"+ [ ("APP_THING","thing")+ , ("APP","root")+ , ("APP_PG_HOST","some host")+ , ("APP_PG_PORT","some port")+ , ("SOMETHING_ELSE","else")+ ]+ res <- getKeyInSource c ""+ res `shouldBe` Just "root" it "getting an existent key for a child gets that value" $ do c <- mkEnvConfig- res <- getKey "pane" c- res `shouldBe` Just "%1"+ [ ("APP_THING","thing")+ ]+ res <- getKeyInSource c "thing"+ res `shouldBe` Just "thing" - it "keys should always be consistent as to how the words are separated" $ do+ it "listing some keys returns all the right values" $ do c <- mkEnvConfig- res <- getKey "pane" c- res `shouldBe` Just "%1"+ [ ("APP_PG_HOST","some host")+ , ("APP_PG_PORT","some port")+ ]+ res <- getSubkeysInSource c "pg"+ res `shouldBe` ["pg.host", "pg.port"]++ it "listing the keys under the root list everything excluding ." $ do+ c <- mkEnvConfig+ [ ("APP_SOME","value")+ , ("APP","value")+ , ("SOMETHING_ELSE","else")+ ]+ res <- getSubkeysInSource c ""+ res `shouldBe` ["some"]++ it "key to env var and back be identity" $ property $+ forAll+ (elements ["a", "a.b", "", "a.b.c", "UPPER"])+ $ \k ->+ envVarToKey "theapp" (keyToEnvVar "theapp" k)+ == Just k+
+ test/Conferer/Source/InMemorySpec.hs view
@@ -0,0 +1,32 @@+module Conferer.Source.InMemorySpec where++import Test.Hspec++import Conferer.Source+import Conferer.Source.InMemory++spec :: Spec+spec = do+ describe "in memory source" $ do+ let source =+ fromAssociations [("postgres.url", "some url")]++ it "getting a non existent key returns an empty config" $ do+ res <- getKeyInSource source "some.key"+ res `shouldBe` Nothing++ it "getting an existent key returns unwraps the original map" $ do+ res <- getKeyInSource source "postgres.url"+ res `shouldBe` Just "some url"++ it "listing subkeys works" $ do+ res <- getSubkeysInSource source "postgres"+ res `shouldBe` ["postgres.url"]++ it "listing subkeys never returns the same key" $ do+ res <- getSubkeysInSource source "postgres.url"+ res `shouldBe` []++ it "listing subkeys that are not present" $ do+ res <- getSubkeysInSource source "stuff"+ res `shouldBe` []
− test/Conferer/Source/MappingSpec.hs
@@ -1,43 +0,0 @@-module Conferer.Source.MappingSpec where--import Test.Hspec-import qualified Data.Map as Map-import Data.Function ((&))--import Conferer--spec :: Spec-spec = do- describe "with a mapping source" $ do- it "getting an existent key in the original map but that's not mapped in the \- \wrapper doesn't exist" $ do- c <- emptyConfig- & addSource (mkMappingSource Map.empty- $ mkMapSource [("some.key", "some value")])- res <- getKey "some.key" c- res `shouldBe` Nothing-- it "getting a non existent key isn't there" $ do- c <- emptyConfig- & addSource (mkMappingSource (Map.fromList [("k", "key")])- $ mkMapSource [("key", "75")])-- res <- getKey "xxxx" c- res `shouldBe` Nothing-- it "getting an existent key that's mapped but doesn't exist on the \- \inner source isn't there" $ do- c <- emptyConfig- & addSource (mkMappingSource (Map.fromList [("another.key", "some.key")])- $ mkMapSource [])-- res <- getKey "another.key" c- res `shouldBe` Nothing-- it "getting an existent key that's mapped properly gets it and exists on \- \the inner source gets it" $ do- c <- emptyConfig- & addSource (mkMappingSource (Map.fromList [("another.key", "some.key")])- $ mkMapSource [("some.key", "some value")])- res <- getKey "another.key" c- res `shouldBe` Just "some value"
test/Conferer/Source/NamespacedSpec.hs view
@@ -2,20 +2,24 @@ import Test.Hspec -import Conferer+import Conferer.Source +import qualified Conferer.Source.InMemory as InMemory+import Conferer.Source.Namespaced+ spec :: Spec spec = do describe "namespaced config" $ do+ let source =+ fromInner "postgres" $+ InMemory.fromAssociations [("url", "some url")] it "return nothing if the key doesn't match" $ do- c <- emptyConfig- & addSource (mkNamespacedSource "postgres"- $ mkMapSource [("url", "some url")])- res <- getKey "url" c+ res <- getKeyInSource source "url" res `shouldBe` Nothing it "returns the wrapped value" $ do- c <- emptyConfig- & addSource (mkNamespacedSource "postgres"- $ mkMapSource [("url", "some url")])- res <- getKey "postgres.url" c+ res <- getKeyInSource source "postgres.url" res `shouldBe` Just "some url"+ it "listing subkeys" $ do+ res <- getSubkeysInSource source "postgres"+ res `shouldBe` ["postgres.url"]+
test/Conferer/Source/NullSpec.hs view
@@ -2,12 +2,11 @@ import Test.Hspec -import Conferer+import Conferer.Source+import Conferer.Source.Null spec :: Spec spec = do it "always fails to get a key" $ do- c <- emptyConfig- & addSource mkNullSource- res <- getKey "some.key" c+ res <- getKeyInSource empty "some.key" res `shouldBe` Nothing
test/Conferer/Source/PropertiesFileSpec.hs view
@@ -1,39 +1,32 @@ module Conferer.Source.PropertiesFileSpec where -import Test.Hspec-import Data.Text (Text)-import qualified Data.Text as Text-import qualified Data.Map as Map+import Test.Hspec -import Conferer-import Conferer.Core+import Conferer.Source import Conferer.Source.PropertiesFile -fakeLookupEnv :: [(String, String)] -> LookupEnvFunc-fakeLookupEnv fakeEnv = \envName ->- return $ Map.lookup envName $ Map.fromList fakeEnv- spec :: Spec spec = do describe "with a properties file config" $ do- it "getting an existent key returns unwraps top level value (wihtout \+ let mk = return . fromFileContent "file.properties"+ it "getting an existent key returns unwraps top level value (without \ \children)" $ do- c <- mkStandaloneSource $ mkPropertiesFileSource' "some.key=a value"+ c <- mk "some.key=a value" res <- getKeyInSource c "some.key" res `shouldBe` Just "a value" it "getting an existent key for a child gets that value" $ do- c <- mkStandaloneSource $ mkPropertiesFileSource' $ Text.unlines- [ "some.key=a value"- , "some.key=b"- ]+ c <- mk "some.key=b" res <- getKeyInSource c "some.key" res `shouldBe` Just "b" it "keys should always be consistent as to how the words are separated" $ do- c <- mkStandaloneSource $ mkPropertiesFileSource' $ Text.unlines- [ "some.key=a value"- , "some.key=b"- ]- res <- getKeyInSource c "another.key"- res `shouldBe` Nothing+ c <- mk "some.key=a value"+ res <- getKeyInSource c "some.key"+ res `shouldBe` Just "a value"++ it "respects leading spaces in values" $ do+ c <- mk "some.key= a value "+ res <- getKeyInSource c "some.key"+ res `shouldBe` Just " a value "+
− test/Conferer/Source/SimpleSpec.hs
@@ -1,22 +0,0 @@-module Conferer.Source.SimpleSpec where--import Test.Hspec--import Conferer--spec :: Spec-spec = do- describe "json source" $ do- let creator =- emptyConfig- & addSource (mkMapSource [ ("postgres.url", "some url")])-- it "getting a non existent key returns an empty config" $ do- c <- creator- res <- getKey "some.key" c- res `shouldBe` Nothing-- it "getting an existent key returns unwraps the original map" $ do- c <- creator- res <- getKey "postgres.url" c- res `shouldBe` Just "some url"
test/ConfererSpec.hs view
@@ -1,33 +1,47 @@+{-# LANGUAGE TypeApplications #-}+-- {-# LANGUAGE DeriveGeneric #-}+-- {-# LANGUAGE TypeApplications #-}+ module ConfererSpec where import Test.Hspec-import Conferer +import Data.Text (Text)+import Conferer.FromConfig (fetchFromConfig)+import Conferer.Source.InMemory (fromConfig)+import Data.Dynamic (toDyn)+import Conferer.Config (Config)+import Conferer (mkConfig', Key)+ spec :: Spec spec = do- describe "keys" $ do- it "parsing keys does the right thing" $ do- "some.key" `shouldBe` Path ["some", "key"]- it "an empty string is the empty list" $ do- "" `shouldBe` Path []+ describe "mkConfig'" $ do+ let a = fromConfig [("keyPresentInBoth", "valueInA"), ("keyPresentOnlyInA", "valueInA")]+ b = fromConfig [("keyPresentInBoth", "valueInB"), ("keyPresentOnlyInB", "valueInB")]+ shouldHaveKeyWithValue :: Config -> Key -> Text -> Expectation+ shouldHaveKeyWithValue config key expected = do+ actual <- fetchFromConfig key config+ actual `shouldBe` expected+ describe "the precedence of the sources is defined by the order in the list" $ do+ it "if several sources have a value for the key, the first source in the list that has it is used" $ do+ config <- mkConfig' [] [a, b] - describe "with a config with a nested value" $ do- let mkConfig =- pure emptyConfig- >>= addSource (mkMapSource [ ("postgres.url", "some url")])- >>= addSource (mkMapSource [ ("postgres.url", "different url") , ("server.port", "4000")])+ config `shouldHaveKeyWithValue` "keyPresentInBoth" $ "valueInA"+ it "if only one source has a value for the key, that source is used" $ do+ config <- mkConfig' [] [a, b] - it "getting a non existent key returns an empty config" $ do- c <- mkConfig- res <- getKey "aaa" c- res `shouldBe` Nothing+ config `shouldHaveKeyWithValue` "keyPresentOnlyInB" $ "valueInB"+ it "if the key is not present in any source, it doesn't retrieve any value" $ do+ config <- mkConfig' [] [a, b] - it "getting an existent key returns unwraps the original map" $ do- c <- mkConfig- res <- getKey "postgres.url" c- res `shouldBe` Just "some url"- it "getting an existent key returns in the bottom maps gets it" $ do- c <- mkConfig- res <- getKey "server.port" c- res `shouldBe` Just "4000"+ value <- fetchFromConfig "keyNotPresentInAnySource" config+ value `shouldBe` (Nothing :: Maybe Text)+ describe "adds the defaults to the config" $ do+ it "if the key is present in a source and in the default, it uses the first source that has it" $ do+ config <- mkConfig' [("keyPresentOnlyInA", toDyn @Text "defaultValue")] [a] + config `shouldHaveKeyWithValue` "keyPresentOnlyInA" $ "valueInA"+ it "if the key is not present in any source but it's in a default, it uses the default" $ do+ config <- mkConfig' [("keyPresentOnlyInA", toDyn @Text "defaultValue")] [b]++ config `shouldHaveKeyWithValue` "keyPresentOnlyInA" $ "defaultValue"
+ test/ConfererSpecMain.hs view
@@ -0,0 +1,7 @@+module ConfererSpecMain where++import Test.Hspec.Runner+import qualified Spec++main :: IO ()+main = hspecWith defaultConfig Spec.spec
test/Spec.hs view
@@ -1,1 +1,1 @@-{-# OPTIONS_GHC -F -pgmF hspec-discover #-}+{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec #-}