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

env_prefix[RW]

The prefix used for searching ENV variables @api public

env_separator[RW]

The string used to separate parts in ENV variable name @api public

extname[R]

The name of the configuration file extension @api public

filename[RW]

The name of the configuration file without extension @api public

key_delim[R]

The key delimiter used for specifying deeply nested keys @api public

location_paths[R]

A collection of config paths @api public

validators[R]

The validations for this configuration @api public

Public Class Methods

coerce(hash, &block) click to toggle source

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
new(settings = {}) { |self| ... } click to toggle source

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
normalize_hash(hash, method = :to_sym) click to toggle source

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

alias_setting(*keys, to: nil) click to toggle source

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: nil) click to toggle source

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
append_path(path) click to toggle source

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
autoload_env() click to toggle source

Auto load env variables

@api public

# File lib/tty/config.rb, line 168
def autoload_env
  @autoload_env = true
end
autoload_env?() click to toggle source

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(*keys, &default) click to toggle source

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
exist?() click to toggle source

Check if configuration file exists

@return [Boolean]

@api public

# File lib/tty/config.rb, line 439
def exist?
  !find_file.nil?
end
Also aliased as: persisted?
extname=(name) click to toggle source

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(*keys, default: nil, &block) click to toggle source

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_file() click to toggle source

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
Also aliased as: source_file
merge(other_settings) click to toggle source

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
persisted?()
Alias for: exist?
prepend_path(path) click to toggle source

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
read(file = find_file, format: :auto) click to toggle source

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(*values, from: nil) click to toggle source

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(*keys, value: nil, &block) click to toggle source

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_metadata(file) click to toggle source

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
set_from_env(*keys, &block) click to toggle source

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_if_empty(*keys, value: nil, &block) click to toggle source

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
source_file()
Alias for: find_file
to_env_key(key) click to toggle source

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
to_h()
Alias for: to_hash
to_hash() click to toggle source

Current configuration

@api public

# File lib/tty/config.rb, line 523
def to_hash
  @settings.dup
end
Also aliased as: to_h
validate(*keys, &validator) click to toggle source

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(file = find_file, create: false, force: false, format: :auto, path: nil) click to toggle source

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

assert_either_value_or_block(value, block) click to toggle source

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
assert_valid(key, value) click to toggle source

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
callable_without_params?(object) click to toggle source

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_can_write(file, force) click to toggle source

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_to_keys(keys) click to toggle source

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
create_marshaller(ext) click to toggle source

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_missing_dirs(filepath, create) click to toggle source

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
deep_delete(*keys, settings, &default) click to toggle source

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
deep_fetch(settings, *keys) click to toggle source

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
deep_merge(this_hash, other_hash, &block) click to toggle source

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
deep_set(settings, *keys) click to toggle source

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
delay_validation(key, callback) click to toggle source

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
flatten_keys(keys) click to toggle source

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
fullpath(file, path) click to toggle source

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(object, ext: nil) click to toggle source

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_in_path(path) click to toggle source

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, ext: nil) click to toggle source

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