conferer 0.1.0.0 → 0.1.0.1
raw patch · 11 files changed
+290/−37 lines, 11 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
- Conferer.Types: type Prefix = Text
+ Conferer: (/.) :: Key -> Key -> Key
+ Conferer: Provider :: (Key -> IO (Maybe Text)) -> Provider
+ Conferer: [getKeyInProvider] :: Provider -> Key -> IO (Maybe Text)
+ Conferer: addProvider :: ProviderCreator -> Config -> IO Config
+ Conferer: class FetchFromConfig a
+ Conferer: data Config
+ Conferer: data Provider
+ Conferer: emptyConfig :: Config
+ Conferer: getFromConfig :: FetchFromConfig a => Key -> Config -> IO a
+ Conferer: getKey :: Key -> Config -> IO (Either Text Text)
+ Conferer: type ProviderCreator = Config -> IO Provider
+ Conferer: unsafeGetKey :: Key -> Config -> IO Text
+ Conferer.Provider.Env: type Prefix = Text
- Conferer.Provider.Env: type LookupEnvFunc = (String -> IO (Maybe String))
+ Conferer.Provider.Env: type LookupEnvFunc = String -> IO (Maybe String)
Files
- README.md +59/−0
- conferer.cabal +7/−4
- src/Conferer.hs +80/−5
- src/Conferer/Core.hs +23/−8
- src/Conferer/Provider/CLIArgs.hs +15/−1
- src/Conferer/Provider/Env.hs +48/−13
- src/Conferer/Provider/Mapping.hs +14/−1
- src/Conferer/Provider/Namespaced.hs +9/−1
- src/Conferer/Provider/Null.hs +8/−1
- src/Conferer/Provider/Simple.hs +11/−1
- src/Conferer/Types.hs +16/−2
+ README.md view
@@ -0,0 +1,59 @@+# Conferer++## what, why and a bit of how++VERY UNSTABLE DON'T USE IN PRODUCTION JUST YET++Conferer is a library that defines ways to get configuration for your Haskell+application and the libraries it uses, which is at heart string keys which map+to other strings.++To get this map we use `Providers` which define a way to get a key (eg.+`db.username`) that may or may not exist, we then use a list of providers for+getting the value for that key (position on the list defines priority). This+allows adding new providers easily (for example a dhall file provider,+a git repo or a etcd database)++The other side of this is that we have the `FromConfig` which gets some value+from a `Config` at a certain key possibly using only keys under some namespacing+key++## Example (not implemented)++Let's say I want to configure warp. Let's say we wrote this program.++```haskell+main = do+ -- by default gets cli parameters, envvars and json file+ config <- getDefaultConfigFor "awesomeapp"+ warpConfig :: Warp.Settings <- getKey "warp" config++ Warp.runSettings warpConfig myApp+```++Now I need to chage the port of the app, I can change it by either:++* Setting cli params like `./myApp -Cwarp.port=5555`+* Setting an environment variable called `AWESOMEAPP_WARP_PORT=5555`+* In a json file you can have `{"warp": {"port": 5555}}`++And you may also get that value from different configuration providers like+redis or etc or whichever you may need.++## Utilities++There are as well some utilities to change providers:++* `Conferer.Provider.Namespace`: All keys must be namespaced and the namespace+ is striped for lookup+* `Conferer.Provider.Mapped`: Using a map key to maybe key you can change the+ name of a key or even hiding some key+* `Conferer.Provider.Simple`: Get keys from a hardcoded map key to string++## Future maybe things++* Interpolate keys with other keys: `{a: "db", b: "${a}_thing"}`, getting `b`+ will give `"db_thing"` (maybe)+* A LOT of providers+* A LOT of `FromConfig` implementations+* Docs
conferer.cabal view
@@ -1,14 +1,14 @@-cabal-version: 1.12+cabal-version: 1.18 -- This file has been generated from package.yaml by hpack version 0.31.2. -- -- see: https://github.com/sol/hpack ----- hash: eea931123405e74a86b3e1283519e3369618e126404652c3818e55ad1071dad5+-- hash: a139753b9c1bc2266c7b90feb57d35995b1b57ffa23dcf6f7d8a2b8dad4a7238 name: conferer-version: 0.1.0.0-synopsis: Configuration management library +version: 0.1.0.1+synopsis: Configuration management library description: Library to abstract the parsing of many haskell config values from different config sources category: Configuration@@ -19,6 +19,9 @@ license: BSD3 license-file: LICENSE build-type: Simple+extra-doc-files:+ README.md+ LICENSE library exposed-modules:
src/Conferer.hs view
@@ -1,21 +1,94 @@+-- |+-- Module: Conferer+-- Copyright: (c) 2019 Lucas David Traverso+-- License: MIT+-- Maintainer: Lucas David Traverso <lucas6246@gmail.com>+-- Stability: experimental+-- Portability: portable+--+-- Types and functions for managing configuration effectively module Conferer- ( module Conferer.Core+ (++ -- * 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.FetchFromConfig.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.++ -- ** Getting configuration for existing libraries+ -- | There is a typeclass 'FetchFromConfig' 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!++ -- ** 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 'FetchFromConfig' (like+ -- 'Warp.Settings')+ --+ -- Internally a 'Config' is made of many 'Provider's which have a simpler+ -- interface:+ --+ -- @+ -- 'getKeyInProvider' :: Provider -> Key -> IO (Maybe Text)+ -- @+ --+ -- Most configuration providers 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 provider+ -- requires a string to namespace the env vars that can affect the+ -- configuration)+ --+ -- Once you have your 'Provider' you can add it to a 'Config' using the+ -- 'addProvider' function. One final note: each provider has a different+ -- priority, which depends on when is was added to the config ('Provider's+ -- added later have lower priority) so the config searches keys in providers+ -- in the same order they were added.++ module Conferer.Types+ , module Conferer.Core+ , defaultConfig+ , Key(..)++ -- * Providers , module Conferer.Provider.Env , module Conferer.Provider.Simple , module Conferer.Provider.Namespaced , module Conferer.Provider.Mapping , module Conferer.Provider.CLIArgs , module Conferer.Provider.Null- , defaultConfig- , Key(..)+ -- * Re-Exports , (&) ) where import Data.Text (Text) import Data.Function ((&)) -import Conferer.Core-import Conferer.Types+import Conferer.Core (emptyConfig, addProvider, getFromConfig, getKey, unsafeGetKey, (/.))+import Conferer.Types (Config, Key(..), ProviderCreator, Provider(..), FetchFromConfig) import Conferer.Provider.Env import Conferer.Provider.Simple import Conferer.Provider.Namespaced@@ -25,6 +98,8 @@ +-- | Default config which reads from command line arguments, env vars and+-- property files defaultConfig :: Text -> IO Config defaultConfig appName = do pure emptyConfig
src/Conferer/Core.hs view
@@ -6,14 +6,9 @@ import Conferer.Types -unsafeGetKey :: Key -> Config -> IO Text-unsafeGetKey k config =- either (error . Text.unpack) id <$> getKey k config--getFromConfig :: FetchFromConfig a => Key -> Config -> IO a-getFromConfig k config =- either (error . Text.unpack) id <$> fetch k config-+-- | Most Basic function to interact directly with a 'Config'. It always returns+-- 'Text' in the case of success and implements the logic to traverse+-- providers inside the 'Config'. getKey :: Key -> Config -> IO (Either Text Text) getKey k config = go $ providers config@@ -25,17 +20,32 @@ Just t -> return $ Right t Nothing -> go providers ++-- | Fetch a value from a config key that's parsed using the FetchFromConfig+-- instance.+--+-- This function throws an exception if the key is not found.+getFromConfig :: FetchFromConfig a => Key -> Config -> IO a+getFromConfig k config =+ either (error . Text.unpack) id <$> fetch k config++-- | 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 [] +-- | Instantiate a 'ProviderCreator' using the 'emptyConfig' mkStandaloneProvider :: ProviderCreator -> IO Provider mkStandaloneProvider mkProvider = mkProvider emptyConfig +-- | Instantiate a 'Provider' using an 'ProviderCretor' and a 'Config' and add+-- to the config addProvider :: ProviderCreator -> Config -> IO Config addProvider mkProvider config = do newProvider <- mkProvider config@@ -43,3 +53,8 @@ Config { providers = providers config ++ [ newProvider ] }++-- | Same as 'getKey' but it throws if the 'Key' isn't found+unsafeGetKey :: Key -> Config -> IO Text+unsafeGetKey k config =+ either (error . Text.unpack) id <$> getKey k config
src/Conferer/Provider/CLIArgs.hs view
@@ -1,4 +1,14 @@-module Conferer.Provider.CLIArgs where+module Conferer.Provider.CLIArgs+ (+ -- * Command line arguments Provider+ -- | This provider 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@+ mkCLIArgsProvider+ , mkCLIArgsProvider'+ , parseArgsIntoKeyValue+ )+where import Data.Text (Text) import qualified Data.Text as Text@@ -10,16 +20,20 @@ import Conferer.Provider.Simple +-- | Create a 'ProviderCreator' for CLIArgs from a argument list mkCLIArgsProvider' :: [String] -> ProviderCreator mkCLIArgsProvider' args = \config -> do let configMap = parseArgsIntoKeyValue args mkMapProvider configMap config +-- | Same as 'mkCLIArgsProvider'' but using 'getArgs' to provide the argument+-- list mkCLIArgsProvider :: ProviderCreator mkCLIArgsProvider = \config -> do args <- getArgs mkCLIArgsProvider' args config +-- | Parse an argument list into a dictionary suitable for a 'Provider' parseArgsIntoKeyValue :: [String] -> [(Key, Text)] parseArgsIntoKeyValue = fmap (\(k, s) -> (fromString $ Text.unpack k, s)) .
src/Conferer/Provider/Env.hs view
@@ -1,27 +1,42 @@-module Conferer.Provider.Env where+module Conferer.Provider.Env+ (+-- * Env Provider+-- | This 'Provider' 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 provider will try to lookup the env var called+-- @AWESOMEAPP_WARP_PORT@. +-- * Usage+-- | To use this provider simply choose a prefix and add it using the+-- 'addProvider' function like:+--+-- @+-- config & 'addProvider' ('mkEnvProvider' "awesomeapp")+-- @+ mkEnvProvider+ , mkEnvProvider'+ , Prefix+ , LookupEnvFunc+ , keyToEnvVar+ ) where++ import Data.Text (Text) import qualified Data.Text as Text import qualified System.Environment as System import Conferer.Types --type LookupEnvFunc = (String -> IO (Maybe String))---keyToEnvVar :: Prefix -> Key -> Text-keyToEnvVar prefix (Path keys) =- Text.toUpper- $ Text.intercalate "_"- $ filter (/= mempty)- $ prefix : keys--+-- | 'ProviderCreator' for env 'Provider' that uses the real 'System.lookupEnv'+-- function mkEnvProvider :: Prefix -> ProviderCreator mkEnvProvider prefix = mkEnvProvider' System.lookupEnv prefix +-- | 'ProviderCreator' for env 'Provider' that allows parameterizing the+-- function used to lookup for testing mkEnvProvider' :: LookupEnvFunc -> Prefix -> ProviderCreator mkEnvProvider' lookupEnv prefix = \_config -> return $@@ -30,3 +45,23 @@ let envVarName = Text.unpack $ keyToEnvVar prefix k fmap Text.pack <$> lookupEnv envVarName }++-- | Type alias for the function to lookup env vars+type LookupEnvFunc = String -> IO (Maybe String)+++-- | A text to namespace env vars+type Prefix = Text++-- | 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) =+ Text.toUpper+ $ Text.intercalate "_"+ $ filter (/= mempty)+ $ prefix : keys+
src/Conferer/Provider/Mapping.hs view
@@ -1,10 +1,21 @@-module Conferer.Provider.Mapping where+module Conferer.Provider.Mapping+ (+ -- * Namespaced higher-order provider+ -- | This provider takes a provider and returns a new provider that+ -- always transforms the key according to either a function for+ -- 'mkMappingProvider'' or a 'Map' for 'mkMappingProvider'+ mkMappingProvider+ , mkMappingProvider'+ )+where import Data.Map (Map) import qualified Data.Map as Map import Conferer.Types +-- | Create a 'ProviderCreator' using a function to transform the supplied keys+-- and another 'ProviderCreator' mkMappingProvider' :: (Key -> Maybe Key) -> ProviderCreator -> ProviderCreator mkMappingProvider' mapper providerCreator config = do configProvider <- providerCreator config@@ -16,6 +27,8 @@ Nothing -> return Nothing } +-- | Create a 'ProviderCreator' using a 'Map' 'Key' 'Key' to transform the supplied keys+-- and another 'ProviderCreator' mkMappingProvider :: Map Key Key -> ProviderCreator -> ProviderCreator mkMappingProvider configMap configProvider = mkMappingProvider' (`Map.lookup` configMap) configProvider
src/Conferer/Provider/Namespaced.hs view
@@ -1,9 +1,17 @@-module Conferer.Provider.Namespaced where+module Conferer.Provider.Namespaced+ (+ -- * Namespaced higher-order provider+ -- | This provider takes a provider and returns a new provider that+ -- always checks that the 'Key' given always starts with certain 'Key'+ -- and then strips that prefix before consulting its inner Provider+ mkNamespacedProvider+ ) where import Data.List (stripPrefix) import Conferer.Types +-- | Create a 'ProviderCreator' from a prefix and another 'ProviderCreator' mkNamespacedProvider :: Key -> ProviderCreator -> ProviderCreator mkNamespacedProvider (Path key) configCreator = \config -> do configProvider <- configCreator config
src/Conferer/Provider/Null.hs view
@@ -1,7 +1,14 @@-module Conferer.Provider.Null where+module Conferer.Provider.Null+ (+ -- * Does Nothing Provider+ -- | The stub provider that never has a key+ mkNullProvider+ )+where import Conferer.Types +-- | Create a null 'Provider' mkNullProvider :: ProviderCreator mkNullProvider _config = return $ Provider
src/Conferer/Provider/Simple.hs view
@@ -1,4 +1,12 @@-module Conferer.Provider.Simple where+module Conferer.Provider.Simple+ (+ -- * Simple Provider+ -- | This provider provides values from a hardcoded Map passed at creation+ -- time that can not be changed afterwards, it's mostly used as a necessary+ -- utility+ mkMapProvider+ , mkMapProvider'+ ) where import Data.Map (Map) import qualified Data.Map as Map@@ -6,6 +14,7 @@ import Conferer.Types +-- | Make a 'Provider' from a 'Map' mkMapProvider' :: Map Key Text -> ProviderCreator mkMapProvider' configMap _config = return $ Provider@@ -14,5 +23,6 @@ return $ Map.lookup k configMap } +-- | Make a 'Provider' from 'List' of 'Key', 'Text' pairs mkMapProvider :: [(Key, Text)] -> ProviderCreator mkMapProvider = mkMapProvider' . Map.fromList
src/Conferer/Types.hs view
@@ -5,29 +5,43 @@ import Data.Text (Text) import qualified Data.Text as Text +-- | 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 provider data Provider = Provider { getKeyInProvider :: Key -> IO (Maybe Text) } +-- | The way to index 'Provider's, basically list of names that will be adapted+-- to whatever the provider needs newtype Key = Path { unKey :: [Text] } deriving (Show, Eq, Ord) +-- | 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 providers data Config = Config { providers :: [Provider] } +-- | The type for creating a provider given a 'Config', some providers require a+-- certain configuration to be initialized (for example: the redis provider+-- needs connection info to connect to the server) type ProviderCreator = Config -> IO Provider +-- | Main typeclass for defining the way to get values from config, hiding the+-- 'Text' based nature of the 'Provider's+--+-- Here an error means that the value couldn't be parsed and that a reasonable+-- default was not possible. class FetchFromConfig a where fetch :: Key -> Config -> IO (Either Text a) instance IsString Key where fromString s = Path $ filter (/= mempty) $ Text.split (== '.') $ fromString s--type Prefix = Text