Class: Jeckyl::Config

Inherits:
Hash
  • Object
show all
Includes:
Helpers
Defined in:
lib/jeckyl.rb

Overview

This is the main Jeckyl class from which to create specific application classes. For example, to create a new set of parameters, define a class as

class MyConfig < Jeckyl::Config

More details are available in the Readme file

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Helpers

#a_boolean, #a_flag, #a_hash, #a_matching_string, #a_member_of, #a_number, #a_positive_number, #a_readable_dir, #a_readable_file, #a_string, #a_type_of, #a_writable_dir, #an_array, #an_array_of, #an_executable, #in_range

Constructor Details

#initialize(config_file = nil, opts = {}) ⇒ Config

create a configuration hash by evaluating the parameters defined in the given config file.

If no config file is given then the hash of options returned will only have the defaults defined for the given class.

Parameters:

  • config_file (String) (defaults to: nil)

    string path to a ruby file,

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

    contains the following options.

Options Hash (opts):

  • :flag_errors_on_defaults (Boolean)

    will raise exceptions from checks during default evaluation - although why is not clear, so best not to use it.

  • :local (Boolean)

    limits generated defaults to the direct class being evaluated and should only be set internally on this call

  • :relax, (Boolean)

    if set to true will not check for parameter methods but instead add unknown methods to the hash unchecked.



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/jeckyl.rb', line 59

def initialize(config_file=nil, opts={})
  # do whatever a hash has to do
  super()
  
  flag_errors_on_defaults = opts[:flag_errors_on_defaults] || false
  local = opts[:local] || false
  @_relax = opts[:relax] || false

  # somewhere to save the most recently set symbol
  @_last_symbol = nil
  # hash for comments accessed with the same symbol
  @_comments = {}
  # hash for input defaults
  @_defaults = {}
  # hash for optparse options
  @_options = {}
  # hash for short descriptions
  @_descriptions = {}
  # save order in which methods are defined for generating config files
  @_order = Array.new

  # get the defaults defined in the config parser
  get_defaults(:local=> local, :flag_errors => flag_errors_on_defaults)
  
  self[:config_files] = Array.new

  return self if config_file.nil?

  # remember where the config file itself is
  self[:config_files] = [config_file]
  
  # and finally get the values from the config file itself
  self.instance_eval(File.read(config_file), config_file)

rescue SyntaxError => err
  raise ConfigSyntaxError, err.message
rescue Errno::ENOENT
  # duff file path so tell the caller
  raise ConfigFileMissing, "#{config_file}"
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(symb, parameter) ⇒ Object (private)

decides what to do with parameters that have not been defined. unless @_relax then it will raise an exception. Otherwise it will create a key value pair

This method also remembers the method name as the key to prevent the parsers etc from having to carry this around just to do things like report on it.



440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
# File 'lib/jeckyl.rb', line 440

def method_missing(symb, parameter)

  @_last_symbol = symb
  #@parameter = parameter
  method_to_call = ("#{self.prefix}_" + symb.to_s).to_sym
  set_method = self.method(method_to_call)

  self[@_last_symbol] = set_method.call(parameter)

rescue NameError
  #raise if @@debug
  # no parser method defined.
  unless @_relax then
    # not tolerable
    raise UnknownParameter, format_error(symb, parameter, "Unknown parameter")
  else
    # feeling relaxed, so lets store it anyway.
    self[symb] = parameter
  end

end

Class Method Details

.check_config(config_file, report_file = nil) ⇒ Boolean

a class method to check a given config file one item at a time

This evaluates the given config file and reports if there are any errors to the report_file, which defaults to Stdout. Can only do the checking one error at a time.

To use this method, it is necessary to write a script that calls it for the particular subclass.

Parameters:

  • config_file (String)

    is the file to check

  • report_file (String) (defaults to: nil)

    is a file to write the report to, or stdout

Returns:

  • (Boolean)

    indicates if the check was OK or not



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/jeckyl.rb', line 138

def self.check_config(config_file, report_file=nil)

  # create myself to generate defaults, but nothing else
  me = self.new

  success = true
  message = "No errors found in: #{config_file}"

  begin
    # evaluate the config file
    me.instance_eval(File.read(config_file), config_file)

  rescue Errno::ENOENT
    message = "No such config file: #{config_file}"
    success = false
  rescue JeckylError => err
    message = err.message
    success = false
  rescue SyntaxError => err
    message = err.message
    success = false
  end

  begin
    if report_file.nil? then
      puts message
    else
      File.open(report_file, "w") do |rfile|
        rfile.puts message
      end
    end
    return success
  rescue Errno::ENOENT
    raise ReportFileError, "Error with file: #{report_file}"
  end

end

.descendantsArray

return a list of descendant classes in the current context. This is provided to help find classes for the jeckyl utility, e.g. to generate a default config file

Returns:

  • (Array)

    classes that are descendants of this class, sorted with the least ancestral first



239
240
241
242
243
244
# File 'lib/jeckyl.rb', line 239

def self.descendants
  descs = Array.new
  ObjectSpace.each_object {|obj| descs << obj if obj.kind_of?(Class) && obj < self}
  descs.sort! {|a,b| a < b ? -1 : 1}
  return descs
end

.generate_config(local = false) ⇒ Object

a class method to generate a config file from the class definition

This calls each of the parameter methods, and creates a commented template with the comments and default lines

Parameters:

  • local (Boolean) (defaults to: false)

    when set to true will limit the parameters to those defined in the immediate class and excludes any ancestors.



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/jeckyl.rb', line 184

def self.generate_config(local=false)
  me = self.new(nil, :local => local)
  # everything should now exist
  me._order.each do |key|
    
    if me._descriptions.has_key?(key) then
      puts "# #{me._descriptions[key]}"
      puts "#"
    end

    if me._comments.has_key?(key) then
      me._comments[key].each do |comment|
        puts "# #{comment}"
      end
    end
    # output an option description if needed
    if me._options.has_key?(key) then
      puts "#"
      puts "# Optparse options for this parameter:"
      puts "#  #{me._options[key].join(", ")}"
      puts "#"
    end
    def_value = me._defaults[key]
    default = def_value.nil? ? '' : def_value.inspect

    puts "##{key.to_s} #{default}"
    puts ""
  end
end

.get_config_opt(args, c_file) ⇒ Object

get a config file option from the given command line args

This is needed with the optparse methods for obvious reasons - the options can only be parsed once and you may want to parse them with a config file specified on the command line. This does it the old-fashioned way and strips the option from the command line arguments.

Note that the optparse method also includes this option but just for the benefit of –help

Parameters:

  • args (Array)

    which should usually be set to ARGV

  • c_file (String)

    being the path to the config file, which will be updated with the command line option if specified.



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/jeckyl.rb', line 259

def self.get_config_opt(args, c_file)
  #c_file = nil
  if arg_index = args.index('-c') then
    # got a -c option so expect a file next
    c_file = args[arg_index + 1]
    
    # check the file exists
    if c_file && FileTest.readable?(c_file) then
      # it does so strip the args out
      args.slice!(arg_index, 2)

    end
  end
  return [args, c_file]
end

.intersection(full_config) ⇒ Hash

Note:

this returns a plain hash and not an instance of Jeckyl::Config

extract only those parameters in a hash that are from the given class

Parameters:

  • full_config (Hash)

    is the config from which to extract the intersecting options and it can be an instance of Jeckyl::Config or a hash

Returns:

  • (Hash)

    containing all of the intersecting parameters



222
223
224
225
226
227
228
229
230
231
# File 'lib/jeckyl.rb', line 222

def self.intersection(full_config)
  me = self.new # create the defaults for this class
  my_hash = {}
  me._order.each do |my_key|
    if full_config.has_key?(my_key) then
      my_hash[my_key] = full_config[my_key]
    end
  end
  return my_hash
end

Instance Method Details

#_commentsObject

gives access to a hash containing an entry for each parameter and the comments defined by the class definitions - used internally by class methods



102
103
104
# File 'lib/jeckyl.rb', line 102

def _comments
  @_comments
end

#_defaultsObject

this contains a hash of the defaults for each parameter - used internally by class methods



112
113
114
# File 'lib/jeckyl.rb', line 112

def _defaults
  @_defaults
end

#_descriptionsObject

return has of descriptions



122
123
124
# File 'lib/jeckyl.rb', line 122

def _descriptions
  @_descriptions
end

#_optionsObject

return hash of options - used internally to generate files etc



117
118
119
# File 'lib/jeckyl.rb', line 117

def _options
  @_options
end

#_orderObject

This contains an array of the parameter names - used internally by class methods



107
108
109
# File 'lib/jeckyl.rb', line 107

def _order
  @_order
end

#complement(conf_to_remove) ⇒ Object

Delete those parameters that are in the given hash from this instance of Jeckyl::Config. Useful for tailoring parameter sets to specific uses (e.g. removing logging parameters)

Parameters:

  • conf_to_remove (Hash)

    which is a hash or an instance of Jeckyl::Config



295
296
297
# File 'lib/jeckyl.rb', line 295

def complement(conf_to_remove)
  self.delete_if {|key, value| conf_to_remove.has_key?(key)}
end

#merge(conf_file) ⇒ Object

Read, check and merge another parameter file into this one, being of the same config class.

If the file does not exist then silently ignore the merge

Parameters:

  • conf_file (String)
    • path to file to parse



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/jeckyl.rb', line 305

def merge(conf_file)
  
  if conf_file.kind_of?(Hash) then
    self.merge!(conf_file)
  else
    
    return unless FileTest.exists?(conf_file)
    
    self[:config_files] << conf_file
    
    # get the values from the config file itself
    self.instance_eval(File.read(conf_file), conf_file)
  end
rescue SyntaxError => err
  raise ConfigSyntaxError, err.message
rescue Errno::ENOENT
  # duff file path so tell the caller
  raise ConfigFileMissing, "#{conf_file}"
end

#optparse(args = ARGV) { ... } ⇒ Object

parse the given command line using the defined options

Parameters:

  • args (Array) (defaults to: ARGV)

    which should usually be ARGV

Yields:

  • self and optparse object to allow incidental options to be added

Returns:

  • false if –help so that the caller can decide what to do (e.g. exit)



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/jeckyl.rb', line 330

def optparse(args=ARGV)
  
  # ensure calls to parameter methods do not trample on things
  @_last_symbol = nil
  
  opts = OptionParser.new
  # get the prefix for parameter methods (once)
  prefix = self.prefix
  
  opts.on('-c', '--config-file [FILENAME]', String, 'specify an alternative config file')
  
  # need to define usage etc
  
  # loop through each of the options saved
  @_options.each_pair do |param, options|
    
    options << @_descriptions[param] if @_descriptions.has_key?(param)
    
    # get the method itself to call with the given arg
    pref_method = self.method("#{prefix}_#{param}".to_sym)
    
    # now process the option
    opts.on(*options) do |val|
      # and save the results having passed it through the parameter method
      self[param] = pref_method.call(val)
      
    end
  end
  
  # allow non-jeckyl options to be added (without the checks!)
  if block_given? then
    # pass out self to allow parameters to be saved and the opts object
    yield(self, opts)
  end
  
  # add in a little bit of help
  opts.on_tail('-h', '--help', 'you are looking at it') do
    puts opts
    return false
  end
  
  opts.parse!(args)
  
  return true

end

#prefixObject

set the prefix to the parameter names that should be used for corresponding parameter methods defined for a subclass. Parameter names in config files are mapped onto parameter method by prefixing the methods with the results of this function. So, for a parameter named ‘greeting’, the parameter method used to check the parameter will be, by default, ‘configure_greeting’.

For example, to define parameter methods prefix with ‘set’ redefine this method to return ‘set’. The greeting parameter method should then be called ‘set_greeting’



286
287
288
# File 'lib/jeckyl.rb', line 286

def prefix
  'configure'
end

#to_s(opts = {}) ⇒ Object

output the hash as a formatted set



378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/jeckyl.rb', line 378

def to_s(opts={})
  keys = self.keys.collect {|k| k.to_s}
  cols = 0
  strs = Array.new
  keys.each {|k| cols = k.length if k.length > cols}
  keys.sort.each do |key_s|
    str = '  '
    str << key_s.ljust(cols)
    key = key_s.to_sym
    desc = @_descriptions[key]
    value = self[key].inspect
    str << ": #{value}"
    str << " (#{desc})" unless desc.nil?
    strs << str
  end
  return strs.join("\n")
end