class FeduxOrgStdlib::AppConfig

This class makes a config file available as an object. The config file needs to be `YAML` by default. It is read by `Psych` and converted to a hash. If you chose to use a different file format: Each config file needs to translatable to a hash or another data structure which responds to `[]` by the given `config_engine`. If no suitable config file can be found the config uses only the defined defaults within the class.

By default it will look for a suitable config file in the given order:

  1. $HOME/.config/<application_name>/<config_file>.yaml

  2. $HOME/.<application_name>/<config_file>.yaml

  3. $HOME/.<config_file>.yaml

  4. $HOME/.<config_file>rc

  5. /etc/.<application_name>/<config_file>.yaml

Please keep in mind

Most conventions defined by me are implemented as separate methods. If one convention is not suitable for your use case, just overwrite the method.

If you prefer to use a different path to the config file or name of the config file one of the following methods needs to be overwritten:

If you want the class to look for your config file at a different place overwrite the following method

Below you find some examples for the usage of the class:

@example Create config with one writer and reader

module MyApplication
  class ClientConfig < AppConfig
    # 'data1' is the default for option1
    # if you create a file
    option :option1, 'data1'
  end
end

@example Create config with a reader only

module MyApplication
  class ClientConfig < AppConfig
    # 'data1' is the default for option1
    # if you create a file
    option_reader :option1, 'data1'
  end
end

@example Config yaml file for the classes above: clients.yaml

---
option1: 'data2'

Attributes

check_unknown_options[R]

Create a new instance of config

It tries to find a suitable configuration file. If it doesn't find one the config is empty and uses the defaults defined within a config class

@param [String] file

Path where config file is stored. The file will be read by the
`config_engine`.

@param [Object] config_engine (Psych)

The engine to read config file

@param [true, false] check_unknown_options

Should a warning be put to stdout if there are unknown options in yaml
config file

@raise [Exceptions::ConfigFileNotReadable]

If an avaiable config file could not be read by the config engine

@return [AppConfig]

The config instance. If the resulting data structure created by the
config_engine does not respond to `:[]` an empty config object will be
created.
config_engine[R]

Create a new instance of config

It tries to find a suitable configuration file. If it doesn't find one the config is empty and uses the defaults defined within a config class

@param [String] file

Path where config file is stored. The file will be read by the
`config_engine`.

@param [Object] config_engine (Psych)

The engine to read config file

@param [true, false] check_unknown_options

Should a warning be put to stdout if there are unknown options in yaml
config file

@raise [Exceptions::ConfigFileNotReadable]

If an avaiable config file could not be read by the config engine

@return [AppConfig]

The config instance. If the resulting data structure created by the
config_engine does not respond to `:[]` an empty config object will be
created.
files[R]

Create a new instance of config

It tries to find a suitable configuration file. If it doesn't find one the config is empty and uses the defaults defined within a config class

@param [String] file

Path where config file is stored. The file will be read by the
`config_engine`.

@param [Object] config_engine (Psych)

The engine to read config file

@param [true, false] check_unknown_options

Should a warning be put to stdout if there are unknown options in yaml
config file

@raise [Exceptions::ConfigFileNotReadable]

If an avaiable config file could not be read by the config engine

@return [AppConfig]

The config instance. If the resulting data structure created by the
config_engine does not respond to `:[]` an empty config object will be
created.
logger[R]

Create a new instance of config

It tries to find a suitable configuration file. If it doesn't find one the config is empty and uses the defaults defined within a config class

@param [String] file

Path where config file is stored. The file will be read by the
`config_engine`.

@param [Object] config_engine (Psych)

The engine to read config file

@param [true, false] check_unknown_options

Should a warning be put to stdout if there are unknown options in yaml
config file

@raise [Exceptions::ConfigFileNotReadable]

If an avaiable config file could not be read by the config engine

@return [AppConfig]

The config instance. If the resulting data structure created by the
config_engine does not respond to `:[]` an empty config object will be
created.
merge_files[R]

Create a new instance of config

It tries to find a suitable configuration file. If it doesn't find one the config is empty and uses the defaults defined within a config class

@param [String] file

Path where config file is stored. The file will be read by the
`config_engine`.

@param [Object] config_engine (Psych)

The engine to read config file

@param [true, false] check_unknown_options

Should a warning be put to stdout if there are unknown options in yaml
config file

@raise [Exceptions::ConfigFileNotReadable]

If an avaiable config file could not be read by the config engine

@return [AppConfig]

The config instance. If the resulting data structure created by the
config_engine does not respond to `:[]` an empty config object will be
created.
safe[R]

Create a new instance of config

It tries to find a suitable configuration file. If it doesn't find one the config is empty and uses the defaults defined within a config class

@param [String] file

Path where config file is stored. The file will be read by the
`config_engine`.

@param [Object] config_engine (Psych)

The engine to read config file

@param [true, false] check_unknown_options

Should a warning be put to stdout if there are unknown options in yaml
config file

@raise [Exceptions::ConfigFileNotReadable]

If an avaiable config file could not be read by the config engine

@return [AppConfig]

The config instance. If the resulting data structure created by the
config_engine does not respond to `:[]` an empty config object will be
created.
working_directory[R]

Create a new instance of config

It tries to find a suitable configuration file. If it doesn't find one the config is empty and uses the defaults defined within a config class

@param [String] file

Path where config file is stored. The file will be read by the
`config_engine`.

@param [Object] config_engine (Psych)

The engine to read config file

@param [true, false] check_unknown_options

Should a warning be put to stdout if there are unknown options in yaml
config file

@raise [Exceptions::ConfigFileNotReadable]

If an avaiable config file could not be read by the config engine

@return [AppConfig]

The config instance. If the resulting data structure created by the
config_engine does not respond to `:[]` an empty config object will be
created.

Public Class Methods

known_options() click to toggle source

@api private

# File lib/fedux_org_stdlib/app_config.rb, line 74
def known_options
  @options ||= Set.new
end
new( file: nil, merge_files: false, config_engine: Psych, logger: FeduxOrgStdlib::Logging::Logger.new, check_unknown_options: true, working_directory: Dir.getwd, safe: true ) click to toggle source
# File lib/fedux_org_stdlib/app_config.rb, line 186
def initialize(
  file: nil,
  merge_files: false,
  config_engine: Psych,
  logger: FeduxOrgStdlib::Logging::Logger.new,
  check_unknown_options: true,
  working_directory: Dir.getwd,
  safe: true
)
  @logger                = logger
  @files                 = Array(file)
  @merge_files           = merge_files
  @config_engine         = config_engine
  @check_unknown_options = check_unknown_options
  @working_directory     = working_directory
  @safe                  = safe

  detect_files if @files.blank?
  load_config
end
option(option, default_value) click to toggle source

Define a writer and a reader for option

@param [Symbol] option

Name of option

@param [Object] default_value

Default value of option
# File lib/fedux_org_stdlib/app_config.rb, line 143
def option(option, default_value)
  option_reader(option, default_value)
  option_writer(option)
end
option_reader(option, default_value) click to toggle source

Define a reader for option

@param [Symbol] option

Name of option

@param [Object] default_value

The default value of this option
# File lib/fedux_org_stdlib/app_config.rb, line 97
def option_reader(option, default_value)
  option =  option.to_sym

  fail Exceptions::OptionNameForbidden, JSON.dump(option: option) if _reserved_key_words.include? option

  define_method option do
    _config.fetch(option, default_value)
  end

  known_options << option
end
option_writer(option) click to toggle source

Define a writer for option

Please make sure that you define a reader as well. Otherwise you cannot access the option. Under normal conditions it does not make sense to use this method.

@api private

@param [Symbol] option

Name of option

@raise [Exceptions::ConfigLocked]

If one tries to modified a locked config
# File lib/fedux_org_stdlib/app_config.rb, line 122
def option_writer(option)
  fail Exceptions::OptionNameForbidden, JSON.dump(option: option) if _reserved_key_words.include? "#{option}=".to_sym

  define_method "#{option}=".to_sym do |value|
    begin
      _config[option.to_sym] = value
    rescue RuntimeError
      raise Exceptions::ConfigLocked
    end
  end

  known_options << option
end
process_environment() click to toggle source

Get access to process environment

This might be handy to define default options

@example Get variable

process_environment.fetch('HOME')
# => ENV['HOME']
# File lib/fedux_org_stdlib/app_config.rb, line 86
def process_environment
  @process_environment ||= ProcessEnvironment.new
end

Private Class Methods

_reserved_key_words() click to toggle source
# File lib/fedux_org_stdlib/app_config.rb, line 434
def self._reserved_key_words
  (methods | instance_methods | private_methods | private_instance_methods) - (Class.methods | Class.private_methods) | [:to_s]
end

Public Instance Methods

clear() click to toggle source

Clear configuration

# File lib/fedux_org_stdlib/app_config.rb, line 267
def clear
  @__config = {}
end
defaults() click to toggle source

Return configuration resetted to defaults

# File lib/fedux_org_stdlib/app_config.rb, line 272
def defaults
  config = dup
  config.clear

  config
end
known_options() click to toggle source

Show known options for configuration

# File lib/fedux_org_stdlib/app_config.rb, line 219
def known_options
  self.class.known_options
end
lock() click to toggle source

Lock the configuration

# File lib/fedux_org_stdlib/app_config.rb, line 224
def lock
  _config.freeze
end
preferred_configuration_file() click to toggle source

Return the path to the preferred configuration file @return [String]

The path to the preferred configuration file
# File lib/fedux_org_stdlib/app_config.rb, line 262
def preferred_configuration_file
  _allowed_config_file_paths.first
end
redetect() click to toggle source

Redected configuration file and reload config afterwards

# File lib/fedux_org_stdlib/app_config.rb, line 213
def redetect
  detect_files
  load_config
end
reload() click to toggle source

Reload from already found config file

# File lib/fedux_org_stdlib/app_config.rb, line 208
def reload
  load_config
end
to_h(keys: [], remove_blank: false) click to toggle source
# File lib/fedux_org_stdlib/app_config.rb, line 279
def to_h(keys: [], remove_blank: false)
  options_to_return = if keys.blank?
                        known_options.to_a
                      else
                        known_options.to_a & keys.map(&:to_sym)
                     end

  options_to_return.sort.each_with_object({}) do |e, a|
    next if remove_blank && public_send(e).blank?
    a[e] = public_send(e)
  end
end
to_s() click to toggle source

Output a string presentation of the configuration

@return [String]

An formatted version of the configuration
# File lib/fedux_org_stdlib/app_config.rb, line 232
def to_s
  # header 'length' = 6 letters
  length = self.class.known_options.reduce(6) { |a, e| e.size > a ? e.size : a }

  result = []
  result << format("%#{length}s | %s", 'option', 'value')
  result << format('%s + %s', '-' * length, '-' * 80)

  self.class.known_options.sort.each do |o|
    value = public_send(o)

    value = if value == false
              Array(value)
            elsif value.blank?
              Array('is undefined')
            elsif value.is_a?(Hash) || value.is_a?(Array)
              value
            else
              Array(value)
            end

    result << format("%#{length}s | %s", o, value.to_list)
  end

  result.join("\n")
end
to_yaml(prepend: nil, **args) click to toggle source

Convert config to yaml

# File lib/fedux_org_stdlib/app_config.rb, line 293
def to_yaml(prepend: nil, **args)
  yaml = Psych.dump to_h(**args).deep_stringify_keys

  return yaml.split("\n").map { |l| l.prepend prepend }.join("\n") if prepend

  yaml
end

Private Instance Methods

_allowed_config_file_paths() click to toggle source

The paths where to look for the config file

@return [Array]

A list of paths where the config object should look for its config
file.
# File lib/fedux_org_stdlib/app_config.rb, line 411
def _allowed_config_file_paths
  [
    ::File.expand_path(::File.join('~', '.config', _application_name, _config_file)),
    ::File.expand_path(::File.join('~', format('.%s', _application_name), _config_file)),
    ::File.expand_path(::File.join('~', format('.%s', _config_file))),
    ::File.expand_path(::File.join('~', format('.%src', _config_name))),
    ::File.expand_path(::File.join('/etc', _application_name, _config_file)),
    ::File.expand_path(::File.join(working_directory, _config_file))
  ]
end
_application_name() click to toggle source

The name of your application

@return [String]

This will strip of the class part of fully qualified class name and
converted it to a path.

@example Determine application name

class MyApplication::MyConfig; end

This will be converted to

my_application
# File lib/fedux_org_stdlib/app_config.rb, line 402
def _application_name
  _module_name.underscore
end
_available_config_file() click to toggle source
# File lib/fedux_org_stdlib/app_config.rb, line 430
def _available_config_file
  _allowed_config_file_paths.select { |f| ::File.exist? f }
end
_class_name() click to toggle source
# File lib/fedux_org_stdlib/app_config.rb, line 422
def _class_name
  self.class.name.to_s.demodulize
end
_config() click to toggle source

Holds the config rubocop:disable Style/TrivialAccessors

# File lib/fedux_org_stdlib/app_config.rb, line 153
def _config
  @__config
end
_config_file() click to toggle source

The name of the config file

@return [String]

The name of the config file. It defaults to `<config_name>.yaml`. If
you want to use a different file name you need to overwrite this
method.
# File lib/fedux_org_stdlib/app_config.rb, line 358
def _config_file
  "#{_config_name}#{_config_file_suffix}"
end
_config_file_suffix() click to toggle source

The suffix of the config file

@return [String]

The suffix of the config file
# File lib/fedux_org_stdlib/app_config.rb, line 366
def _config_file_suffix
  '.yaml'
end
_config_name() click to toggle source

The base name of the config

@return [String]

This one returns the base name of the config file (without the file
extension). It uses the class name of the config class

@example Determine the base name of the config

class ClientConfig; end

This will result in `client` as base name for the config file.

# File lib/fedux_org_stdlib/app_config.rb, line 381
def _config_name
  unless (name = _class_name.sub(/Config/, '').underscore.pluralize).blank?
    return name
  end

  fail Exceptions::ClassNameIsMissing, JSON.dump(klass: _class_name)
end
_module_name() click to toggle source
# File lib/fedux_org_stdlib/app_config.rb, line 426
def _module_name
  self.class.to_s.deconstantize
end
detect_files() click to toggle source
# File lib/fedux_org_stdlib/app_config.rb, line 303
def detect_files
  @files = _available_config_file
end
load_config() click to toggle source
# File lib/fedux_org_stdlib/app_config.rb, line 307
def load_config
  @__config = {}

  if files.blank?
    logger.debug "No configuration files found at #{_allowed_config_file_paths.to_list}, therefor I'm going to use an empty config object instead."
    return
  end

  config_files = if merge_files
                   files
                 else
                   Array(files.first)
          end

  config_files.each do |f|
    begin
      yaml = if safe
               config_engine.safe_load(File.read(f), [Symbol, Regexp, Date, Time])
             else
               config_engine.load(File.read(f))
             end
    rescue StandardError => e
      raise Exceptions::ConfigFileNotReadable, JSON.dump(message: e.message, file: f)
    end

    next if yaml.blank?

    unless yaml.is_a? Hash
      logger.warn "There seems to be a problem transforming config file \"#{f}\" to a hash, therefor I will use an empty config object."
      next
    end

    yaml = yaml.deep_symbolize_keys

    yaml_withknown_options = yaml.deep_symbolize_keys.slice(*known_options)
    unknown_options = yaml.keys - yaml_withknown_options.keys

    logger.warn "Unknown config options #{(unknown_options).to_list} in config file #{f} detected. Please define them in your config class or remove the entries in your config file or disable check via `check_unknown_options: false` to get rid of this warning." unless unknown_options.blank? && check_unknown_options == true

    @__config.reverse_merge! Hash(yaml_withknown_options)
  end

  self
end