class Jeckyl::Config
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 {file:README.md Readme} file
Public Class Methods
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.
@param [String] config_file is the file to check @param [String] report_file is a file to write the report to, or stdout @return [Boolean] indicates if the check was OK or not
# 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
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
@return [Array] classes that are descendants of this class, sorted with the least ancestral
first
# 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
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
@param [Boolean] local when set to true will limit the parameters to those defined in the
immediate class and excludes any ancestors.
# 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 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
@param [Array] args which should usually be set to ARGV @param [String] c_file being the path to the config file, which will be
updated with the command line option if specified.
# 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
extract only those parameters in a hash that are from the given class
@param [Hash] full_config is the config from which to extract the intersecting options
and it can be an instance of Jeckyl::Config or a hash
@return [Hash] containing all of the intersecting parameters
@note this returns a plain hash and not an instance of Jeckyl::Config
# 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
create a configuration hash by evaluating the parameters defined in the given config file.
@param [String] config_file string path to a ruby file, @param [Hash] opts contains the following options. @option opts [Boolean] :flag_errors_on_defaults will raise exceptions from checks during default
evaluation - although why is not clear, so best not to use it.
@option opts [Boolean] :local limits generated defaults to the direct class being evaluated
and should only be set internally on this call
@option opts [Boolean] :relax, if set to true will not check for parameter methods but instead
add unknown methods to the hash unchecked.
If no config file is given then the hash of options returned will only have the defaults defined for the given class.
# 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
Public Instance Methods
gives access to a hash containing an entry for each parameter and the comments defined by the class definitions - used internally by class methods
# File lib/jeckyl.rb, line 102 def _comments @_comments end
this contains a hash of the defaults for each parameter - used internally by class methods
# File lib/jeckyl.rb, line 112 def _defaults @_defaults end
return has of descriptions
# File lib/jeckyl.rb, line 122 def _descriptions @_descriptions end
return hash of options - used internally to generate files etc
# File lib/jeckyl.rb, line 117 def _options @_options end
This contains an array of the parameter names - used internally by class methods
# File lib/jeckyl.rb, line 107 def _order @_order end
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)
@param [Hash] conf_to_remove which is a hash or an instance of Jeckyl::Config
# File lib/jeckyl.rb, line 295 def complement(conf_to_remove) self.delete_if {|key, value| conf_to_remove.has_key?(key)} end
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
@param [String] conf_file - path to file to parse
# 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
parse the given command line using the defined options
@param [Array] args which should usually be ARGV @yield self and optparse object to allow incidental options to be added @return false if –help so that the caller can decide what to do (e.g. exit)
# 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
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'
# File lib/jeckyl.rb, line 286 def prefix 'configure' end
output the hash as a formatted set
# 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
Protected Instance Methods
create a description for the current parameter, to be used when generating a config template
@param [*String] strings being one or more string arguments that are used to generate config file templates
and documents
# File lib/jeckyl.rb, line 403 def comment(*strings) @_comments[@_last_symbol] = strings unless @_last_symbol.nil? end
set default value(s) for the current parameter.
@param [Object] val - any valid object as expected by the parameter method
# File lib/jeckyl.rb, line 410 def default(val) return if @_last_symbol.nil? || @_defaults.has_key?(@_last_symbol) @_defaults[@_last_symbol] = val end
set optparse description for the parameter
@param [Array] str - options using the same format as optparse
# File lib/jeckyl.rb, line 425 def describe(str) @_descriptions[@_last_symbol] = str unless @_last_symbol.nil? end
helper method to format an error
# File lib/jeckyl.rb, line 538 def format_error(key, value, message) "[#{key}]: #{value} - #{message}" end
set optparse options for the parameter
@param [Array] opts - options using the same format as optparse
# File lib/jeckyl.rb, line 418 def option(*opts) @_options[@_last_symbol] = opts unless @_last_symbol.nil? end
helper method to format exception messages. A config error should be raised when the given parameter does not match the checks.
The exception is raised in the caller's context to ensure backtraces are accurate.
@param [Object] value - the object that caused the error @param [String] message to include in the exception
# File lib/jeckyl.rb, line 522 def raise_config_error(value, message) raise ConfigError, format_error(@_last_symbol, value, message), caller end
helper method to format exception messages. A syntax error should be raised when the check method has been used incorrectly. See check methods for examples.
The exception is raised in the caller's context to ensure backtraces are accurate.
@param [String] message to include in the exception
# File lib/jeckyl.rb, line 533 def raise_syntax_error(message) raise ConfigSyntaxError, message, caller end
Private Instance Methods
calls each method with the specified prefix with no parameters so that the defaults defined for each will be passed back and used to set the hash before the config file is evaluated.
# File lib/jeckyl.rb, line 468 def get_defaults(opts={}) flag_errors = opts[:flag_errors] local = opts[:local] # go through all of the methods self.class.instance_methods(!local).each do |method_name| if md = /^#{self.prefix}_/.match(method_name) then # its a prefixed method so get it pref_method = self.method(method_name.to_sym) # get the corresponding symbol for the hash @_last_symbol = md.post_match.to_sym @_order << @_last_symbol # and call the method with any parameter, which will # call the default method where defined and capture its value # Note that a default is defined in the same terms as the input # to its parameter method, and may therefore need to be processed # by the method to get the desired value. For example, if a parameter # method expects data expressed in MBytes, but passes on bytes then # the method has to be called with the default to get the real or final # default. This is done in the block after this one: begin a_value = pref_method.call(1) rescue Exception # ignore any errors, which are bound to result from passing in 1 end begin # now set the actual default by calling the method again and passing # the captured default, providing a result which may be different if the method transforms # the parameter! param = @_defaults[@_last_symbol] self[@_last_symbol] = pref_method.call(param) unless param.nil? rescue Exception raise if flag_errors # ignore any errors raised end end end end
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.
# 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