class Kithe::ConfigBase

A central place for environmental/infrastructure type configuration. There were many existing ruby/rails 'config' solutions, but none did quite what I wanted without extra complexity. There are now kithe dependencies on this file, this is available solely as something for an individual app to use when it is convenient.

You may also want to consider [railsconfig](github.com/railsconfig/config)

Kithe::ConfigBase:

# Usage

You will define a custom app subclass of Kithe::ConfigBase, and define allowable config keys in there. In the simplest case:

class Config < Kithe::ConfigBase
  config_file Rails.root.join("config", "local_env.yml")
  define_key :foo_bar, default: "foo bar"
end

We recommend you put your local class in `./lib` to avoid any oddness with Rails auto-re-loading.

This can then be looked up with:

Config.lookup("foo_bar")

If you request a key that was not defined, an ArgumentError is raised. `lookup` will happily return nil if no value or default were provided. Instead, for early raise (of a TypeError) on nil or `blank?`:

Config.lookup!("foo_bar")

By default this will load from:

1. a system ENV value `FOO_BAR`
2. the specified `config_file` (can specify an array of multiple, later in list take priority;
   config files are run through ERB)
3. the default provided in the `define_key` definition

All values are cached after first lookup for performance and stabilty – this kind of environmental configuration should not change for life of process.

## Specifying ENV lookup

You can disable the ENV lookup:

define_key :foo_bar, env_key: false

Or specify a value to use in ENV lookup, instead of the automatic translation:

define_key :foo_bar, env_key: "unconventional_foo_bar"

Since ENV values are always strings, you can also specify a proc meant for use to transform to some other type:

define_key :foo_bar, system_env_transform: ->(str) { Integer(str) }

A built in transform is provided for keys meant to be boolean, which uses ActiveModel-compatible translation (“0”, “false” and empty string are falsey):

define_key :foo_bar, system_env_transform: Kithe::ConfigBase::BOOLEAN_TRANSFORM

## Allowable values

You can specify allowable values as an array, regex, or proc, to fail quickly if a provided value is not allowed.

define_key :foo_bar, default: "one", allows: ["one", "two", "three"]
define_key :key, allows: /one|two|three/
define_key :other, allows: ->(val) { !val.include?("foo") }

## Default value as proc

A default value can be provided as a proc. It is still only lazily executed once.

define_key :foo_bar, default: -> { "something" }

A proc default value can also use other config keys, simply by looking them up as usual:

define_key :foo_bar, default: => { "#{Config.lookup!('baz')} plus more" }

## Concurrency warning

This doesn't use any locking for concurrent initial loads, which is technically not great, but probably shouldn't be a problem in practice, especially in MRI. Trying to do proper locking with lazy load was too hard for me right now.

## Auto-loading

This is intentionally NOT in an auto-loaded directory, so it can be used more easily in Rails initialization without problems. github.com/rails/rails/issues/40904

Constants

BOOLEAN_TRANSFORM
NoValueProvided

Public Class Methods

config_file(args) click to toggle source
# File lib/kithe/config_base.rb, line 137
def self.config_file(args)
  self.config_file_paths = (self.config_file_paths + Array(args)).freeze
end
define_key(*args) click to toggle source
# File lib/kithe/config_base.rb, line 119
def self.define_key(*args)
  instance.define_key(*args)
end
lookup(*args) click to toggle source
# File lib/kithe/config_base.rb, line 129
def self.lookup(*args)
  instance.lookup(*args)
end
lookup!(*args) click to toggle source
# File lib/kithe/config_base.rb, line 133
def self.lookup!(*args)
  instance.lookup!(*args)
end
new() click to toggle source
# File lib/kithe/config_base.rb, line 115
def initialize
  @key_definitions = {}
end

Public Instance Methods

define_key(name, env_key: nil, default: nil, system_env_transform: nil, allows: nil) click to toggle source
# File lib/kithe/config_base.rb, line 141
def define_key(name, env_key: nil, default: nil, system_env_transform: nil, allows: nil)
  @key_definitions[name.to_sym] = {
    name: name.to_s,
    env_key: env_key,
    default: default,
    system_env_transform: system_env_transform,
    allows: allows
  }
end
lookup(name) click to toggle source
# File lib/kithe/config_base.rb, line 151
def lookup(name)
  name = name.to_sym
  defn = @key_definitions[name]

  unless defn
    raise ArgumentError.new("No env key defined for: #{name}")
  end

  defn[:cached_result] ||= compute_lookup(name)
end
lookup!(name) click to toggle source

like lookup, but raises on no or blank value.

# File lib/kithe/config_base.rb, line 163
def lookup!(name)
  lookup(name).tap do |value|
    raise TypeError, "No value was provided for `#{name}`" if value.blank?
  end
end

Private Instance Methods

compute_lookup(name) click to toggle source
# File lib/kithe/config_base.rb, line 171
def compute_lookup(name)
  defn = @key_definitions[name.to_sym]
  raise ArgumentError.new("No env key defined for: #{name}") unless defn

  result = system_env_lookup(defn)
  result = file_lookup(defn) if result == NoValueProvided
  result = default_lookup(defn) if result == NoValueProvided
  result = nil if result == NoValueProvided

  if disallowed?(defn, result)
    raise TypeError.new("config #{name.to_sym} is required to match #{defn[:allows].inspect}, but is #{result.inspect}")
  end

  result
end
default_lookup(defn) click to toggle source
# File lib/kithe/config_base.rb, line 223
def default_lookup(defn)
  if !defn.has_key?(:default)
    NoValueProvided
  elsif defn[:default].respond_to?(:to_proc)
    # allow a proc that gets executed on demand
    self.instance_exec(&defn[:default])
  else
    defn[:default]
  end
end
disallowed?(defn, value) click to toggle source
# File lib/kithe/config_base.rb, line 234
def disallowed?(defn, value)
  spec = defn[:allows]
  return false unless spec

  if spec.kind_of?(Array)
    !spec.include?(value)
  else
    !(spec === value)
  end
end
file_lookup(defn) click to toggle source
# File lib/kithe/config_base.rb, line 204
def file_lookup(defn)
  @loaded_from_files ||= load_from_files!
  if @loaded_from_files.has_key?(defn[:name])
    @loaded_from_files[defn[:name]]
  else
    NoValueProvided
  end
end
load_from_files!() click to toggle source
# File lib/kithe/config_base.rb, line 213
def load_from_files!
  loaded = {}
  config_file_paths.each do |file_path|
    if File.exist?(file_path)
      loaded.merge!( YAML.load(ERB.new(File.read(file_path)).result) || {} )
    end
  end
  return loaded
end
system_env_lookup(defn) click to toggle source
# File lib/kithe/config_base.rb, line 188
def system_env_lookup(defn)
  return NoValueProvided if defn[:env_key] == false

  value = if defn[:env_key] && ENV.has_key?(defn[:env_key].to_s)
    ENV[defn[:env_key].to_s]
  elsif ENV.has_key?(defn[:name].upcase)
    ENV[defn[:name].upcase]
  end

  if value
    defn[:system_env_transform] ? defn[:system_env_transform].call(value) : value
  else
    NoValueProvided
  end
end