Coder Social home page Coder Social logo

haskell-etc's Introduction

Build Status CircleCI Github Hackage Hackage Dependencies Stackage LTS Stackage Nightly

etc

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

Table Of Contents

Raison d'etre

etc is a configuration management that:

  • Allows to have a versioned spec of all values your application can accept

  • Provides documentation about Environment Variables used by your application

  • Provides an API for gathering values from multiple sources (files, overwrite files, cli arguments, environment variables) and then composing them into a single configuration map

  • Gives a sane precedence over sources of the configuration value sources

  • Provides inspection utilities to understand why the configuration is the way it is

  • Provides an API that abstracts away the source of configuration values and allows easy casting into record types your application or other libraries understand

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"
      header: ""
    run:
      desc: "Executes main program"
      header: ""

###
# 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:
        type: string
        # 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
          required: false
          # option is going to be available only on run sub-command
          commands:
          - run

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

The important keys to notice on the previous example:

  • etc/filepaths tells where to look for files to gather the configuration of your app, it could be more than one file because you may want to have a default file for development, and then override it with some configurations for production/integration, The further the filepath is the higher precedence its values are going to have.

  • etc/entries specifies how your configuration map is going to look like and how your business logic will be accessing it

  • etc/spec provide means to define metadata for a configuration value entry, what is its default value, if it can be found via an environment variable, or if it may be specified as an CLI option/argument input.

Reading a spec file

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

NOTE: When using System.Etc.parseConfigSpec or System.Etc.readConfigSpec and the CLI cabal feature flag is true, unless you use the System.Etc.resolveCommandCli function, you will have to explicitly declare the ConfigSpec type parameter.

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 System.Etc.resolveDefault with the result from System.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 System.Etc.resolveFiles function with the result of System.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 (mappend)
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, _fileWarnings) <- Etc.resolveFiles spec

  return (fileConfig `mappend` 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 System.Etc.resolveEnv function with the result of System.Etc.readConfigSpec as a parameter.

Example

import Data.Monoid (mappend)
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, _fileWarnings) <- Etc.resolveFiles spec
  envConfig  <- Etc.resolveEnv spec

  return (fileConfig `mappend` envConfig `mappend` 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:

  • desc: A one line description of what your application does

  • header: The header used when getting the information from the auto-generated --help option

  • commands: A map of sub-commands that this program can have; each entry is the name of the sub-command, and the value is a map with the key desc with the same purpose as the top-level desc entry defined above.

    NOTE: you must use System.Etc.resolveCommandCli for the commands entry to take effect

cli entries

The cli entry map can have the following keys (input is required):

  • required: specifies if the entry is required on the CLI

  • input: how you want to receive the input value, it can either be argument or option

  • metavar: the name of the input argument on the example/documentation string of the CLI help

  • long (only available on option inputs): the name of the option in long form (e.g. --name)

  • short (only available on option inputs): the name of the option in short form (.e.g -n)

  • commands: A list of sub-commands that are going to have this option/argument available; make sure the commands listed here are also listed in the etc/cli entry of your spec file.

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 System.Etc.resolvePlainCli function with the result of System.Etc.readConfigSpec as a parameter.

Example
import Data.Monoid (mappend)
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, _fileWarnings) <- Etc.resolveFiles spec
  envConfig  <- Etc.resolveEnv spec
  cliConfig  <- Etc.resolvePlainCli spec

  return (fileConfig
          `mappend` cliConfig
          `mappend` envConfig
          `mappend` 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 System.Etc.resolveCommandCli function with the result of System.Etc.readConfigSpec as a parameter.

This will return a tuple with the chosen sub-command and the configuration map; the command Haskell 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 (mappend)
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 (" `mappend` Text.unpack cmdName `mappend` ")") 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

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

  return ( cmd
         , fileConfig
          `mappend` cliConfig
          `mappend` envConfig
          `mappend` 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 sources

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:

  • System.Etc.resolveEnvPure

  • System.Etc.resolvePlainCliPure

  • System.Etc.resolveCommandCliPure

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:

  • System.Etc.getConfigValue

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

  • System.Etc.getConfigValueWith

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 System.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.

Example

Here is the output of one of the example applications:

$ MY_APP_USERNAME=foo etc-command-example run -u bar -p 123
Executing main program
credentials.username
  bar        [ Cli        ]
  foo        [ Env: MY_APP_USERNAME ]
  root       [ Default    ]

credentials.password
  123        [ Cli        ]

The output displays all the configuration values and their sources, the first value on the list is the value that System.Etc.getConfigValue returns for that particular key.

Report Misspellings on Environment Variables

When you define env keys on the etc/entries map of your spec file, we can infer what are the valid Environment Variables that need to be defined for your application, knowing this, etc can infer when there is a typo on one of this environment variables and report this. You need to have the extra cabal flag and call the System.Etc.reportEnvMisspellingWarnings with the configuration spec as as an argument.

Example

Here is an example of the output this function prints to stderr when the given Environment Variables are almost identical to the ones found on the spec file:

$ MY_AP_USERNAME=foo etc-command-example run -u bar -p 123

WARNING: Environment variable `MY_AP_USERNAME' found, perhaps you meant `MY_APP_USERNAME'

Cabal Flags

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

  • yaml: Allows (in addition of JSON) to have spec file and configuration files in YAML format

  • cli: Provides the CLI functionality explained in this README

  • extra: Provides helper functions for inspecting the resolved configuration as well as providing warning messages for misspelled environment variables

Full Example

NOTE: This example uses the spec file stated above

import Control.Applicative ((<$>), (<*>))
import Data.Aeson ((.:))
import Data.Hashable (Hashable)
import Data.Monoid (mappend)
import GHC.Generics (Generic)

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)

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 (" `mappend` Text.unpack cmdName `mappend` ")") json
      _ ->
        JSON.typeMismatch "Cmd" json

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

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

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

  Etc.reportEnvMisspellingWarnings spec

  let
    defaultConfig =
      Etc.resolveDefault spec

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

  return ( cmd
         , fileConfig
          `mappend` cliConfig
          `mappend` envConfig
          `mappend` defaultConfig )

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

  case cmd of
    Config -> do
      Etc.printPrettyConfig config

    Run -> do
      -- 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

haskell-etc's People

Contributors

locallycompact avatar qrilka avatar roman avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

haskell-etc's Issues

Reporting invalid input

I've just started using this, and the experience so far has been great - thank you! A few onboarding observations, mostly around how invalid input is handled at runtime. There seems opportunity for making error messages more helpful for the end user:

Yaml files

If there's a problem parsing a user-supplied yaml file, then providing the filename in question would make for a more helpful error message. eg if an app update is incompatible with some option lurking in a pre-existing config file, then even the existence of that file probably isn't in the user's mind, so it won't be obvious that they should go look in that file to fix the problem. (It wasn't for me :)

CLI arguments

Similarly, if there's a problem parsing a CLI argument, then having the argument and metavar would help provide a more helpful error message at low developer effort. For example, suppose I have a config type including

data Verbosity = Debug | Info | Warning | Error deriving Generic

and just a generic JSON parser, but the user enters -v warn (rather than -v warning, which is what the generic decoder expects). Then the resulting message

Error in $.verbosity: The key "warn" was not found

is a bit cryptic. One could manually supply a JSON parser with better error message in each case. But assuming the provided metavar is already meaningful, one could imagine the generic infra creating an error (maybe via a helper function, rather than literally the show instance for the type) more like:

Couldn't understand argument "-v warn" (expecting "-v {debug|info|warning|error}"):
The key "warn" was not found

This is at least piecing together all the relevant information already available.

ConfigurationErrors

This is a very low priority observation. But FWIW: with the app I migrated to use etc, ConfigurationFileNotFound is totally benign, not worthy of even a warning to the user, and all other ConfigurationErrors are totally fatal. It's obviously easy to filter accordingly. But the rest of the config load code is so compact, that I wonder if it might be worth something built-in (eg an annotation to etc/paths saying whether absence of any file should constitute an error) to make this even smoother. One day.

Other

I couldn't get the switch CLI type to work (assuming this is how you do something like grep -v, rather than grep -v true using an explicit bool), so an example would be handy.

Better error message when etc/spec.cli doesn't contains a `commands` key

Currently, when we have a config spec like the following:

etc/cli:
  desc: "Some Desc"
  header: "Some Header"
  commands:
    run:
      desc: "some desc"
      header: "some header"

etc/entries:
  username:
    etc/spec:
      default: "etc-user"
      cli:
        input: "option"
        short: "u"  

It reports an error: CommandKeyMissing, when instead it should report:

ERROR - Invalid Configuration Spec:

  Entry `etc/entries.username.etc/spec.cli` does not contain a `commands` key, you may want to add one like the following:

  > etc/entries:
  >   ...
  >   username:
  >     etc/spec:
  >       ...
  >       cli:
  >          commands:
  >          - run

  Also, make sure you use `resolveCommandCli` instead of `resolvePureCli`

Report duplicated entries on cli options

Description

When using CLI options on the spec/entries keys, validate that every CLI long and short are unique, if this is not the case, throw an exception on the resolveCli functions

Example for Maybe values

I've been trying to define configuration field that has no default value (Nothing). I know aeson can parse Maybe a values, but I always seem to get an InvalidConfigKeyPath rather than a Maybe a even though it compiles:

maybePassword <- Etc.getConfigValue ["irc", "password"] config
case maybePassword of
    Just password -> write "PASS" password
    Nothing -> return ()

I've tried specifying default: null in my spec.yaml but that doesn't seem to help.

Is there a way to handle completely optional configuration values? Would it be worth adding an example to the documentation?


Unsure if it's related, but I needed to add an explicit type annotation for Etc.readConfigSpec:

getConfiguration :: IO Etc.Config
getConfiguration = do
    path <- Text.pack <$> getDataFileName "resources/spec.yaml"
    spec <- Etc.readConfigSpec path :: IO (Etc.ConfigSpec JSON.Object)
    (configFiles, _fileWarnings) <- Etc.resolveFiles spec
    return $ configFiles `mappend` Etc.resolveDefault spec

`metavar` isn't displayed with `--help`

Not sure if it's me doing something wrong, but it looks like metavar in specified in cli doesn't get displayed when invoked with --help, or failed parsing for that matter.

It is critical for positional arguments, otherwise there is no way to know why parsing has failed or how to properly use the command.

  foo:
    etc/spec:
      type: number
      cli:
        input: option
        metavar: FOO_METAVAR
        required: true
        long: foo
        help: "foo does something cool, but doesn't have it FOO_METAVAR displayed"
        commands:
        - run

Results in:

$ my-cmd run --help
Usage: my-cmd run --foo ARG

Available options:
  -h,--help                Show this help text
  --foo ARG                foo does something cool, but doesn't have it
                           FOO_METAVAR displayed

Throw error when same option is used in multiple entries

Context

When specifying multiple entries in a configuration map with the same CLI configuration, a warning or error should be displayed

Input

hello:
  etc/spec:
    cli:
      input: option
      long: hello
      command:
      - one
hola:
  etc/spec:
    cli:
      input: option
      long: hello
      command:
      - one

Both hello and hola have the exact same CLI configuration, this should throw an error

Remove cabal data-files from examples

Currently, some of the examples make use of the data-files feature in cabal to lookup the configuration spec. This causes confusion on the on-boarding process of the library given this is a feature that can fail in certain scenarios (ghci and whatnot).

Replace this approach with the embed-file project

Add support for rio-0.1

There were some changes in the module exports (they are saner now).

  • Change the stack.yaml to support rio-0.1
  • Make testsuite pass

Add Config Printer without colors

Context

Currently, when logging a Config using the Printer API, we log color codes in the output, this is not desirable. By providing a renderConfig and a renderConfigColor, we make sure that we can add colors to output in an explicit manner

Value of type bool always falls back to CLI and sets it to False if it hasn't been passed

If there is a value of type bool, the resolver would always try to read it from args passed via CLI. If it is not there, it would set it to False. This behaviour is undesirable if you wish to specify the value via configuration file.

Expected behaviour

  • The library is not trying to resolve the bool value in CLI args.
  • The library does not set the value to False if it was not found in CLI args.

Support config values to have object types

Hi, I'd like to be able to do something like

aliases:
  etc/spec:
    type: "object"
    cli:
      commands:
      - foo

Where aliases is an object in the aeson sense - a hashmap that may change between configurations. It is naturally impractical to supply an hashmap via environment variable or command line so this would be file config only. Any thoughts?

Trouble running examples

Hi when I build the examples and try to run them I get the following.

[lc@ashenvale etc-plain-example]$ etc-plain-example -u foo -p foo
etc-plain-example: InvalidConfiguration Nothing "Error in $['etc/entries'].credentials.cli: cli option contains invalid key \"type\""

Thanks

Basic config value validation

This is just an idea, but it could be valuable.
Have a small collection of declarative validators that can be specified directly in the spec. Validators are simply functions that evaluate to Bool and are applied to the value that was parsed successfully. For example range or compare for numbers and other Ord things, length for strings and lists, eg:

    port:
      etc/spec:
        default: 3000
        type: number
        validators:
        - lte:
            value: 0
            error: "Ports with non-positive values don't make sense"
        - gt:
            value: 65535
            error: "Port numbers are 16-bit"
        - lt:
            value: 1024
            warn: "Using system level port, root privilege will be necessary"

Allow to have filesource coming from environment variables

Context

When specifying a filepath source, allow fetching one from an environment variable, this value should have the highest precedence over others (e.g. should be indexed last)

Implementation Details

  • Enhance the File Config Entry to have a env var source field
  • Update syntax for spec filepaths entry

Supported entries:

without env var entry

etc/filepaths:
- file1
- file2

with env var entry

etc/files:
  env: PROJECT_CONFIG_FILE
  paths:
  - path1
  - path2 

Improve documentation for the CLI spec

For the input value, use something similar to this example:

With my-app --foo the --foo is an input of type switch (from what I gather, it does not have a value associated to the flag), and it translates to a Boolean in the end (was --foo specified?)

With my-app --foo bar the --foo is an input of type option

With my-app bar, the input is of type argument as it is a positional argument

I think there is a limitation in regards on how the positional arguments order is decided (right now it could be rather arbitrary)

For the metavar, use something similar to this:

The metavar value is to change the name of an input in the --help message, when you do a --help and you have a command with option --option that has a metavar with METAVAR_VALUE_IS_THIS it will display a message somewhat like:

program header - some header description

--option METAVAR_VALUE_IS_THIS

Add `help` support to `argument` in cli

It be very nice if positional argument also had help, just as option and switch do. It only really makes sense when metavar is supplied, otherwise, if I remember correctly, optparse-applicative won't really be able to display it nicely. Which means that #64 is a blocker for this issue.

Allow validation of keys coming from Config Files

Context

Currently, etc will aggregate all the configuration values that come from different configuration files, this has the benefit of being open around defining config entries, without necessarily having to define them in the spec.

However, when we have all the configuration values that we are expecting defined on the spec, and we read config files that have typos on those keys, we don't get notified appropriately.

Requirements

Have a resolveFile function that is strict on the keys from the spec, meaning, if the key is not defined in the spec, fail fast and throw an exception.

Why do you need to read a spec file at runtime?

I'm trying to understand why this library has runtime file dependencies. It seems like you could just construct an element of ConfigSpec in your Haskell source and avoid the trouble of locating files at runtime and including Aeson as a (massive) dependency. What's the advantage to having a spec file? Or am I misunderstanding something?

fails to build with aeson-2.0

etc                            > Building library for etc-0.4.1.0..
etc                            > [ 1 of 13] Compiling Paths_etc
etc                            > [ 2 of 13] Compiling System.Etc.Internal.Spec.Types
etc                            > 
etc                            > /tmp/stack-118e294368372c73/etc-0.4.1.0/src/System/Etc/Internal/Spec/Types.hs:50:10: warning: [-Wmissing-methods]
etc                            >     • No explicit implementation for
etc                            >         ‘liftTyped’
etc                            >     • In the instance declaration for ‘Lift CliOptMetadata’
etc                            >    |
etc                            > 50 | instance Lift CliOptMetadata where
etc                            >    |          ^^^^^^^^^^^^^^^^^^^
etc                            > 
etc                            > /tmp/stack-118e294368372c73/etc-0.4.1.0/src/System/Etc/Internal/Spec/Types.hs:73:10: warning: [-Wmissing-methods]
etc                            >     • No explicit implementation for
etc                            >         ‘liftTyped’
etc                            >     • In the instance declaration for ‘Lift CliArgMetadata’
etc                            >    |
etc                            > 73 | instance Lift CliArgMetadata where
etc                            >    |          ^^^^^^^^^^^^^^^^^^^
etc                            > 
etc                            > /tmp/stack-118e294368372c73/etc-0.4.1.0/src/System/Etc/Internal/Spec/Types.hs:92:10: warning: [-Wmissing-methods]
etc                            >     • No explicit implementation for
etc                            >         ‘liftTyped’
etc                            >     • In the instance declaration for ‘Lift CliSwitchMetadata’
etc                            >    |
etc                            > 92 | instance Lift CliSwitchMetadata where
etc                            >    |          ^^^^^^^^^^^^^^^^^^^^^^
etc                            > 
etc                            > /tmp/stack-118e294368372c73/etc-0.4.1.0/src/System/Etc/Internal/Spec/Types.hs:109:10: warning: [-Wmissing-methods]
etc                            >     • No explicit implementation for
etc                            >         ‘liftTyped’
etc                            >     • In the instance declaration for ‘Lift CliEntryMetadata’
etc                            >     |
etc                            > 109 | instance Lift CliEntryMetadata where
etc                            >     |          ^^^^^^^^^^^^^^^^^^^^^
etc                            > 
etc                            > /tmp/stack-118e294368372c73/etc-0.4.1.0/src/System/Etc/Internal/Spec/Types.hs:124:10: warning: [-Wmissing-methods]
etc                            >     • No explicit implementation for
etc                            >         ‘liftTyped’
etc                            >     • In the instance declaration for ‘Lift (CliEntrySpec cmd)’
etc                            >     |
etc                            > 124 | instance Lift cmd => Lift (CliEntrySpec cmd) where
etc                            >     |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
etc                            > 
etc                            > /tmp/stack-118e294368372c73/etc-0.4.1.0/src/System/Etc/Internal/Spec/Types.hs:140:10: warning: [-Wmissing-methods]
etc                            >     • No explicit implementation for
etc                            >         ‘liftTyped’
etc                            >     • In the instance declaration for ‘Lift CliCmdSpec’
etc                            >     |
etc                            > 140 | instance Lift CliCmdSpec where
etc                            >     |          ^^^^^^^^^^^^^^^
etc                            > 
etc                            > /tmp/stack-118e294368372c73/etc-0.4.1.0/src/System/Etc/Internal/Spec/Types.hs:156:10: warning: [-Wmissing-methods]
etc                            >     • No explicit implementation for
etc                            >         ‘liftTyped’
etc                            >     • In the instance declaration for ‘Lift (ConfigSources cmd)’
etc                            >     |
etc                            > 156 | instance Lift cmd => Lift (ConfigSources cmd) where
etc                            >     |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
etc                            > 
etc                            > /tmp/stack-118e294368372c73/etc-0.4.1.0/src/System/Etc/Internal/Spec/Types.hs:200:10: warning: [-Wmissing-methods]
etc                            >     • No explicit implementation for
etc                            >         ‘liftTyped’
etc                            >     • In the instance declaration for ‘Lift (ConfigValue cmd)’
etc                            >     |
etc                            > 200 | instance Lift cmd => Lift (ConfigValue cmd) where
etc                            >     |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
etc                            > 
etc                            > /tmp/stack-118e294368372c73/etc-0.4.1.0/src/System/Etc/Internal/Spec/Types.hs:216:10: warning: [-Wmissing-methods]
etc                            >     • No explicit implementation for
etc                            >         ‘liftTyped’
etc                            >     • In the instance declaration for ‘Lift CliProgramSpec’
etc                            >     |
etc                            > 216 | instance Lift CliProgramSpec where
etc                            >     |          ^^^^^^^^^^^^^^^^^^^
etc                            > 
etc                            > /tmp/stack-118e294368372c73/etc-0.4.1.0/src/System/Etc/Internal/Spec/Types.hs:231:10: warning: [-Wmissing-methods]
etc                            >     • No explicit implementation for
etc                            >         ‘liftTyped’
etc                            >     • In the instance declaration for ‘Lift FilesSpec’
etc                            >     |
etc                            > 231 | instance Lift FilesSpec where
etc                            >     |          ^^^^^^^^^^^^^^
etc                            > 
etc                            > /tmp/stack-118e294368372c73/etc-0.4.1.0/src/System/Etc/Internal/Spec/Types.hs:251:10: warning: [-Wmissing-methods]
etc                            >     • No explicit implementation for
etc                            >         ‘liftTyped’
etc                            >     • In the instance declaration for ‘Lift (ConfigSpec cmd)’
etc                            >     |
etc                            > 251 | instance Lift cmd => Lift (ConfigSpec cmd) where
etc                            >     |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
etc                            > [ 3 of 13] Compiling System.Etc.Internal.Errors
etc                            > [ 4 of 13] Compiling System.Etc.Internal.Spec.Parser
etc                            > 
etc                            > /tmp/stack-118e294368372c73/etc-0.4.1.0/src/System/Etc/Internal/Spec/Parser.hs:108:37: error:
etc                            >     • Couldn't match type: Data.Aeson.KeyMap.KeyMap JSON.Value
etc                            >                      with: HashMap Text v1
etc                            >       Expected: HashMap Text v1
etc                            >         Actual: JSON.Object
etc                            >     • In the first argument of ‘HashMap.keys’, namely ‘object’
etc                            >       In the first argument of ‘forM_’, namely ‘(HashMap.keys object)’
etc                            >       In the first argument of ‘($)’, namely
etc                            >         ‘forM_ (HashMap.keys object)’
etc                            >     |
etc                            > 108 |                 forM_ (HashMap.keys object) $ \key ->
etc                            >     |                                     ^^^^^^
etc                            > 
etc                            > /tmp/stack-118e294368372c73/etc-0.4.1.0/src/System/Etc/Internal/Spec/Parser.hs:115:37: error:
etc                            >     • Couldn't match type: Data.Aeson.KeyMap.KeyMap JSON.Value
etc                            >                      with: HashMap Text v2
etc                            >       Expected: HashMap Text v2
etc                            >         Actual: JSON.Object
etc                            >     • In the first argument of ‘HashMap.keys’, namely ‘object’
etc                            >       In the first argument of ‘forM_’, namely ‘(HashMap.keys object)’
etc                            >       In the first argument of ‘($)’, namely
etc                            >         ‘forM_ (HashMap.keys object)’
etc                            >     |
etc                            > 115 |                 forM_ (HashMap.keys object) $ \key ->
etc                            >     |                                     ^^^^^^
etc                            > 
etc                            > /tmp/stack-118e294368372c73/etc-0.4.1.0/src/System/Etc/Internal/Spec/Parser.hs:122:37: error:
etc                            >     • Couldn't match type: Data.Aeson.KeyMap.KeyMap JSON.Value
etc                            >                      with: HashMap Text v3
etc                            >       Expected: HashMap Text v3
etc                            >         Actual: JSON.Object
etc                            >     • In the first argument of ‘HashMap.keys’, namely ‘object’
etc                            >       In the first argument of ‘forM_’, namely ‘(HashMap.keys object)’
etc                            >       In the first argument of ‘($)’, namely
etc                            >         ‘forM_ (HashMap.keys object)’
etc                            >     |
etc                            > 122 |                 forM_ (HashMap.keys object) $ \key ->
etc                            >     |                                     ^^^^^^
etc                            > 
etc                            > /tmp/stack-118e294368372c73/etc-0.4.1.0/src/System/Etc/Internal/Spec/Parser.hs:221:40: error:
etc                            >     • Couldn't match type: Data.Aeson.KeyMap.KeyMap JSON.Value
etc                            >                      with: HashMap k1 JSON.Value
etc                            >       Expected: HashMap k1 JSON.Value
etc                            >         Actual: JSON.Object
etc                            >     • In the second argument of ‘HashMap.lookup’, namely ‘object’
etc                            >       In the expression: HashMap.lookup "etc/spec" object
etc                            >       In the expression:
etc                            >         case HashMap.lookup "etc/spec" object of
etc                            >           Nothing
etc                            >             -> do subConfigMap <- foldM
etc                            >                                     (\ subConfigMap (key, value) -> ...) HashMap.empty
etc                            >                                     (HashMap.toList object)
etc                            >                   if HashMap.null subConfigMap then
etc                            >                       fail "Entries cannot have empty maps as values"
etc                            >                   else
etc                            >                       return (SubConfig subConfigMap)
etc                            >           Just (JSON.Object fieldSpec)
etc                            >             -> if HashMap.size object == 1 then
etc                            >                    do let ...
etc                            >                       ....
etc                            >                else
etc                            >                    fail "etc/spec object can only contain one key"
etc                            >           Just _ -> fail "etc/spec value must be a JSON object"
etc                            >     |
etc                            > 221 |         case HashMap.lookup "etc/spec" object of
etc                            >     |                                        ^^^^^^
etc                            > 
etc                            > /tmp/stack-118e294368372c73/etc-0.4.1.0/src/System/Etc/Internal/Spec/Parser.hs:229:41: error:
etc                            >     • Couldn't match type: Data.Aeson.KeyMap.KeyMap JSON.Value
etc                            >                      with: HashMap Text JSON.Value
etc                            >       Expected: HashMap Text JSON.Value
etc                            >         Actual: JSON.Object
etc                            >     • In the first argument of ‘HashMap.toList’, namely ‘object’
etc                            >       In the third argument of ‘foldM’, namely ‘(HashMap.toList object)’
etc                            >       In a stmt of a 'do' block:
etc                            >         subConfigMap <- foldM
etc                            >                           (\ subConfigMap (key, value)
etc                            >                              -> do innerValue <- JSON.parseJSON value
etc                            >                                    return $ HashMap.insert key innerValue subConfigMap)
etc                            >                           HashMap.empty (HashMap.toList object)
etc                            >     |
etc                            > 229 |                         (HashMap.toList object)
etc                            >     |                                         ^^^^^^
etc                            > 
etc                            > /tmp/stack-118e294368372c73/etc-0.4.1.0/src/System/Etc/Internal/Spec/Parser.hs:237:29: error:
etc                            >     • Couldn't match type: Data.Aeson.KeyMap.KeyMap JSON.Value
etc                            >                      with: HashMap k0 v0
etc                            >       Expected: HashMap k0 v0
etc                            >         Actual: JSON.Object
etc                            >     • In the first argument of ‘HashMap.size’, namely ‘object’
etc                            >       In the first argument of ‘(==)’, namely ‘HashMap.size object’
etc                            >       In the expression: HashMap.size object == 1
etc                            >     |
etc                            > 237 |             if HashMap.size object == 1 then do
etc                            >     |                             ^^^^^^
etc                            > 
etc                            > /tmp/stack-118e294368372c73/etc-0.4.1.0/src/System/Etc/Internal/Spec/Parser.hs:240:81: error:
etc                            >     • Couldn't match type: Data.Aeson.KeyMap.KeyMap JSON.Value
etc                            >                      with: HashMap k2 a
etc                            >       Expected: HashMap k2 a
etc                            >         Actual: JSON.Object
etc                            >     • In the second argument of ‘HashMap.lookup’, namely ‘fieldSpec’
etc                            >       In the second argument of ‘($)’, namely
etc                            >         ‘HashMap.lookup "default" fieldSpec’
etc                            >       In the expression:
etc                            >         maybe Nothing Just $ HashMap.lookup "default" fieldSpec
etc                            >     • Relevant bindings include
etc                            >         mDefaultValue :: Maybe a
etc                            >           (bound at src/System/Etc/Internal/Spec/Parser.hs:240:19)
etc                            >     |
etc                            > 240 |               let mDefaultValue = maybe Nothing Just $ HashMap.lookup "default" fieldSpec
etc                            >     |                                                                                 ^^^^^^^^^

Allow passing ParserPrefs for optparse-applicative

Context

The current release only allows to generate optparse-applicative parsers with default parsing preferences, provide functions for both Cli and Plain optparse resolvers to pass optparse-applicative ParserPrefs records

Allow the CliProgramOpts record to contain an Aeson.Value that later can be parsed to a ParserPrefs with default values, that way, the CLI options are part of the file spec

Fix type field on etc/entries cli

etc/entries:
  dev:
    build-db:
      etc/spec:
        default: false
        cli:
          input: option
          type: switch
          required: false
          long: dev-build-db
          metavar: MIGRATE AND SEED

upon compile, will result in:

InvalidConfiguration Nothing "Error in $['etc/entries'].dev.cli: cli option contains invalid key \"type\"", despite what is described here https://github.com/roman/Haskell-etc#cli-entries

This was also mentioned in issue #37 under "Other".

Might also be related to the change discussed in issue #38

PS - Really enjoying the library @roman, thanks!

Stackage LTS-14.0 failure

I have the YAML and CLI flags enabled.

etc               >     Ambiguous occurrence `exitSuccess'
etc               >     It could refer to either `RIO.exitSuccess',
etc               >                              imported from `RIO' at src\System\Etc\Internal\Resolver\Cli\Common.hs:10:1-20
etc               >                              (and originally defined in `rio-0.1.11.0:RIO.Prelude.Exit')
etc               >                           or `System.Exit.exitSuccess',
etc               >                              imported from `System.Exit' at src\System\Etc\Internal\Resolver\Cli\Common.hs:21:1-18
etc               >     |
etc               > 151 |       exitSuccess
etc               >     |       ^^^^^^^^^^^
etc               >
etc               > src\System\Etc\Internal\Resolver\Cli\Common.hs:155:7: error:
etc               >     Ambiguous occurrence `exitWith'
etc               >     It could refer to either `RIO.exitWith',
etc               >                              imported from `RIO' at src\System\Etc\Internal\Resolver\Cli\Common.hs:10:1-20
etc               >                              (and originally defined in `rio-0.1.11.0:RIO.Prelude.Exit')
etc               >                           or `System.Exit.exitWith',
etc               >                              imported from `System.Exit' at src\System\Etc\Internal\Resolver\Cli\Common.hs:21:1-18
etc               >     |
etc               > 155 |       exitWith exitCode
`

Add template haskell function to parse config spec

Using Template Haskell, provide a function that can read a JSON FilePath, parse the config spec and lift the config spec in the code.

This utility will allow users to validate the config spec at compile time, and not at runtime.

Allow environment variables in default values

Is not uncommon to require some default configuration values to expand from environment variables (e.g. $HOME)

Implementation Suggestions:

  • Add a new function resolveDefaultEnv that makes use of the environment variables
  • Interpolates the default values; the API detects that a ${VAL} is being used in value and it interpolates it with the correct value

Add warning when expected key is not present on spec file

Context

When using the resolveFiles function, there must be an entry on the ConfigSpec for file sources, if there is one with a similar name, a warning should be displayed so that people know why config values are not being loaded.

CLI entry with string type does not accept numbers

Current

When having a configuration spec like the following:

etc/entries:
    amount:
      etc/spec:
        type: "string"
        required: true
        cli:
          input: "argument"
          metavar: "AMOUNT"

And then call the program with:

program 100

It will complain with the error message invalid input.

Expected

It should work as expected

Implementation notes:

Add support for dhall

Ideally, we would like to be able to parse dhall files. We should experiment using the dhall-json project and figure out how much of dhall can be supported without much effort.

Allow Printer API to work with `Array` values

Context

When having configuration values in array form, we should display a listing of all configuration with the index as key, currently it fails with an InvalidConfiguration error

Give `ConfigValue` records with `JSON.Null` less precedence than others

Context

Currently, if we have a configuration value coming from a File, it will always take precedence over a Default value, despite the fact that the File value might be JSON.Null. We need to make sure that in case we get JSON.Null values, the lower precedence values should take more precedence over them.

Keep JSON data in the Spec Records

Context

To achieve #43, we have to drop closed ADTs that manage the known back-ends to etc (e.g. cli)

To support this, we are going to keep the original JSON on each Spec.ConfigValue entry, also we will keep the top level entries (excluding etc/entries) in the Spec.ConfigSpec record so that other resolvers can make use of top level entries.

The next goal is, refactor the CLI using this JSON on a more open constructor settings in the ConfigValue record

Cabal flags cause compile error

Compiling with nightly-2018-06-27 causes a compile error:

        ........./etc/src/System/Etc/Spec.hs:83:30: error:
        Not in scope: type constructor or class ‘JSON.FromJSON’
        No module named ‘JSON’ is imported.
       |
    83 | readConfigSpecTH :: (Lift k, JSON.FromJSON k) => Proxy k -> Text -> ExpQ
       |    

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.