class BatchKit::Config

Defines a class for managing configuration properties; essentially, this is a hash that is case and Strng/Symbol insensitive with respect to keys; this means a value can be retrieved using any mix of case using either a String or Symbol as the lookup key.

In addition, there are some further conveniences added on:

Internally, the case and String/Symbol insensitivity is managed by maintaining a second Hash that converts the lower-cased symbol value of all keys to the actual keys used to store the object. When looking up a value, we use this lookup Hash to find the actual key used, and then lookup the value.

@note As this Config object is case and String/Symbol insensitive,

different case and type keys that convert to the same lookup key are
considered the same. The practical implication is that you can't have
two different values in this Config object where the keys differ only
in case and/or String/Symbol class.

Public Class Methods

expand_placeholders(str, properties, raise_on_unknown_var = false) click to toggle source

Expand any ${<variable>} or %{<variable>} placeholders in str from either the supplied props hash or the system environment variables. The props hash is assumed to contain string or symbol keys matching the variable name between ${ and } (or %{ and }) delimiters. If no match is found in the supplied props hash or the environment, the default behaviour returns the string with the placeholder variable still in place, but this behaviour can be overridden to cause an exception to be raised if desired.

@param str [String] A String to be expanded from 0 or more placeholder

substitutions

@param properties [Hash, Array<Hash>] A properties Hash or array of Hashes

from which placeholder variable values can be looked up.

@param raise_on_unknown_var [Boolean] Whether or not an exception should

be raised if no property is found for a placeholder expression. If false,
unrecognised placeholder variables are left in the returned string.

@return [String] A new string with placeholder variables replaced by

the values in +props+.
# File lib/batch-kit/config.rb, line 107
def self.expand_placeholders(str, properties, raise_on_unknown_var = false)
    chain = properties.is_a?(Hash) ? [properties] : properties.reverse
    str.gsub(/(?:[$%])\{([a-zA-Z0-9_]+)\}/) do
        case
        when src = chain.find{ |props| props.has_key?($1) ||
                               props.has_key?($1.intern) ||
                               props.has_key?($1.downcase.intern) }
            src[$1] || src[$1.intern] || src[$1.downcase.intern]
        when ENV[$1] then ENV[$1]
        when raise_on_unknown_var
            raise KeyError, "No value supplied for placeholder variable '#{$&}'"
        else
            $&
        end
    end
end
load(file, props = nil, options = {}) click to toggle source

Create a new Config object, and initialize it from the specified file.

@param file [String] A path to a properties or YAML file to load. @param props [Hash, Config] An optional Hash (or Config) object to

seed this Config object with.

@param options [Hash] An options hash. @option options [Boolean] :raise_on_unknown_var Whether to raise an

error if an unrecognised placeholder variable is encountered in the
file.

@option options [Boolean] :use_erb If true, the contents of file

is first run through ERB.

@option options [Binding] :binding The binding to use when evaluating

expressions in ERB

@return [Config] A new Config object populated from file and

+props+, where placeholder variables have been expanded.
# File lib/batch-kit/config.rb, line 57
def self.load(file, props = nil, options = {})
    cfg = self.new(props, options)
    cfg.load(file, options)
    cfg
end
new(hsh = nil, options = {}) click to toggle source

Create a Config object, optionally initialized from hsh.

@param hsh [Hash] An optional Hash to seed this Config object with. @param options [Hash] An options hash.

Calls superclass method
# File lib/batch-kit/config.rb, line 129
def initialize(hsh = nil, options = {})
    super(nil)
    @lookup_keys = {}
    @decryption_key = nil
    merge!(hsh, options) if hsh
end
properties_to_hash(str) click to toggle source

Converts a str in the form of a properties file into a Hash.

# File lib/batch-kit/config.rb, line 65
def self.properties_to_hash(str)
    hsh = props = {}
    str.each_line do |line|
        line.chomp!
        if match = /^\s*\[([A-Za-z0-9_ ]+)\]\s*$/.match(line)
            # Section heading
            props = hsh[match[1]] = {}
        elsif match = /^\s*([A-Za-z0-9_\.]+)\s*=\s*([^#]+)/.match(line)
            # Property setting
            val = match[2]
            props[match[1]] = case val
            when /^\d+$/ then val.to_i
            when /^\d*\.\d+$/ then val.to_f
            when /^:/ then val.intern
            when /false/i then false
            when /true/i then true
            else val
            end
        end
    end
    hsh
end

Public Instance Methods

[](key) click to toggle source

Override [] to be agnostic as to the case of the key, and whether it is a String or a Symbol.

Calls superclass method
# File lib/batch-kit/config.rb, line 326
def [](key)
    key = @lookup_keys[convert_key(key)]
    val = super(key)
    if @decryption_key && val.is_a?(String) && val =~ /!AES:([a-zA-Z0-9\/+=]+)!/
        begin
            val = Encryption.decrypt(@decryption_key, $1)
        rescue Exception => ex
            raise "An error occurred while decrypting the value for key '#{key}': #{ex.message}"
        end
    end
    val
end
[]=(key, val) click to toggle source

Override []= to be agnostic as to the case of the key, and whether it is a String or a Symbol.

Calls superclass method
# File lib/batch-kit/config.rb, line 342
def []=(key, val)
    std_key = convert_key(key)
    if @lookup_keys[std_key] != key
        delete(key)
        @lookup_keys[std_key] = key
    end
    super key, val
end
clone() click to toggle source

Override clone to also clone contents of @lookup_keys.

Calls superclass method
# File lib/batch-kit/config.rb, line 378
def clone
    copy = super
    copy.instance_variable_set(:@lookup_keys, @lookup_keys.clone)
    copy
end
decryption_key=(key) click to toggle source

If set, encrypted strings (only) will be decrypted when accessed via [] or method_missing (for property-like access, e.g. cfg.password).

@param key [String] The master encryption key used to encrypt sensitive

values in this Config object.
# File lib/batch-kit/config.rb, line 279
def decryption_key=(key)
    require_relative 'encryption'
    self.each do |_, val|
        val.decryption_key = key if val.is_a?(Config)
    end
    @decryption_key = key
end
Also aliased as: encryption_key=
delete(key) click to toggle source

Override delete to be agnostic as to the case of the key, and whether it is a String or a Symbol.

Calls superclass method
# File lib/batch-kit/config.rb, line 354
def delete(key)
    key = @lookup_keys.delete(convert_key(key))
    super key
end
dup() click to toggle source

Override dup to also clone contents of @lookup_keys.

Calls superclass method
# File lib/batch-kit/config.rb, line 386
def dup
    copy = super
    copy.instance_variable_set(:@lookup_keys, @lookup_keys.dup)
    copy
end
encode_with(coder) click to toggle source

Ensure that Config objects are saved as normal hashes when writing YAML.

# File lib/batch-kit/config.rb, line 232
def encode_with(coder)
    coder.represent_map nil, self
end
encrypt(key_pat, master_key = @decryption_key) click to toggle source

Recursively encrypts the values of all keys in this Config object that match key_pat.

Note: key_pat will be compared against the standardised key values of each object (i.e. lowercase, with spaces converted to _).

@param key_pat [Regexp|String] A regular expression to be used to identify

the keys that should be encrypted, e.g. /password/ would encrypt all
values that have "password" in their key.

@param master_key [String] The master key that should be used when

encrypting. If not specified, uses the current value of the
+decryption_key+ set for this Config object.
# File lib/batch-kit/config.rb, line 301
def encrypt(key_pat, master_key = @decryption_key)
    key_pat = Regexp.new(key_pat, true) if key_pat.is_a?(String)
    raise ArgumentError, "key_pat must be a Regexp or String" unless key_pat.is_a?(Regexp)
    raise ArgumentError, "No master key has been set or passed" unless master_key
    require_relative 'encryption'
    self.each do |key, val|
        if Config === val
            val.encrypt(key_pat, master_key)
        else
            if @decryption_key && val.is_a?(String) && val =~ /!AES:([a-zA-Z0-9\/+=]+)!/
                # Decrypt using old master key
                val = self[key]
                self[key] = val
            end
            if val.is_a?(String) && convert_key(key) =~ key_pat
                self[key] = "!AES:#{Encryption.encrypt(master_key, val).strip}!"
            end
        end
    end
    @decryption_key = master_key
end
encryption_key=(key)
Alias for: decryption_key=
expand_placeholders(str, raise_on_unknown_var = true) click to toggle source

Expand any ${<variable>} or %{<variable>} placeholders in str from this Config object or the system environment variables. This Config object is assumed to contain string or symbol keys matching the variable name between ${ and } (or %{ and }) delimiters. If no match is found in the supplied props hash or the environment, the default behaviour is to raise an exception, but this can be overriden to leave the placeholder variable still in place if desired.

@param str [String] A String to be expanded from 0 or more placeholder

substitutions

@param raise_on_unknown_var [Boolean] Whether or not an exception should

be raised if no property is found for a placeholder expression. If false,
unrecognised placeholder variables are left in the returned string.

@return [String] A new string with placeholder variables replaced by

the values in +props+.
# File lib/batch-kit/config.rb, line 447
def expand_placeholders(str, raise_on_unknown_var = true)
    self.class.expand_placeholders(str, self, raise_on_unknown_var)
end
fetch(key, *rest) click to toggle source

Override fetch to be agnostic as to the case of the key, and whether it is a String or a Symbol.

Calls superclass method
# File lib/batch-kit/config.rb, line 371
def fetch(key, *rest)
    key = @lookup_keys[convert_key(key)] || key
    super
end
has_key?(key) click to toggle source

Override has_key? to be agnostic as to the case of the key, and whether it is a String or a Symbol.

Calls superclass method
# File lib/batch-kit/config.rb, line 362
def has_key?(key)
    key = @lookup_keys[convert_key(key)]
    super key
end
Also aliased as: include?
include?(key)
Alias for: has_key?
load(path, options = {}) click to toggle source

Read a properties or YAML file at the path specified in path, and load the contents to this Config object.

@param path [String] The path to the properties or YAML file to be

loaded.

@param options [Hash] An options hash. @option options [Boolean] @raise_on_unknown_var Whether to raise an

error if an unrecognised placeholder variable is encountered in the
file.
# File lib/batch-kit/config.rb, line 146
def load(path, options = {})
    props = case File.extname(path)
    when /\.yaml/i then self.load_yaml(path, options)
    else self.load_properties(path, options)
    end
end
load_properties(prop_file, options = {}) click to toggle source

Process a property file, returning its contents as a Hash. Only lines of the form KEY=VALUE are processed, and # indicates the start of a comment. Property files can contain sections, denoted by [SECTION].

@example

If a properties file contains the following:

  FOO=Bar             # This is a comment
  BAR=${FOO}\Baz

  [BAT]
  Car=Ford

Then we would return a Config object containing the following:

  {'FOO' => 'Bar', 'BAR' => 'Bar\Baz', 'BAT' => {'Car' => 'Ford'}}

This config content could be accessed via #[] or as properties of
the Config object, e.g.

  cfg[:foo]    # => 'Bar'
  cfg.bar      # => 'Bar\Baz'
  cfg.bat.car  # => 'Ford'

@param prop_file [String] A path to the properties file to be parsed. @param options [Hash] An options hash. @option options [Boolean] :use_erb If true, the contents of prop_file

is first passed through ERB before being processed. This allows for
the use of <% %> and <%= %> directives in +prop_file+. The binding
passed to ERB is the value of any :binding option specified, or else
this Config object. If not specified, ERB is used if the file is
found to contain the string '<%'.

@option options [Binding] :binding The binding for ERB to use when

processing expressions. Defaults to this Config instance if not
specified.

@return [Hash] The parsed contents of the file as a Hash.

# File lib/batch-kit/config.rb, line 190
def load_properties(prop_file, options = {})
    str = read_file(prop_file, options)
    hsh = self.class.properties_to_hash(str)
    self.merge!(hsh, options)
end
load_yaml(yaml_file, options = {}) click to toggle source

Load the YAML file at yaml_file.

@param yaml_file [String] A path to a YAML file to be loaded. @param options [Hash] An options hash. @option options [Boolean] :use_erb If true, the contents of yaml_file

are run through ERB before being parsed as YAML. This allows for use
of <% %> and <%= %> directives in +yaml_file+. The binding passed to
ERB is the value of any :binding option specified, or else this
Config object. If not specified, ERB is used if the file is found to
contain the string '<%'.

@option options [Binding] :binding The binding for ERB to use when

processing expressions. Defaults to this Config instance if not
specified.

@return [Object] The results of parsing the YAML contents of yaml_file.

# File lib/batch-kit/config.rb, line 211
def load_yaml(yaml_file, options = {})
    require 'yaml'
    str = read_file(yaml_file, options)
    yaml = YAML.load(str)
    self.merge!(yaml, options)
end
merge(hsh, options = {}) click to toggle source

Merge the contents of the specified hsh into a new Config object.

@param hsh [Hash] The Hash object to merge with this Config object. @param options [Hash] An options hash. @option options [Boolean] @raise_on_unknown_var Whether to raise an

error if an unrecognised placeholder variable is encountered in the
file.

@return A new Config object with the combined contents of this Config

object plus the contents of +hsh+.
# File lib/batch-kit/config.rb, line 267
def merge(hsh, options = {})
    cfg = self.dup
    cfg.merge!(hsh, options)
    cfg
end
merge!(hsh, options = {}) click to toggle source

Merge the contents of the specified hsh into this Config object.

@param hsh [Hash] The Hash object to merge into this Config object. @param options [Hash] An options hash. @option options [Boolean] :raise_on_unknown_var Whether to raise an

exception if an unrecognised placeholder variable is encountered in
+hsh+.
# File lib/batch-kit/config.rb, line 244
def merge!(hsh, options = {})
    if hsh && !hsh.is_a?(Hash)
        raise ArgumentError, "Only Hash objects can be merged into Config (got #{hsh.class.name})"
    end
    hsh && hsh.each do |key, val|
        self[key] = convert_val(val, options[:raise_on_unknown_var])
    end
    if hsh.is_a?(Config)
        @decryption_key = hsh.instance_variable_get(:@decryption_key) unless @decryption_key
    end
    self
end
method_missing(name, *args) click to toggle source

Override method_missing to respond to method calls with the value of the property, if this Config object contains a property of the same name.

# File lib/batch-kit/config.rb, line 401
def method_missing(name, *args)
    if name =~ /^(.+)\?$/
        has_key?($1)
    elsif has_key?(name)
        self[name]
    elsif has_key?(name.to_s.gsub('_', ''))
        self[name.to_s.gsub('_', '')]
    elsif name =~ /^(.+)=$/
        self[$1]= args.first
    else
        raise ArgumentError, "No configuration entry for key '#{name}'"
    end
end
read_template(template_name, raise_on_unknown_var = true) click to toggle source

Reads a template file at template_name, and expands any substitution variable placeholder strings from this Config object.

@param template_name [String] The path to the template file containing

placeholder variables to expand from this Config object.

@return [String] The contents of the template file with placeholder

variables replaced by the content of this Config object.
# File lib/batch-kit/config.rb, line 459
def read_template(template_name, raise_on_unknown_var = true)
    template = IO.read(template_name)
    expand_placeholders(template, raise_on_unknown_var)
end
respond_to?(name) click to toggle source

Override respond_to? to indicate which methods we will accept.

Calls superclass method
# File lib/batch-kit/config.rb, line 417
def respond_to?(name)
    if name =~ /^(.+)\?$/
        has_key?($1)
    elsif has_key?(name)
        true
    elsif has_key?(name.to_s.gsub('_', ''))
        true
    elsif name =~ /^(.+)=$/
        true
    else
        super
    end
end
save_yaml(yaml_file, options = {}) click to toggle source

Save this config object as a YAML file at yaml_file.

@param yaml_file [String] A path to the YAML file to be saved. @param options [Hash] An options hash.

# File lib/batch-kit/config.rb, line 223
def save_yaml(yaml_file, options = {})
    require 'yaml'
    str = self.to_yaml
    File.open(yaml_file, 'wb'){ |f| f.puts(str) }
end
to_cfg() click to toggle source

Override Hash core extension method to_cfg and just return self.

# File lib/batch-kit/config.rb, line 394
def to_cfg
    self
end

Private Instance Methods

convert_key(key) click to toggle source

Convert the supplied key to a lower-case symbol representation, which is the key to the @lookup_keys hash.

# File lib/batch-kit/config.rb, line 484
def convert_key(key)
    key.to_s.downcase.gsub(' ', '_').intern
end
convert_val(val, raise_on_unknown_var, parents = [self]) click to toggle source

Convert a value before merging it into the Config. This consists of these tasks:

- Converting Hashes to Config objects
- Propogating decryption keys to child Config objects
- Expanding placeholder variables in strings
# File lib/batch-kit/config.rb, line 494
def convert_val(val, raise_on_unknown_var, parents = [self])
    case val
    when Config then val
    when Hash
        cfg = Config.new
        cfg.instance_variable_set(:@decryption_key, @decryption_key)
        new_parents = parents.clone
        new_parents << cfg
        val.each do |k, v|
            cfg[k] = convert_val(v, raise_on_unknown_var, new_parents)
        end
        cfg
    when Array
        val.map{ |v| convert_val(v, raise_on_unknown_var, parents) }
    when /[$%]\{[a-zA-Z0-9_]+\}/
        self.class.expand_placeholders(val, parents, raise_on_unknown_var)
    else val
    end
end
read_file(file, options) click to toggle source

Reads the contents of file into a String. The file is passed through ERB if the :use_erb option is true, or if the options does not contain the :use_erb key and the file does contain the string '<%'.

# File lib/batch-kit/config.rb, line 472
def read_file(file, options)
    str = IO.read(file)
    if (options.has_key?(:use_erb) && options[:use_erb]) || str =~ /<%/
        require 'erb'
        str = ERB.new(str).result(options[:binding] || binding)
    end
    str
end