Class: BatchKit::Config

Inherits:
Hash
  • Object
show all
Defined in:
lib/batch-kit/config.rb

Overview

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.

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:

  • Items can be accessed by either [] (using a String or Symbol key) or as methods on the Config object, i.e. the following are all equivalent:

    config['Foo']
    config[:foo]
    config.foo
    
  • Contents can be loaded from:

    • an existing Hash object

    • a properties file using [Section] and KEY=VALUE syntax

    • a YAML file

  • String values can contain placeholder variables that will be replaced when the item is added to the Config collection. Placeholder variables are denoted by $<variable> or %<variable> syntax, where <variable> is the name of another configuration variable or a key in a supplied expansion properties hash.

  • Support for encrypted values. These are decrypted on the fly, provided a decryption key has been set on the Config object.

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.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(hsh = nil, options = {}) ⇒ Config

Create a Config object, optionally initialized from hsh.

Parameters:

  • hsh (Hash) (defaults to: nil)

    An optional Hash to seed this Config object with.

  • options (Hash) (defaults to: {})

    An options hash.



129
130
131
132
133
134
# 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

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args) ⇒ Object

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.



401
402
403
404
405
406
407
408
409
410
411
412
413
# 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

Class Method Details

.expand_placeholders(str, properties, raise_on_unknown_var = false) ⇒ String

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.

Parameters:

  • str (String)

    A String to be expanded from 0 or more placeholder substitutions

  • properties (Hash, Array<Hash>)

    A properties Hash or array of Hashes from which placeholder variable values can be looked up.

  • raise_on_unknown_var (Boolean) (defaults to: false)

    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.

Returns:

  • (String)

    A new string with placeholder variables replaced by the values in props.



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# 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 = {}) ⇒ Config

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

Parameters:

  • file (String)

    A path to a properties or YAML file to load.

  • props (Hash, Config) (defaults to: nil)

    An optional Hash (or Config) object to seed this Config object with.

  • options (Hash) (defaults to: {})

    An options hash.

Options Hash (options):

  • :raise_on_unknown_var (Boolean)

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

  • :use_erb (Boolean)

    If true, the contents of file is first run through ERB.

  • :binding (Binding)

    The binding to use when evaluating expressions in ERB

Returns:

  • (Config)

    A new Config object populated from file and props, where placeholder variables have been expanded.



57
58
59
60
61
# 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

.properties_to_hash(str) ⇒ Object

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



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# 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

Instance Method Details

#[](key) ⇒ Object

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



326
327
328
329
330
331
332
333
334
335
336
337
# 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) ⇒ Object

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



342
343
344
345
346
347
348
349
# 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

#cloneObject

Override #clone to also clone contents of @lookup_keys.



378
379
380
381
382
# 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) ⇒ Object Also known as: encryption_key=

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

Parameters:

  • key (String)

    The master encryption key used to encrypt sensitive values in this Config object.



279
280
281
282
283
284
285
# 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

#delete(key) ⇒ Object

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



354
355
356
357
# File 'lib/batch-kit/config.rb', line 354

def delete(key)
    key = @lookup_keys.delete(convert_key(key))
    super key
end

#dupObject

Override #dup to also clone contents of @lookup_keys.



386
387
388
389
390
# 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) ⇒ Object

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



232
233
234
# 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) ⇒ Object

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 _).

Parameters:

  • 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.

  • master_key (String) (defaults to: @decryption_key)

    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.

Raises:

  • (ArgumentError)


301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# 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

#expand_placeholders(str, raise_on_unknown_var = true) ⇒ String

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.

Parameters:

  • str (String)

    A String to be expanded from 0 or more placeholder substitutions

  • raise_on_unknown_var (Boolean) (defaults to: true)

    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.

Returns:

  • (String)

    A new string with placeholder variables replaced by the values in props.



447
448
449
# 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) ⇒ Object

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



371
372
373
374
# File 'lib/batch-kit/config.rb', line 371

def fetch(key, *rest)
    key = @lookup_keys[convert_key(key)] || key
    super
end

#has_key?(key) ⇒ Boolean Also known as: include?

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

Returns:

  • (Boolean)


362
363
364
365
# File 'lib/batch-kit/config.rb', line 362

def has_key?(key)
    key = @lookup_keys[convert_key(key)]
    super key
end

#load(path, options = {}) ⇒ Object

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

Parameters:

  • path (String)

    The path to the properties or YAML file to be loaded.

  • options (Hash) (defaults to: {})

    An options hash.

Options Hash (options):

  • @raise_on_unknown_var (Boolean)

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



146
147
148
149
150
151
# 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 = {}) ⇒ Hash

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].

Examples:

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'

Parameters:

  • prop_file (String)

    A path to the properties file to be parsed.

  • options (Hash) (defaults to: {})

    An options hash.

Options Hash (options):

  • :use_erb (Boolean)

    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 ‘<%’.

  • :binding (Binding)

    The binding for ERB to use when processing expressions. Defaults to this Config instance if not specified.

Returns:

  • (Hash)

    The parsed contents of the file as a Hash.



190
191
192
193
194
# 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 = {}) ⇒ Object

Load the YAML file at yaml_file.

Parameters:

  • yaml_file (String)

    A path to a YAML file to be loaded.

  • options (Hash) (defaults to: {})

    An options hash.

Options Hash (options):

  • :use_erb (Boolean)

    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 ‘<%’.

  • :binding (Binding)

    The binding for ERB to use when processing expressions. Defaults to this Config instance if not specified.

Returns:

  • (Object)

    The results of parsing the YAML contents of yaml_file.



211
212
213
214
215
216
# 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 = {}) ⇒ Object

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

Parameters:

  • hsh (Hash)

    The Hash object to merge with this Config object.

  • options (Hash) (defaults to: {})

    An options hash.

Options Hash (options):

  • @raise_on_unknown_var (Boolean)

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

Returns:

  • A new Config object with the combined contents of this Config object plus the contents of hsh.



267
268
269
270
271
# File 'lib/batch-kit/config.rb', line 267

def merge(hsh, options = {})
    cfg = self.dup
    cfg.merge!(hsh, options)
    cfg
end

#merge!(hsh, options = {}) ⇒ Object

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

Parameters:

  • hsh (Hash)

    The Hash object to merge into this Config object.

  • options (Hash) (defaults to: {})

    An options hash.

Options Hash (options):

  • :raise_on_unknown_var (Boolean)

    Whether to raise an exception if an unrecognised placeholder variable is encountered in hsh.



244
245
246
247
248
249
250
251
252
253
254
255
# 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

#read_template(template_name, raise_on_unknown_var = true) ⇒ String

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

Parameters:

  • template_name (String)

    The path to the template file containing placeholder variables to expand from this Config object.

Returns:

  • (String)

    The contents of the template file with placeholder variables replaced by the content of this Config object.



459
460
461
462
# 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) ⇒ Boolean

Override respond_to? to indicate which methods we will accept.

Returns:

  • (Boolean)


417
418
419
420
421
422
423
424
425
426
427
428
429
# 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 = {}) ⇒ Object

Save this config object as a YAML file at yaml_file.

Parameters:

  • yaml_file (String)

    A path to the YAML file to be saved.

  • options (Hash) (defaults to: {})

    An options hash.



223
224
225
226
227
# 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_cfgObject

Override Hash core extension method #to_cfg and just return self.



394
395
396
# File 'lib/batch-kit/config.rb', line 394

def to_cfg
    self
end