lib/jeckyl.rb in jeckyl-0.2.7 vs lib/jeckyl.rb in jeckyl-0.3.7

- old
+ new

@@ -9,12 +9,14 @@ # must themselves be licensed under the Open Software Licence v. 3.0 # # # == JECKYL # +require 'optparse' require 'jeckyl/version' require 'jeckyl/errors' +require 'jeckyl/helpers' # # The Jeckyl configurator module, which is just a wrapper. See {file:README.md Readme} for details. # module Jeckyl @@ -35,10 +37,11 @@ # # class MyConfig < Jeckyl::Config # # More details are available in the {file:README.md Readme} file class Config < Hash + # 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. @@ -64,16 +67,22 @@ # 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={} + @_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] @@ -88,24 +97,34 @@ raise ConfigFileMissing, "#{config_file}" end # gives access to a hash containing an entry for each parameter and the comments # defined by the class definitions - used internally by class methods - def comments + def _comments @_comments end # This contains an array of the parameter names - used internally by class methods - def order + def _order @_order end # this contains a hash of the defaults for each parameter - used internally by class methods - def defaults + def _defaults @_defaults end + # return hash of options - used internally to generate files etc + def _options + @_options + end + + # return has of descriptions + def _descriptions + @_descriptions + end + # 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. # @@ -163,18 +182,30 @@ # immediate class and excludes any ancestors. # def self.generate_config(local=false) me = self.new(nil, :local => local) # everything should now exist - me.order.each do |key| + 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| + if me._comments.has_key?(key) then + me._comments[key].each do |comment| puts "# #{comment}" end end - def_value = me.defaults[key] + # 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 @@ -189,11 +220,11 @@ # @note this returns a plain hash and not an instance of Jeckyl::Config # def self.intersection(full_config) me = self.new # create the defaults for this class my_hash = {} - me.order.each do |my_key| + 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 @@ -210,11 +241,40 @@ ObjectSpace.each_object {|obj| descs << obj if obj.kind_of?(Class) && obj < self} descs.sort! {|a,b| a < b ? -1 : 1} return descs 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. + # + 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 + # 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'. @@ -236,32 +296,116 @@ 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 # def merge(conf_file) - self[:config_files] << conf_file - - # get the values from the config file itself - self.instance_eval(File.read(conf_file), 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) + def optparse(args) + + # 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) + + # opt_str = '' + # options.each do |os| + # opt_str << os.inspect + # end + + # puts "#{param}: #{opt_str}" + + # 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 + # output the hash as a formatted set + def to_s(opts={}) + keys = self.keys.collect {|k| k.to_s} + cols = 0 + keys.each {|k| cols = k.length if k.length > cols} + keys.sort.each do |key_s| + print ' ' + print key_s.ljust(cols) + key = key_s.to_sym + desc = @_descriptions[key] + value = self[key].inspect + print ": #{value}" + print " (#{desc})" unless desc.nil? + puts + end + end + + protected # create a description for the current parameter, to be used when generating a config template # - # @param [*String] being one or more string arguments that are used to generate config file templates + # @param [*String] strings being one or more string arguments that are used to generate config file templates # and documents def comment(*strings) @_comments[@_last_symbol] = strings unless @_last_symbol.nil? end @@ -271,249 +415,27 @@ def default(val) return if @_last_symbol.nil? || @_defaults.has_key?(@_last_symbol) @_defaults[@_last_symbol] = val end - # the following are all helper methods to parse values and raise exceptions if the values are not correct - - # file helpers - meanings should be apparent - - # check that the parameter is a directory and that the directory is writable + # set optparse options for the parameter # - # Jeckyl checking method to be used in parameter methods to check the validity of - # given parameters, returning the parameter if valid or else raising an exception - # which is either ConfigError if the parameter fails the check or ConfigSyntaxError if - # the parameter is not validly formed - # - # @param [String] - path - # - def a_writable_dir(path) - if FileTest.directory?(path) && FileTest.writable?(path) then - path - else - raise_config_error(path, "directory is not writable or does not exist") - end + # @param [Array] opts - options using the same format as optparse + def option(*opts) + @_options[@_last_symbol] = opts unless @_last_symbol.nil? end - # check parameter is a readable file + # set optparse description for the parameter # - # Jeckyl checking method to be used in parameter methods to check the validity of - # given parameters, returning the parameter if valid or else raising an exception - # which is either ConfigError if the parameter fails the check or ConfigSyntaxError if - # the parameter is not validly formed - # - # @param [String] - path to file - # - def a_readable_file(path) - if FileTest.readable?(path) then - path - else - raise_config_error(path, "file does not exist") - end + # @param [Array] str - options using the same format as optparse + def describe(str) + @_descriptions[@_last_symbol] = str unless @_last_symbol.nil? end - # simple type helpers + # add in all the parameter checking helper methods + include Jeckyl::Helpers - # check the parameter is of the required type - # - # Jeckyl checking method to be used in parameter methods to check the validity of - # given parameters, returning the parameter if valid or else raising an exception - # which is either ConfigError if the parameter fails the check or ConfigSyntaxError if - # the parameter is not validly formed - # - # @param [Object] obj to check type of - # @param [Class] type, being a class constant such as Numeric, String - # - def a_type_of(obj, type) - if obj.kind_of?(type) then - obj - else - raise_config_error(obj, "value is not of required type: #{type}") - end - end - - # check that the parameter is within the required range - # - # Jeckyl checking method to be used in parameter methods to check the validity of - # given parameters, returning the parameter if valid or else raising an exception - # which is either ConfigError if the parameter fails the check or ConfigSyntaxError if - # the parameter is not validly formed - # - # @param [Numeric] val to check - # @param [Numeric] lower bound of range - # @param [Numeric] upper bound of range - # - def in_range(val, lower, upper) - raise_syntax_error("#{lower.to_s}..#{upper.to_s} is not a range") unless (lower .. upper).kind_of?(Range) - if (lower .. upper) === val then - val - else - raise_config_error(val, "value is not within required range: #{lower.to_s}..#{upper.to_s}") - end - end - - - # boolean helpers - - # check parameter is a boolean, true or false but not strings "true" or "false" - # - # Jeckyl checking method to be used in parameter methods to check the validity of - # given parameters, returning the parameter if valid or else raising an exception - # which is either ConfigError if the parameter fails the check or ConfigSyntaxError if - # the parameter is not validly formed - # - # @param [Boolean] val to check - # - def a_boolean(val) - if val.kind_of?(TrueClass) || val.kind_of?(FalseClass) then - val - else - raise_config_error(val, "Value is not a Boolean") - end - end - - # check the parameter is a flag, being "true", "false", "yes", "no", "on", "off", or 1 , 0 - # and return a proper boolean - # - # Jeckyl checking method to be used in parameter methods to check the validity of - # given parameters, returning the parameter if valid or else raising an exception - # which is either ConfigError if the parameter fails the check or ConfigSyntaxError if - # the parameter is not validly formed - # - # @param [String] val to check - # - def a_flag(val) - val = val.downcase if val.kind_of?(String) - case val - when "true", "yes", "on", 1 - true - when "false", "no", "off", 0 - false - else - raise_config_error(val, "Cannot convert to Boolean") - end - end - - - # compound objects - - # check the parameter is an array - # - # Jeckyl checking method to be used in parameter methods to check the validity of - # given parameters, returning the parameter if valid or else raising an exception - # which is either ConfigError if the parameter fails the check or ConfigSyntaxError if - # the parameter is not validly formed - # - # @param [Array] ary to check - # - def an_array(ary) - if ary.kind_of?(Array) then - ary - else - raise_config_error(ary, "value is not an Array") - end - end - - # check the parameter is an array and the array is of the required type - # - # Jeckyl checking method to be used in parameter methods to check the validity of - # given parameters, returning the parameter if valid or else raising an exception - # which is either ConfigError if the parameter fails the check or ConfigSyntaxError if - # the parameter is not validly formed - # - # @param [Array] ary of values to check - # @param [Class] type being the class that the values must belong to - # - def an_array_of(ary, type) - raise_syntax_error("Provided a value that is a type: #{type.to_s}") unless type.class == Class - if ary.kind_of?(Array) then - ary.each do |element| - unless element.kind_of?(type) then - raise_config_error(element, "element of array is not of type: #{type}") - end - end - return ary - else - raise_config_error(ary, "value is not an Array") - end - end - - # check the parameter is a hash - # - # Jeckyl checking method to be used in parameter methods to check the validity of - # given parameters, returning the parameter if valid or else raising an exception - # which is either ConfigError if the parameter fails the check or ConfigSyntaxError if - # the parameter is not validly formed - # - # @param [Hash] hsh to check - # - def a_hash(hsh) - if hsh.kind_of?(Hash) then - true - else - raise_config_error(hsh, "value is not a Hash") - end - end - - # strings and text and stuff - - # check the parameter is a string - # - # Jeckyl checking method to be used in parameter methods to check the validity of - # given parameters, returning the parameter if valid or else raising an exception - # which is either ConfigError if the parameter fails the check or ConfigSyntaxError if - # the parameter is not validly formed - # - # @param [String] str to check - # - def a_string(str) - if str.kind_of?(String) then - str - else - raise_config_error(str.to_s, "is not a String") - end - end - - # check the parameter is a string and matches the required pattern - # - # Jeckyl checking method to be used in parameter methods to check the validity of - # given parameters, returning the parameter if valid or else raising an exception - # which is either ConfigError if the parameter fails the check or ConfigSyntaxError if - # the parameter is not validly formed - # - # @param [String] str to match against the pattern - # @param [Regexp] pattern to match with - # - def a_matching_string(str, pattern) - raise_syntax_error("Attempt to pattern match without a Regexp") unless pattern.kind_of?(Regexp) - if pattern =~ a_string(str) then - str - else - raise_config_error(str, "does not match required pattern: #{pattern.source}") - end - end - - # set membership - set is an array of members, usually symbols - # - # Jeckyl checking method to be used in parameter methods to check the validity of - # given parameters, returning the parameter if valid or else raising an exception - # which is either ConfigError if the parameter fails the check or ConfigSyntaxError if - # the parameter is not validly formed - # - # @param [Symbol] symb being the symbol to check - # @param [Array] set containing the valid symbols that symb should belong to - # - def a_member_of(symb, set) - raise_syntax_error("Sets to test membership must be arrays") unless set.kind_of?(Array) - if set.include?(symb) then - symb - else - raise_config_error(symb, "is not a member of: #{set.join(', ')}") - end - end - - 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 # @@ -554,18 +476,23 @@ # 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 call it + # 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 no parameters, which will - # call the comment method and the default method where defined - # and thereby capture their values + # 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