etc: Declarative configuration spec for Haskell projects

This is a package candidate release! Here you can preview how this package release will appear once published to the main package index (which can be accomplished via the 'maintain' link below). Please note that once a package has been published to the main package index it cannot be undone! Please consult the package uploading documentation for more information.

[maintain] [Publish]

Please see README.md


[Skip to Readme]

Properties

Versions 0.0.0.0, 0.0.0.0, 0.0.0.1, 0.0.0.2, 0.1.0.0, 0.2.0.0, 0.3.0.0, 0.3.1.0, 0.3.2.0, 0.4.0.0, 0.4.0.1, 0.4.0.2, 0.4.0.3, 0.4.1.0
Change log None available
Dependencies aeson (>=1.0 && <1.1), ansi-wl-pprint (>=0.6 && <0.7), base (>=4.7 && <5), bytestring (>=0.10 && <0.11), containers (>=0.5 && <0.6), directory (>=1.3 && <1.4), exceptions (>=0.8 && <0.9), hashable (>=1.2 && <1.3), optparse-applicative (>=0.13 && <0.14), protolude (>=0.1 && <0.2), text (>=1.2 && <1.3), unordered-containers (>=0.2 && <0.3), vector (>=0.11 && <0.12), yaml (>=0.8 && <0.9) [details]
License MIT
Copyright 2017 Roman Gonzalez
Author Roman Gonzalez
Maintainer romanandreg@gmail.com
Category Configuration, System
Home page https://github.com/roman/Haskell-etc
Source repo head: git clone https://github.com/roman/Haskell-etc
Uploaded by RomanGonzalez at 2017-04-03T07:35:35Z

Modules

Flags

Automatic Flags
NameDescriptionDefault
printer

Include support for config printer

Disabled
cli

Include support for cli arguments

Disabled
yaml

Include support to parse YAML files

Disabled

Use -f <flag> to enable a flag, or -f -<flag> to disable that flag. More info

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees


Readme for etc-0.0.0.0

[back to package description]

Build Status

etc

etc gathers configuration values from multiple sources (cli options, OS environment variables, files) using a declarative spec file that defines where this values are to be found and located in a configuration map.

Rationale

Independently of where the configuration values of your application are found, your application should use a consistent data structure to hold this values; also your business logic must not be concerned around where this values come from, nor decide what is the precedence of the sources of this values, instead, it should use an API that abstracts this details away and also allows to easily cast values to something that makes sense to the business logic (numbers, booleans, strings or complex records).

Defining a spec file

You need to use a spec file to define the structure of your application's configuration map; also if an entry value on this configuration map can have multiple input sources (environment variable, configuration files, command line option, etc), you can specify right there what this sources may be. The map can be defined using JSON or YAML; following an example in YAML format:

###
# These paths are going to be read for configuration values merging values
# from all of them, if they have the same keys, entries on (2) will have
# precedence over entries on (1)
etc/filepaths:
- ./resources/config.json # 1
- /etc/my-app/config.json # 2

###
# The program is going to have a Command Line interface
etc/cli:
  desc: "Description of the program that reads this configuration spec"
  header: "my-app - A program that has declarative configuration input",

  # The program is going to have 2 sub-commands
  commands:
    config:
      desc: "Prints configuration summary"
    run:
      desc: "Executes main program"

###
# With etc/entries we define the configuration map structure your
# application is going to be reading values from
etc/entries:
  credentials:
    username:
      # Define the spec for ["credentials", "username"]
      etc/spec:
        # default value (least precedence)
        default: "root"

        # if environment variable is defined, put its value in this entry
        env: "MY_APP_USERNAME"

        # cli input is going to have one option for this value
        cli:
          input: option
          metavar: USERNAME
          help: Username of the system
          type: string
          required: false
          # option is going to be available only on run sub-command
          commands:
            - run

    # Define the spec for ["credentials", "password"]
    password:
      etc/spec:
        env: "MY_APP_PASSWORD"
        cli:
          input: option
          metavar: PASSWORD
          help: "Password of user"
          type: string
          required: true
          commands:
            - run

The important keys to notice on the previous example:

Reading a spec file

To read a spec file you need to use the Etc.readConfigSpec function, this function can accept either a JSON or YAML filepath. You can also use the Etc.parseConfigSpec if you already gather the contents of a spec file from a different source.

YAML support

In order to allow etc to read from YAML files, you will need to use the yaml cabal flag when installing the library, here are some instructions on how to pass cabal flags using stack and cabal. We do this so that in case you want to stick with the JSON format, you don't have to pull dependencies you don't need.

Gathering configuration values explicitly

Even though a spec file defines where the configuration values can be found, etc won't collect those values unless it is explicitly told to do so. To do this you must use functions that will resolve these configuration sources.

Default

When defining the spec, you can specify default values on the etc/spec metadata entry. To get this values from the spec you must call the Etc.resolveDefault with the result from Etc.readConfigSpec as an argument.

Example

import qualified System.Etc as Etc

getConfiguration :: IO Etc.Config
getConfiguration = do
  spec <- Etc.readConfigSpec "./path/to/spec.yaml"
  return (Etc.resolveDefault spec)

Configuration Files

To get values from configuration files on your filesystem, you must specify an etc/filepaths entry on the spec file, this will tell etc to merge a list of configuration values from each path, the latter the filepath, the more precedence it has on the configuration map.

After this entry is defined in your spec, you must then call the Etc.resolveFiles function with the result of Etc.readConfigSpec as a parameter.

Why have more than one configuration file?

This helps to have a scheme of over-writable configurations on deployed applications, you can have the first path in the list of etc/filepaths entry be the config used while developing your app, and once deployed you can have production configuration values on a well known path (say /etc/my-app/config.yaml).

Example

import Data.Monoid ((<>))
import qualified System.Etc as Etc

getConfiguration :: IO Etc.Config
getConfiguration = do
  spec <- Etc.readConfigSpec "./path/to/spec.yaml"

  let
    defaultConfig =
      Etc.resolveDefault spec

  fileConfig <- Etc.resolveFiles spec

  return (fileConfig <> defaultConfig)

Environment Variables

When an env key is specified in the etc/spec metadata of a configuration value entry, etc will consider an environment variable with the given name.

After this entry is defined in your spec, you must then call the Etc.resolveEnv function with the result of Etc.readConfigSpec as a parameter.

Example

import Data.Monoid ((<>))
import qualified System.Etc as Etc

getConfiguration :: IO Etc.Config
getConfiguration = do
  spec <- Etc.readConfigSpec "./path/to/spec.yaml"

  let
    defaultConfig =
      Etc.resolveDefault spec

  fileConfig <- Etc.resolveFiles spec
  envConfig  <- Etc.resolveEnv spec

  return (fileConfig <> envConfig <> defaultConfig )

Command Line

You can setup a CLI input for your program by using the etc/cli entry at the root of the spec file, and the cli entry on the etc/spec metadata entries for configuration values.

When a cli key is specified in the etc/spec metadata of a configuration value entry, etc will consider inputs from a command line interface for your application.

opt/cli entries

The opt/cli entry map must have the following keys:

CLI entries

The cli entry map can have the following keys (input and type are required):

Using Plain resolver

When the commands key is not specified on the etc/cli entry of the spec file, you must use this resolver.

After the cli entry is defined in your spec, you must then call the Etc.resolvePlainCli function with the result of Etc.readConfigSpec as a parameter.

Example
import Data.Monoid ((<>))
import qualified System.Etc as Etc

getConfiguration :: IO Etc.Config
getConfiguration = do
  spec <- Etc.readConfigSpec "./path/to/spec.yaml"

  let
    defaultConfig =
      Etc.resolveDefault spec

  fileConfig <- Etc.resolveFiles spec
  envConfig  <- Etc.resolveEnv spec
  cliConfig  <- Etc.resolvePlainCli spec

  return (fileConfig
          <> cliConfig
          <> envConfig
          <> defaultConfig)

Using Command resolver

When the commands key is specified on the etc/cli entry of the spec file, you must use this resolver.

After the cli entry is defined in your spec, you must then call the Etc.resolveCommandCli function with the result of Etc.readConfigSpec as a parameter.

This will return a tuple with the chosen sub-command and the configuration map; the command type needs to be an instance of the Aeson.FromJSON, Aeson.ToJSON and Data.Hashable.Hashable typeclasses for the command to be parsed/serialized effectively.

Example
import GHC.Generics (Generic)
import Data.Hashable (Hashable)
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Types as JSON (typeMismatch)
import Data.Monoid ((<>))
import qualified System.Etc as Etc

data Cmd
  = Config
  | Run
  deriving (Show, Eq, Generic)

instance Hashable Cmd

instance JSON.FromJSON Cmd where
  parseJSON json =
    case json of
      JSON.String cmdName ->
        if cmdName == "config" then
          return Config
        else if cmdName == "run" then
          return Run
        else
          JSON.typeMismatch ("Cmd (" <> Text.unpack cmdName <> ")") json
      _ ->
        JSON.typeMismatch "Cmd" json

instance JSON.ToJSON Cmd where
  toJSON cmd =
    case cmd of
      Config ->
        JSON.String "config"
      Run ->
        JSON.String "run"

getConfiguration :: IO (Cmd, Etc.Config)
getConfiguration = do
  spec <- Etc.readConfigSpec "./path/to/spec.yaml"

  let
    defaultConfig =
      Etc.resolveDefault spec

  fileConfig <- Etc.resolveFiles spec
  envConfig  <- Etc.resolveEnv spec
  (cmd, cliConfig) <- Etc.resolveCommandCli spec

  return ( cmd
         , fileConfig
          <> cliConfig
          <> envConfig
          <> defaultConfig )

CLI Support

In order to allow etc to generate CLI inputs for your program, you will need to use the cli cabal flag when installing the library, here are some instructions on how to pass cabal flags using stack and cabal. We do this so that in case you are not interested in generating a CLI input for your program, you don't have to pull dependencies you don't need.

Reading from pure values

Sometimes, you would like to use the concept of CLI or environment variables, without actually calling the OS APIs, etc provides pure versions for these resolvers:

This work exactly the same as their non-pure counterparts, but receive one extra argument to fetch the required input.

Accessing Configuration Values

Internally, etc stores every value that it gathers from all sources like a JSON object (using the Data.Aeson.Value type), this provides a lot of flexibility around what value you can get from your configuration map, allowing your to use Aeson typeclasses to cast configuration values to more business logic data structures.

There are two functions that can be used to get values out from a configuration map:

Reads values specified on a spec file and casts it to a Haskell type using the Aeson.FromJSON typeclass

Reads values specified on a spec file and casts it using a custom function that uses the Aeson parser API; this works great when the data structures of libraries you use don't support Aeson or the format in your config file is not quite the same as the already implemented Aeson.FromJSON parser of a type given by a library.

An example of their usage is given in the full example section

Printing your configuration values

A lot of times you may want to assert where a configuration value is coming from, or if a particular environment variable was considered effectively by your program. You an use the Etc.printPrettyConfig function to render the configuration map and the different values/sources that were resolved when calculating it. This function is really useful for debugging purposes.

Cabal Flags

To reduce the amount of dependencies this library brings, you can choose the exact bits of functionality you need for your application.

Full Example

NOTE: This example uses the spec file stated above

import Control.Applicative ((<$>), (<*>))
import Data.Aeson ((.:))
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Types as JSON (typeMismatch)
import qualified System.Etc as Etc

data Credentials
  = Credentials { username :: Text
                , password :: Text }
  deriving (Show)

parseCredentials json =
  case json of
    JSON.Object object ->
      Credentials
        <$> object .: "user"
        <*> object .: "password"

getConfiguration :: IO Etc.Config
getConfiguration = do
  spec <- Etc.readConfigSpec "./path/to/spec.yaml"

  let
    defaultConfig =
      Etc.resolveDefault spec

  fileConfig <- Etc.resolveFiles spec
  envConfig  <- Etc.resolveEnv spec
  cliConfig  <- Etc.resolvePlainCli spec

  return (fileConfig
          <> cliConfig
          <> envConfig
          <> defaultConfig)

main :: IO ()
main = do
  config     <- getConfiguration

  -- Get individual entries (Uses instance of Text type for the Aeson.FromJSON
  -- typeclass)
  username <- Etc.getConfigValue ["credentials", "username"]

  -- Get the values with a supplied JSON parser
  creds <- Etc.getConfigValueWith parseCredentials ["credentials"]

  print (username :: Text)
  print creds