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:
-
$HOME/.config/<application_name>/<config_file>.yaml
-
$HOME/.<application_name>/<config_file>.yaml
-
$HOME/.<config_file>.yaml
-
$HOME/.<config_file>rc
-
/etc/.<application_name>/<config_file>.yaml
Please keep in mind
-
application_name: Module of your class, e.g. “MyApplication” becomes “my_application”
-
config_file: Pluarized name of your class and “Config” strip off, e.g “ClientConfig” becomes “clients.yaml” (mind the pluralized name)
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:
-
config_file
-
config_name
-
application_name
If you want the class to look for your config file at a different place overwrite the following method
-
allowed_config_file_paths
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
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.
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.
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.
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.
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.
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.
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
@api private
# File lib/fedux_org_stdlib/app_config.rb, line 74 def known_options @options ||= Set.new end
# 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
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
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
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
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
# 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 configuration
# File lib/fedux_org_stdlib/app_config.rb, line 267 def clear @__config = {} end
Return configuration resetted to defaults
# File lib/fedux_org_stdlib/app_config.rb, line 272 def defaults config = dup config.clear config end
Show known options for configuration
# File lib/fedux_org_stdlib/app_config.rb, line 219 def known_options self.class.known_options end
Lock the configuration
# File lib/fedux_org_stdlib/app_config.rb, line 224 def lock _config.freeze end
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
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 from already found config file
# File lib/fedux_org_stdlib/app_config.rb, line 208 def reload load_config end
# 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
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
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
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
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
# File lib/fedux_org_stdlib/app_config.rb, line 430 def _available_config_file _allowed_config_file_paths.select { |f| ::File.exist? f } end
# File lib/fedux_org_stdlib/app_config.rb, line 422 def _class_name self.class.name.to_s.demodulize end
Holds the config rubocop:disable Style/TrivialAccessors
# File lib/fedux_org_stdlib/app_config.rb, line 153 def _config @__config end
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
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
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
# File lib/fedux_org_stdlib/app_config.rb, line 426 def _module_name self.class.to_s.deconstantize end
# File lib/fedux_org_stdlib/app_config.rb, line 303 def detect_files @files = _available_config_file end
# 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