class TTY::Config
Responsible for managing application configuration
@api public
Constants
- DependencyLoadError
Error raised when failed to load a dependency
- ReadError
Error raised when key fails validation
- UnsupportedExtError
Erorrr raised when setting unknown file extension
- VERSION
- ValidationError
Error raised when validation assertion fails
- WriteError
Error raised when issues writing configuration to a file
Attributes
The prefix used for searching ENV variables @api public
The string used to separate parts in ENV variable name @api public
The name of the configuration file extension @api public
The name of the configuration file without extension @api public
The key delimiter used for specifying deeply nested keys @api public
A collection of config paths @api public
The validations for this configuration @api public
Public Class Methods
Coerce a hash object into Config
instance
@return [TTY::Config]
@api private
# File lib/tty/config.rb, line 37 def self.coerce(hash, &block) new(normalize_hash(hash), &block) end
Create a configuration instance
@api public
# File lib/tty/config.rb, line 90 def initialize(settings = {}) @settings = settings @location_paths = [] @validators = {} @filename = "config" @extname = ".yml" @key_delim = "." @envs = {} @env_prefix = "" @env_separator = "_" @autoload_env = false @aliases = {} register_marshaller :yaml, Marshallers::YAMLMarshaller register_marshaller :json, Marshallers::JSONMarshaller register_marshaller :toml, Marshallers::TOMLMarshaller register_marshaller :ini, Marshallers::INIMarshaller register_marshaller :hcl, Marshallers::HCLMarshaller register_marshaller :jprops, Marshallers::JavaPropsMarshaller yield(self) if block_given? end
Convert string keys via method
@param [Hash] hash
the hash to normalize keys for
@param [Symbol] method
the method to use for converting keys
@return [Hash{Symbol => Object}]
the converted hash
@api private
# File lib/tty/config.rb, line 52 def self.normalize_hash(hash, method = :to_sym) hash.each_with_object({}) do |(key, val), acc| value = val.is_a?(::Hash) ? normalize_hash(val, method) : val acc[key.public_send(method)] = value end end
Public Instance Methods
Define an alias to a nested key
@example
alias_setting(:foo, to: :bar)
@param [Array<String>] keys
the alias key
@api public
# File lib/tty/config.rb, line 390 def alias_setting(*keys, to: nil) flat_setting = flatten_keys(keys) alias_keys = Array(to) alias_key = flatten_keys(alias_keys) if alias_key == flat_setting raise ArgumentError, "Alias matches setting key" end if fetch(alias_key) raise ArgumentError, "Setting already exists with an alias " \ "'#{alias_keys.map(&:inspect).join(', ')}'" end @aliases[alias_key] = flat_setting end
Append values to an already existing nested key
@example
append(1, 2, to: %i[foo bar])
@param [Array<Object>] values
the values to append
@param [Array<String, Symbol] to
the nested key to append to
@return [Array<Object>]
the values for a nested key
@api public
# File lib/tty/config.rb, line 333 def append(*values, to: nil) keys = Array(to) set(*keys, value: Array(fetch(*keys)) + values) end
Add path to locations to search in
@example
append_path(Dir.pwd)
@param [String] path
the path to append
@return [Array<String>]
@api public
# File lib/tty/config.rb, line 137 def append_path(path) @location_paths << path end
Auto load env variables
@api public
# File lib/tty/config.rb, line 168 def autoload_env @autoload_env = true end
Check if env variables are auto loaded
@return [Boolean]
@api public
# File lib/tty/config.rb, line 161 def autoload_env? @autoload_env == true end
Delete a value from a nested key
@example
delete(:foo, :bar, :baz)
@example
delete(:unknown) { |key| "#{key} isn't set" }
@param [Array<String, Symbol>] keys
the keys for a value deletion
@yield [key] Invoke the block with a missing key
@return [Object]
the deleted value(s)
@api public
# File lib/tty/config.rb, line 376 def delete(*keys, &default) keys = convert_to_keys(keys) deep_delete(*keys, @settings, &default) end
Check if configuration file exists
@return [Boolean]
@api public
# File lib/tty/config.rb, line 439 def exist? !find_file.nil? end
Set extension name
@raise [TTY::Config::UnsupportedExtError]
api public
# File lib/tty/config.rb, line 118 def extname=(name) unless extensions.include?(name) raise UnsupportedExtError, "Config file format `#{name}` is not supported." end @extname = name end
Fetch value under a composite key
@example
fetch(:foo, :bar, :baz)
@example
fetch("foo.bar.baz")
@param [Array<String, Symbol>, String] keys
the keys to get value at
@param [Object] default
the default value
@return [Object]
@api public
# File lib/tty/config.rb, line 283 def fetch(*keys, default: nil, &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] end # then try default value = block || default if value.nil? while callable_without_params?(value) value = value.() end value end
Find configuration file matching filename and extension
@api private
# File lib/tty/config.rb, line 425 def find_file @location_paths.each do |location_path| path = search_in_path(location_path) return path if path end nil end
Merge in other configuration settings
@param [Hash{Symbol => Object]] other_settings
@return [Hash, nil]
the combined settings or nil
@api public
# File lib/tty/config.rb, line 313 def merge(other_settings) return unless other_settings.respond_to?(:to_hash) @settings = deep_merge(@settings, other_settings) end
Insert location path at the begining
@example
prepend_path(Dir.pwd)
@param [String] path
the path to prepend
@return [Array<String>]
@api public
# File lib/tty/config.rb, line 152 def prepend_path(path) @location_paths.unshift(path) end
Find and read a configuration file.
If the file doesn't exist or if there is an error loading it the TTY::Config::ReadError
will be raised.
@param [String] file
the path to the configuration file to be read
@param [String] format
the format to read configuration in
@raise [TTY::Config::ReadError]
@api public
# File lib/tty/config.rb, line 458 def read(file = find_file, format: :auto) if file.nil? raise ReadError, "No file found to read configuration from!" elsif !::File.exist?(file) raise ReadError, "Configuration file `#{file}` does not exist!" end set_file_metadata(file) ext = (format == :auto ? extname : ".#{format}") content = ::File.read(file) merge(unmarshal(content, ext: ext)) end
Remove a set of values from a nested key
@example
remove(1, 2, from: :foo)
@example
remove(1, 2, from: %i[foo bar])
@param [Array<Object>] values
the values to remove from a nested key
@param [Array<String, Symbol>, String] from
the nested key to remove values from
@api public
# File lib/tty/config.rb, line 352 def remove(*values, from: nil) keys = Array(from) raise ArgumentError, "Need to set key to remove from" if keys.empty? set(*keys, value: Array(fetch(*keys)) - values) end
Set a value for a composite key and overrides any existing keys Keys are case-insensitive
@example
set(:foo, :bar, :baz, value: 2)
@example
set(:foo, :bar, :baz) { 2 }
@example
set("foo.bar.baz", value: 2)
@param [Array<String, Symbol>, String] keys
the nested key to set value for
@param [Object] value
the value to set
@return [Object]
the set value
@api public
# File lib/tty/config.rb, line 193 def set(*keys, value: nil, &block) assert_either_value_or_block(value, block) keys = convert_to_keys(keys) key = flatten_keys(keys) value_to_eval = block || value if validators.key?(key) if callable_without_params?(value_to_eval) value_to_eval = delay_validation(key, value_to_eval) else assert_valid(key, value) end end deepest_setting = deep_set(@settings, *keys[0...-1]) deepest_setting[keys.last] = value_to_eval deepest_setting[keys.last] end
Set file name and extension
@example
set_file_metadata("config.yml")
@param [File] file
the file to set metadata for
@api public
# File lib/tty/config.rb, line 515 def set_file_metadata(file) self.extname = ::File.extname(file) self.filename = ::File.basename(file, extname) end
Bind a key to ENV variable
@example
set_from_env(:host) set_from_env(:foo, :bar) { 'HOST' }
@param [Array<String>] keys
the keys to bind to ENV variables
@api public
# File lib/tty/config.rb, line 244 def set_from_env(*keys, &block) key = flatten_keys(keys) env_key = block.nil? ? key : block.() env_key = to_env_key(env_key) @envs[key.to_s.downcase] = env_key end
Set a value for a composite key if not present already
@example
set_if_empty(:foo, :bar, :baz, value: 2)
@param [Array<String, Symbol>] keys
the keys to set value for
@param [Object] value
the value to set
@return [Object, nil]
the set value or nil
@api public
# File lib/tty/config.rb, line 227 def set_if_empty(*keys, value: nil, &block) keys = convert_to_keys(keys) return unless deep_fetch(@settings, *keys).nil? block ? set(*keys, &block) : set(*keys, value: value) end
Convert config key to standard ENV var name
@param [String] key
@return [String]
@api private
# File lib/tty/config.rb, line 258 def to_env_key(key) env_key = key.to_s.gsub(key_delim, env_separator).upcase if @env_prefix == "" env_key else "#{@env_prefix.to_s.upcase}#{env_separator}#{env_key}" end end
Current configuration
@api public
# File lib/tty/config.rb, line 523 def to_hash @settings.dup end
Register a validation rule for a nested key
@param [Array<String>] keys
a deep nested keys
@param [Proc] validator
the logic to use to validate given nested key
@api public
# File lib/tty/config.rb, line 415 def validate(*keys, &validator) key = flatten_keys(keys) values = validators[key] || [] values << validator validators[key] = values end
Write current configuration to a file.
@example
write(force: true, create: true)
@param [String] file
the file to write to
@param [Boolean] create
whether or not to create missing path directories, false by default
@param [Boolean] force
whether or not to overwrite existing configuration file, false by default
@param [String] format
the format name for the configuration file, :auto by defualt
@param [String] path
the custom path to use to write a file to
@raise [TTY::Config::WriteError]
@api public
# File lib/tty/config.rb, line 492 def write(file = find_file, create: false, force: false, format: :auto, path: nil) file = fullpath(file, path) check_can_write(file, force) set_file_metadata(file) ext = (format == :auto ? extname : ".#{format}") content = marshal(@settings, ext: ext) filepath = Pathname.new(file) create_missing_dirs(filepath, create) ::File.write(filepath, content) end
Private Instance Methods
Ensure that value is set either through parameter or block
@api private
# File lib/tty/config.rb, line 533 def assert_either_value_or_block(value, block) if value.nil? && block.nil? raise ArgumentError, "Need to set either value or block" elsif !(value.nil? || block.nil?) raise ArgumentError, "Can't set both value and block" end end
Check if key passes all registered validations for a key
@param [String] key
the key to validate a value for
@param [Object] value
the value to check
@api private
# File lib/tty/config.rb, line 578 def assert_valid(key, value) validators[key].each do |validator| validator.(key, value) end end
Check if object is a proc with no arguments
@return [Boolean]
@api private
# File lib/tty/config.rb, line 546 def callable_without_params?(object) object.respond_to?(:call) && (!object.respond_to?(:arity) || object.arity.zero?) end
Check if a file can be written to
@param [String] file
the configuration file
@param [Boolean] force
whether or not to force writing
@raise [TTY::Config::WriteError]
@return [nil]
@api private
# File lib/tty/config.rb, line 771 def check_can_write(file, force) return unless file && ::File.exist?(file) if !force raise WriteError, "File `#{file}` already exists. " \ "Use :force option to overwrite." elsif !::File.writable?(file) raise WriteError, "Cannot write to #{file}." end end
Convert key to an array of key elements
@param [String, Array<String, Symbol>] keys
@return [Array<String>]
@api private
# File lib/tty/config.rb, line 623 def convert_to_keys(keys) first_key = keys[0] if first_key.to_s.include?(key_delim) first_key.split(key_delim) else keys.map(&:to_s) end end
Crate a marshaller instance based on the extension name
@param [String] ext
the extension name
@return [nil, Marshaller]
@api private
# File lib/tty/config.rb, line 811 def create_marshaller(ext) marshaller = marshallers.find { |marsh| marsh.ext.include?(ext) } return nil if marshaller.nil? marshaller.new end
Create any missing directories
@param [Pathname] filepath
the file path
@param [Boolean] create
whether or not to create missing directories
@raise [TTY::Config::WriteError]
@return [nil]
@api private
# File lib/tty/config.rb, line 794 def create_missing_dirs(filepath, create) if !filepath.dirname.exist? && !create raise WriteError, "Directory `#{filepath.dirname}` doesn't exist. " \ "Use :create option to create missing directories." else filepath.dirname.mkpath end end
Delete a deeply nested key
@param [Array<String>] keys
the nested key to delete
@param [Hash{String => Object}]
the settings to delete key from
@return [Object]
the deleted object(s)
@api private
# File lib/tty/config.rb, line 706 def deep_delete(*keys, settings, &default) key, *rest = keys value = settings[key] if !rest.empty? && value.is_a?(::Hash) deep_delete(*rest, value, &default) elsif !value.nil? settings.delete(key) elsif default default.(key) end end
Fetch value under deeply nested keys with indiffernt key access
@param [Hash] settings
the settings to search
@param [Array<Object>] keys
the nested key to look up
@return [Object, nil]
the value or nil
@api private
# File lib/tty/config.rb, line 664 def deep_fetch(settings, *keys) key, *rest = keys value = settings.fetch(key.to_s, settings[key.to_sym]) if value.nil? || rest.empty? value else deep_fetch(value, *rest) end end
Merge two deeply nested hash objects
@param [Hash] this_hash @param [Hash] other_hash
@return [Hash]
the merged hash object
@api private
# File lib/tty/config.rb, line 683 def deep_merge(this_hash, other_hash, &block) this_hash.merge(other_hash) do |key, this_val, other_val| if this_val.is_a?(::Hash) && other_val.is_a?(::Hash) deep_merge(this_val, other_val, &block) elsif block_given? block[key, this_val, other_val] else other_val end end end
Set value under deeply nested keys
The scan starts with the top level key and follows a sequence of keys. In case where intermediate keys do not exist, a new hash is created.
@param [Hash] settings
@param [Array<Object>] keys
the keys to nest
@return [Hash]
the nested setting
@api private
# File lib/tty/config.rb, line 599 def deep_set(settings, *keys) return settings if keys.empty? key, *rest = *keys value = settings[key] if value.nil? && rest.empty? settings[key] = {} elsif value.nil? && !rest.empty? settings[key] = {} deep_set(settings[key], *rest) else # nested hash value present settings[key] = value deep_set(settings[key], *rest) end end
Wrap callback in a proc object that includes validation that will be performed at point when a new proc is invoked.
@param [String] key
the key to set validation for
@param [Proc] callback
the callback to wrap
@return [Proc]
@api private
# File lib/tty/config.rb, line 562 def delay_validation(key, callback) -> do val = callback.() assert_valid(key, val) val end end
Convert nested key from an array to a string
@example
flatten_keys(%i[foo bar baz]) # => "foo.bar.baz"
@param [Array<String, Symbol>] keys
the nested key to convert
@return [String]
the delimited nested key
@api private
# File lib/tty/config.rb, line 644 def flatten_keys(keys) first_key = keys[0] if first_key.to_s.include?(key_delim) first_key else keys.join(key_delim) end end
Create a full path to a configuration file
@param [String] file
the configuration file
@param [String] path
the path to configuration file
@return [String]
the full path to a file
@api private
# File lib/tty/config.rb, line 748 def fullpath(file, path) if file.nil? dir = path || @location_paths.first || Dir.pwd ::File.join(dir, "#{filename}#{@extname}") elsif file && path ::File.join(path, ::File.basename(file)) else file end end
Marshal hash object into a configuration file content
@param [Hash{String => Object}] object
the object to convert to string
@return [String]
@api private
# File lib/tty/config.rb, line 844 def marshal(object, ext: nil) ext ||= extname if marshaller = create_marshaller(ext) marshaller.marshal(object) else raise WriteError, "Config file format `#{ext}` is not supported." end end
Search for a configuration file in a path
@param [String] path
the path to search
@return [String, nil]
the configuration file path or nil
@api private
# File lib/tty/config.rb, line 727 def search_in_path(path) path = Pathname.new(path) extensions.each do |ext| if ::File.exist?(path.join("#{filename}#{ext}").to_s) return path.join("#{filename}#{ext}").to_s end end nil end
Unmarshal content into a hash object
@param [String] content
the content to convert into a hash object
@return [Hash{String => Object}]
@api private
# File lib/tty/config.rb, line 827 def unmarshal(content, ext: nil) ext ||= extname if marshaller = create_marshaller(ext) marshaller.unmarshal(content) else raise ReadError, "Config file format `#{ext}` is not supported." end end