Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add a way to prefer environment over settings #16

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 63 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@

This is a one-stop shop for all your configuration needs:

* [Read](#216-read) and [write](#217-write) config files in YAML, JSON, TOML, INI, XML, HCL and Java Properties formats
* Add [custom marshallers](#222-register_marshaller) or override the built-in ones
* [Set](#21-set) and [read](#24-fetch) settings for deeply nested keys
* [Set](#21-set) defaults for undefined settings
* [Read](#24-fetch) settings with indifferent access
* [Merge](#25-merge) configuration settings from other hash objects
* Read values from [environment variables](#23-set_from_env)
- [Read](#216-read) and [write](#217-write) config files in YAML, JSON, TOML, INI, XML, HCL and Java Properties formats
- Add [custom marshallers](#222-register_marshaller) or override the built-in ones
- [Set](#21-set) and [read](#24-fetch) settings for deeply nested keys
- [Set](#21-set) defaults for undefined settings
- [Read](#24-fetch) settings with indifferent access
- [Merge](#25-merge) configuration settings from other hash objects
- Read values from [environment variables](#23-set_from_env)

## Installation

Expand All @@ -53,35 +53,35 @@ Or install it yourself as:

## Contents

* [1. Usage](#1-usage)
* [1.1 app](#11-app)
* [2. Interface](#2-interface)
* [2.1 set](#21-set)
* [2.2 set_if_empty](#22-set_if_empty)
* [2.3 set_from_env](#23-set_from_env)
* [2.4 fetch](#24-fetch)
* [2.5 merge](#25-merge)
* [2.6 coerce](#26-coerce)
* [2.7 append](#27-append)
* [2.8 remove](#28-remove)
* [2.9 delete](#29-delete)
* [2.10 alias_setting](#210-alias_setting)
* [2.11 validate](#211-validate)
* [2.12 filename=](#212-filename)
* [2.13 extname=](#213-extname)
* [2.14 append_path](#214-append_path)
* [2.15 prepend_path](#215-prepend_path)
* [2.16 read](#216-read)
* [2.17 write](#217-write)
* [2.18 exist?](#218-exist)
* [2.19 env_prefix=](#219-env_prefix)
* [2.20 env_separator=](#220-env_separator)
* [2.21 autoload_env](#221-autoload_env)
* [2.22 register_marshaller](#222-register_marshaller)
* [2.23 unregister_marshaller](#223-unregister_marshaller)
* [3. Examples](#3-examples)
* [3.1 Working with env vars](#31-working-with-env-vars)
* [3.2 Working with optparse](#32-working-with-optparse)
- [1. Usage](#1-usage)
- [1.1 app](#11-app)
- [2. Interface](#2-interface)
- [2.1 set](#21-set)
- [2.2 set_if_empty](#22-set_if_empty)
- [2.3 set_from_env](#23-set_from_env)
- [2.4 fetch](#24-fetch)
- [2.5 merge](#25-merge)
- [2.6 coerce](#26-coerce)
- [2.7 append](#27-append)
- [2.8 remove](#28-remove)
- [2.9 delete](#29-delete)
- [2.10 alias_setting](#210-alias_setting)
- [2.11 validate](#211-validate)
- [2.12 filename=](#212-filename)
- [2.13 extname=](#213-extname)
- [2.14 append_path](#214-append_path)
- [2.15 prepend_path](#215-prepend_path)
- [2.16 read](#216-read)
- [2.17 write](#217-write)
- [2.18 exist?](#218-exist)
- [2.19 env_prefix=](#219-env_prefix)
- [2.20 env_separator=](#220-env_separator)
- [2.21 autoload_env](#221-autoload_env)
- [2.22 register_marshaller](#222-register_marshaller)
- [2.23 unregister_marshaller](#223-unregister_marshaller)
- [3. Examples](#3-examples)
- [3.1 Working with env vars](#31-working-with-env-vars)
- [3.2 Working with optparse](#32-working-with-optparse)

## 1. Usage

Expand Down Expand Up @@ -288,6 +288,22 @@ config.fetch(:settings, "base")
config.fetch("settings", :base)
```

By default, `fetch` prefers values found in settings over those found in environment. If you prefer it the other way around, you can pass `prefer: :environment` as an option to `fetch`.

For example, with `BASE=CAD` in environment, and `base: USD` in a YAML config file...

```ruby
config.fetch(:base) # => "USD"
config.fetch(:base, prefer: :settings) # => "USD" (default, equivalent to above)
config.fetch(:base, prefer: :configuration) # => "USD" (alias for settings)
config.fetch(:base, prefer: :config) # => "USD" (alias for settings)
config.fetch(:base, prefer: :file) # => "USD" (alias for settings)
config.fetch(:base, prefer: :files) # => "USD" (alias for settings)
config.fetch(:base, prefer: :environment) # => "CAD"
config.fetch(:base, prefer: :env) # => "CAD" (alias for environment)
config.fetch(:base, prefer: :ENV) # => "CAD" (alias for environment)
```

### 2.5 merge

To merge in other configuration settings as hash use `merge`:
Expand Down Expand Up @@ -509,13 +525,13 @@ There are two ways for reading configuration files and both use the `read` metho

Currently the supported file formats are:

* `yaml` for `.yaml`, `.yml` extensions
* `json` for `.json` extension
* `toml` for `.toml` extension
* `ini` for `.ini`, `.cnf`, `.conf`, `.cfg`, `.cf extensions`
* `hcl` for `.hcl` extensions
* `xml` for `.xml` extension
* `jprops` for `.properties`, `.props`, `.prop` extensions
- `yaml` for `.yaml`, `.yml` extensions
- `json` for `.json` extension
- `toml` for `.toml` extension
- `ini` for `.ini`, `.cnf`, `.conf`, `.cfg`, `.cf extensions`
- `hcl` for `.hcl` extensions
- `xml` for `.xml` extension
- `jprops` for `.properties`, `.props`, `.prop` extensions

Calling `read` without any arguments searches through provided locations to find configuration file and reads it. Therefore, you need to specify at least one search path that contains the configuration file together with actual filename. When filename is specified then all known extensions will be tried.

Expand Down Expand Up @@ -664,7 +680,7 @@ Then we can make configuration aware of the above variable name in one of these
```ruby
config.set_from_env(:server, :port)
config.set_from_env("server.port")
````
```

And retrieve the value:

Expand Down Expand Up @@ -705,8 +721,8 @@ Currently supported formats out-of-the-box are: `YAML`, `JSON`, `TOML`, `INI`, `

To create your own marshaller use the `TTY::Config::Marshaller` interface. You need to provide the implementation for the following marshalling methods:

* `marshal`
* `unmarshal`
- `marshal`
- `unmarshal`

In addition, you will need to specify the extension types this marshaller will handle using the `extension` method. The method accepts a list of names preceded by a dot:

Expand Down Expand Up @@ -778,7 +794,7 @@ config.unregister_marshaller :yaml, :json, :toml, :ini, :xml, :hcl, :jprops

### 3.1 Working with env vars

*TTY::Config* fully supports working with environment variables. For example, there are couple of environment variables that your configuration is interested in, which normally would be set in terminal but for the sake of this example we assign them:
_TTY::Config_ fully supports working with environment variables. For example, there are couple of environment variables that your configuration is interested in, which normally would be set in terminal but for the sake of this example we assign them:

```ruby
ENV["MYTOOL_HOST"] = "192.168.1.17"
Expand Down
55 changes: 47 additions & 8 deletions lib/tty/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class Config
UnsupportedExtError = Class.new(StandardError)
# Error raised when validation assertion fails
ValidationError = Class.new(StandardError)
# Error raised when told to prefer an unrecognized source
UnsupportedSource = Class.new(StandardError)

# Coerce a hash object into Config instance
#
Expand Down Expand Up @@ -57,6 +59,17 @@ def self.normalize_hash(hash, method = :to_sym)
end
end

def self.normalize_preferred(source)
case source.to_sym
when :settings, :configuration, :config, :file, :files
:settings
when :environment, :env, :ENV
:environment
else
raise UnsupportedSource, "Preferred Source `#{source}` is not supported."
end
end

# A collection of config paths
# @api public
attr_reader :location_paths
Expand Down Expand Up @@ -85,6 +98,18 @@ def self.normalize_hash(hash, method = :to_sym)
# @api public
attr_accessor :env_separator

# The preferred source for settings
# @api public
attr_reader :preferred

# Set the preferred source for settings
# @api public
def preferred=(source)
@preferred = self.class.normalize_preferred(source)
end

alias_method :prefer, :preferred=

# Create a configuration instance
#
# @api public
Expand All @@ -100,6 +125,7 @@ def initialize(settings = {})
@env_separator = "_"
@autoload_env = false
@aliases = {}
self.preferred = :settings

register_marshaller :yaml, Marshallers::YAMLMarshaller
register_marshaller :json, Marshallers::JSONMarshaller
Expand Down Expand Up @@ -282,21 +308,34 @@ def to_env_key(key)
# @return [Object]
#
# @api public
def fetch(*keys, default: nil, &block)
def fetch(*keys, default: nil, prefer: self.preferred, &block)
# check alias
real_key = @aliases[flatten_keys(keys)]
keys = real_key.split(key_delim) if real_key

keys = convert_to_keys(keys)
env_key = autoload_env? ? to_env_key(keys[0]) : @envs[flatten_keys(keys)]
# first try settings
value = deep_fetch(@settings, *keys)
# then try ENV var
if value.nil? && env_key
value = ENV[env_key]

case self.class.normalize_preferred(prefer)
when :settings
# first try settings
value = deep_fetch(@settings, *keys)
# then try ENV var
if value.nil? && env_key
value = ENV[env_key]
end
# then try default
value = block || default if value.nil?
when :environment
# first try ENV var
value = ENV[env_key] if env_key
# then try settings
if value.nil?
value = deep_fetch(@settings, *keys)
end
else
raise UnsupportedSource, "Preferred Source `#{prefer}` is not supported."
end
# then try default
value = block || default if value.nil?

while callable_without_params?(value)
value = value.()
Expand Down
39 changes: 39 additions & 0 deletions spec/unit/fetch_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,43 @@

expect(config.fetch("foo", :bar, "baz")).to eq(2)
end

context "when environment and settings conflict" do
before :each do
ENV.update({ "SETTINGS_BASE" => "CAD" })
@config = TTY::Config.new do |config|
config.read fixtures_path("investments.yml")
config.set_from_env(:settings, :base)
end
@default = @config.preferred
end

context "and settings are preferred (default)" do
before :each do
@config.prefer @default
end

it "uses the value from settings" do
expect(@config.fetch(:settings, :base)).to eq("USD")
end

it "uses the value from environment on demand" do
expect(@config.fetch(:settings, :base, prefer: :environment)).to eq("CAD")
end
end

context "and environment is preferred" do
before :each do
@config.prefer :environment
end

it "uses the value from environment" do
expect(@config.fetch(:settings, :base)).to eq("CAD")
end

it "uses the value from settings on demand" do
expect(@config.fetch(:settings, :base, prefer: :settings)).to eq("USD")
end
end
end
end
48 changes: 48 additions & 0 deletions spec/unit/preferred_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

RSpec.describe TTY::Config, "#preferred" do
it "prefers settings over environment by default" do
config = TTY::Config.new
expect(config.preferred).to eq(:settings)
end

it "sets a preferred source" do
config = TTY::Config.new
config.preferred = :environment

expect(config.preferred).to eq(:environment)
end

it "sets a preferred source in a nicer way" do
config = TTY::Config.new
config.prefer :environment

expect(config.preferred).to eq(:environment)
end

it "accepts aliases for settings" do
config = TTY::Config.new

%i[settings configuration config file files].each do |variant|
config.preferred = variant
expect(config.preferred).to eq(:settings)
end
end

it "accepts aliases for environment" do
config = TTY::Config.new

%i[environment env ENV].each do |variant|
config.preferred = variant
expect(config.preferred).to eq(:environment)
end
end

it "does not accept unknown source names" do
config = TTY::Config.new
expect {
config.preferred = :invalid
}.to raise_error(TTY::Config::UnsupportedSource,
/Preferred Source `invalid` is not supported./)
end
end