collapsium-config

Using collapsium's UberHash class for easy access to configuration values, this gem reads and merges various configuration sources into one configuration object.

Gem Version Build status

Summary

  • Supports JSON and YAML file formats.
  • Generates configuration files from ERB templates, includes, local override files and an extension mechanism.
  • Pathed access to configuration variables.
  • Allows overriding of configuration values from the environment.

Basic Usage

While you can use the Configuration class yourself, the simplest usage is to access a global configuration object:

require 'collapsium-config'
include Collapsium::Config

puts config["foo"] # loaded automatically from config.yml

Advanced Usage

Configuration File Management

Configuration File Location

The friendly neighbour to the #config function introduced in the basic usage section above is the #config_file accessor. Its value will default to config.yml, but you can set it to something different, too:

config_file = 'foo.yaml'
puts config["foo"] # loaded automatically from foo.yaml

Loading Configuration Files

All that #config and #config_file do is wrap #load_config such that configuration is loaded only once. You can load configuration files manually, too:

my_config = Collapsium::Config::Configuration.load_config('filename.yaml')

Loading Options

  • data can be passed for resolving references in ERB templating (see below)
  • resolve_extensions is a flag that determines whether the extends keyword is honoured (see below), and defaults to true.
  • nonexistent_base can be one of :ignore and :extend and determines how extends should behave if a base is referenced that does not exist:
    • :ignore behaves as if the base simply wasn't mentioned.
    • :extend behaves as if the base did in fact exist, but was an empty configuration Hash.

Array Files

Configuration files can also contain Arrays at the top level. While that may make some sense at times, it does not make for good naming schemes and creates problems elsewhere.

Therefore, if your file contains an Array at the top level, the class wraps it into a Hash with a config key containing the Array:

- foo
- bar
config["config"][0] # => "foo"
config["config"][1] # => "bar"

File Formats

The gem supports loading YAML and JSON configuration files. Both formats can be mixed in the various mechanisms described below.

Local Configuration Overrides

For the example file of config/config.yml, if a file with the same path and name, and the name postfix -local exists (i.e. config/config-local.yml), that file will also be loaded. It's keys will be recursively added to the keys from the main configuration file, overwriting only leaves, not entire hashes.

Example:

# config/config.yml
---
foo:
  bar: 42
  baz: quux

# config/config-local.yml
---
something: else
foo:
  baz: override

# result
---
something: else
foo:
  bar: 42
  baz: override

Templating

Configuration files aren't quite static entities even taking merging of local overrides into account. They can further be generated at load time by templating, extension and including.

ERB templating in configuration files works out-of-the-box, but one of the more powerful features is of course to substitute some values in the template which your loading code already knows. If you're using #load_config, you can do that with the data keyword parameter:

my_data_hash = {}
my_config = Configuration.load_config('foo.yaml', data: my_data_hash)

Note that the template has access to the entire hash under the data name, not to its individual keys:

<%= data[:some_key] %> # correct usage
<%= some_key %>        # incorrect usage

But even without explicit passing of a data hash, you can use templating to e.g. include environment variables:

foo: <%= ENV['MYVAR'] %>

Note, though, that this might interact unexpectedly with the environment override feature described later.

Extension

An additional feature is that you can extend individual hashes with values from other hashes.

---
root:
  foo: bar
derived:
  baz: quux
  extends: root

This results in:

---
root:
  foo: bar
derived:
  baz: quux
  foo: bar
  base: root

The special extends keyword is interpreted to merge all values from the value at path .root into the value at path .derived. Additionally, .derived will gain a new key base which is an Array containing all the bases merged into the value.

Notes:

  • Absolute paths are preferred for values of extends.
  • Relative paths for values of extends are looked up in the parent of the value that contains the extends keyword, i.e. the root in the example above. So in this minimal example, specifying .base and base is equivalent.
  • You can specify a comma-separated list of bases in the extends keyword. Latter paths overwrite values in earlier paths.
  • You can also specify an Array of paths, with the same effect.
  • This feature means that extends and base are reserved configuration keys!
  • Multiple levels of extension are supported.
  • The order of items in base is deterministic:
    1. Items are added in the order in which they appear in extends, but...
    2. ... before each item, it's ancestors are listed in depth-first order, which means the root of each item's hierarchy is listed first.

Includes

Includes work just as you might expect: if you specify a key include anywhere, the value will be interpreted as a file system path to another configuration file. That other file will be loaded, and the parent of the include statement will gain all the values from the other configuration file.

Example:

# config/main.yml
include: config/first.yml
foo:
  bar: 42
  include: config/second.yml
# config/first.yml
baz: quux
# config/second.yml
- 123
- 456

Will result in:

# final YAML
baz: quux
foo:
  bar: 42
  data:
    - 123
    - 456

Notes:

  • If your loaded configuration file contains an Array at the top level, then a new key config will be added (see Array Files above)
  • You can specify a comma-separated list of paths in the include keyword. Latter paths overwrite values in earlier paths.
  • You can also specify an Array of paths, with the same effect.
  • This means that include is a reserved configuration key.

Configuration Access

Pathed Access

Thanks to collapsium's UberHash, configuration values can be accessed more easily than in a regular nested structure. Take the following configuration as an example:

---
foo:
  bar:
    baz: 42
    quux:
      - 123
      - "asdf"

Then, the following are equivalent:

config["foo"]["bar"]["baz"]
config["foo.bar.baz"]
config[".foo.bar.baz"]
config["foo.bar"]["baz"]
config["foo"]["bar.baz"]

The major benefit is that if any of the path components does not exist, nil is returned (and the behaviour is equivalent to other access methods such as :fetch, etc.)

Similarly you can use this type of access for writing: config['baz.quux'] = 42 will create both the baz hash, and it's child the quux key.

Environment Override

Given a configuration path, any environment variable with the same name (change path to upper case letters and replace . with _, e.g. foo.bar becomes FOO_BAR) overrides the values in the configuration file.

# Called with FOO_BAR=42
config["foo.bar"] # => 42

If the environment variable is parseable as JSON, then that parsed JSON will replace the original configuration path (i.e. it will not be merged).

# Called with FOO_BAR='{ "baz": 42 }'
config["foo.bar.baz"] # => 42

A Note on Priorities

A lot of the features above interact with each other. For example, environment override still respects pathed access. In other cases, things are not quite so clear, so let's give you a rough idea on priorities in the code:

  1. Templating happens when files are loaded, and generates the most basic data the gem works with.
  2. Configuration file merging happens next, i.e. config.yml and config-local.yml are merged into one data structure.
  3. Next up is handling of include directives.
  4. After that, extends is resolved - that is, extends works on paths that only exists after any of the above steps created them. This step finishes the configuration loading process.
  5. Finally, environment override works whenever a value is accessed, meaning if the environment changes, so does the configuration value.