The problem

Have you ever tried configuring a Haskell application? If you are not the author you are usually out of luck and the only way to configure it is recompiling, and even if you are the author you have to write that logic yourself (reading env vars, files or cli params), what about partial updates? and environments? or error handling?

One solution: Conferer

Conferer is a library that defines ways of getting configuration for your Haskell application and the libraries it uses in a very ergonomical way.

Example: one Settings

Let’s say I want to configure a warp server, then we’d do:

main = do
  -- First we create a Config, which defines which sources our config will be
  -- reading, by default cli params, env vars and .properties files
  config <- defaultConfig "awesomeapp"
  -- Then we use getFromConfig with some arbitrary key (to scope the server
  -- config) and we use our Config to generate a Warp Settings
  warpConfig :: Warp.Settings <- getFromConfig "server" config

  -- Afterwards we use the Settings as usual
  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 --server.port=5555
  • Setting an environment variable called AWESOMEAPP_SERVER_PORT=5555
  • In a config/dev.properties file, you can have server.port=5555

And you may also get that value from different configuration sources like redis, json file, dhall file or whichever you may need.

Example 2: many different values with defaults

Let’s say I want to configure a warp server and a redis db (using hedis), To do that we’d do:


-- First we create our configuration record which holds all the configurations
-- our app needs
data AppConfig = AppConfig
  { appConfigWarp :: Settings
  -- ^- From Warp
  , appConfigHedis :: ConnectionInfo
  -- ^- From Hedis
  , appConfigSecret :: Text
  -- ^- Some custom value we need
  } deriving (Generic)
  -- ^- We need to derive Generic to derive FromConfig

-- This typeclass defines how to create our type from a bunch of string based
-- key/values, (which our Config is), for records we can derive it using
-- Generics
instance FromConfig AppConfig

-- Now we need a default value for our app, all apps should be able to work
-- at least somewhat stupidly even if the user doens't supply configurations
-- at all
instance DefaultConfig AppConfig where
  configDef = AppConfig
    { appConfigWarp = setPort 2222 configDef
    -- ^- We want the default Warp config but the port should be 2222
    --    if the config doesn't mention it
    , appConfigHedis = configDef 
    -- ^- defaults for hedis are ok
    , appConfigSecret = "very secret... shhh"
    -- ^- we decide some random default, notice that Text has no default
    --    so using configDef here won't compile
    }


main = do
  -- Like last time we create the config
  config <- defaultConfig "awesomeapp"
  -- Then we use getFromRootConfig without a key since Generics on AppConfig
  -- already scoped everything inside itself and we use our Config to
  -- generate an AppConfig
  warpConfig :: AppConfig <- getFromRootConfig config

  -- Afterwards we use the Settings as usual
  Warp.runSettings warpConfig myApp

Now to configure our app we can use the same sources as before (env vars, cli, files, etc) but using the following flags we can configure:

  • --warp.port=5555: set warp’s server port to 5555
  • --secret=real_secrets: set our custom secret to "real_secrets"
  • --hedis=redis://username:password@host:42/2: set hedis’ connection to that
  • --hedis.host=redis.example.com: set hedis’ connection host to redis.example.com

Existing sources

Sources usually incur in many dependencies so they are split into different packages

  • Json (depends on aeson)
  • Dhall (depends on dhall)
  • Yaml (depends on yaml)

Existing FromConfig instances

Default instances for fetching a values from a config (usually a config value for some library)

Utilities

There are as well some utilities to change sources:

  • Conferer.Source.Namespace: All keys must be namespaced and the namespace is striped for lookup
  • Conferer.Source.Mapped: Using a map key to maybe key you can change the name of a key or even hiding some key
  • Conferer.Source.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) even in different levels of configuration
  • A LOT of sources
  • A LOT of FromConfig implementations

Changes

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to PVP.

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 Keys
  • 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