module Hashmake # Determine if a class includes the HashMakeable module. def self.hash_makeable? klass klass.included_modules.include?(HashMakeable) end # This module should be included for any class that wants to be 'hash-makeable', # which means that a new object instance expects all its arguments to come in a # single Hash. See the hash_make method in this module and the ArgSpec class for # more details. module HashMakeable # Use the included hook to also extend the including class with HashMake # class methods def self.included(base) base.extend(ClassMethods) end # Process a hash that contains 'hashed args'. Each hashed arg is intended to # be used in initializing an object instance. # # @param [Enumerable] arg_specs An enumerable of ArgSpec objects. Each object # details an arg key that might be expected in the # args hash. # @param [Hash] hashed_args A hash that should contain at least all the required # keys and valid values, according to the arg_specs passed in. # Nonrequired keys can be given as well, but if they are # not then a default value is assigned (again, according to # arg_specs passed in). def hash_make arg_specs, hashed_args, assign_args = true arg_specs.each do |key, arg_spec| raise ArgumentError, "arg_specs item #{arg_spec} is not a ArgSpec" unless arg_spec.is_a?(ArgSpec) end raise ArgumentError, "hashed_args is not a Hash" unless hashed_args.is_a?(Hash) arg_specs.each do |key, arg_spec| if hashed_args.has_key?(key) val = hashed_args[key] if Hashmake::hash_makeable?(arg_spec.type) # If the val is not of the right type, but is a Hash, attempt to # make an object of the right type if it is hash-makeable if arg_spec.container == Array && val.is_a?(Array) val.each_index do |i| item = val[i] if !item.is_a?(arg_spec.type) && item.is_a?(Hash) val[i] = arg_spec.type.new item end end elsif arg_spec.container == Hash && val.is_a?(Hash) val.each_key do |item_key| item = val[item_key] if !item.is_a?(arg_spec.type) && item.is_a?(Hash) val[item_key] = arg_spec.type.new item end end else if !val.is_a?(arg_spec.type) && val.is_a?(Hash) val = arg_spec.type.new val end end end else if arg_spec.reqd raise ArgumentError, "hashed_args does not have required key #{key}" else if arg_spec.default.is_a?(Proc) && arg_spec.type != Proc val = arg_spec.default.call else val = arg_spec.default end end end validate_arg arg_spec, val if assign_args self.instance_variable_set("@#{key.to_s}".to_sym, val) end end end # Check the given value, using the given ArgSpec object. An ArgumentError # exception will be raised if the value is not valid. def validate_arg arg_spec, val if arg_spec.container == Array raise ArgumentError, "val #{val} is not an array" unless val.is_a?(Array) val.each do |item| raise ArgumentError, "array item #{item} is not a #{arg_spec.type}" unless item.is_a?(arg_spec.type) raise ArgumentError, "array item #{item} is not valid" unless arg_spec.validator.call(item) end elsif arg_spec.container == Hash raise ArgumentError, "val #{val} is not a hash" unless val.is_a?(Hash) val.values.each do |item| raise ArgumentError, "hash item #{item} is not a #{arg_spec.type}" unless item.is_a?(arg_spec.type) raise ArgumentError, "hash item #{item} is not valid" unless arg_spec.validator.call(item) end elsif arg_spec.container.nil? raise ArgumentError, "val #{val} is not a #{arg_spec.type}" unless val.is_a?(arg_spec.type) raise ArgumentError, "val #{val} is not valid" unless arg_spec.validator.call(val) else raise ArgumentError, "arg_spec.container #{arg_spec.container} is not valid" end return true end # Look in the current class for a constant that is a Hash containing (only) # ArgSpec objects. Returns the first constant matching this criteria, or nil # if none was found. def find_arg_specs self.class.constants.each do |constant| val = self.class.const_get(constant) if val.is_a? Hash all_arg_specs = true val.each do |key,value| unless value.is_a? ArgSpec all_arg_specs = false break end end if all_arg_specs return val end end end return nil end # Produce a hash that contains 'hashed args'. Each hashed arg is intended to # be used in initializing an object instance. # # @param [Enumerable] arg_specs An enumerable of ArgSpec objects. Each one # details an arg key that might be expected in the # args hash. This param is nil by default. If the param # is nil, this method will attempt to locate arg specs # using find_arg_specs. # def make_hash arg_specs = nil if arg_specs.nil? arg_specs = self.find_arg_specs raise "No arg specs given, and no class constant that is a Hash containing only ArgSpec objects was found" if arg_specs.nil? end arg_specs.each do |key, arg_spec| raise ArgumentError, "arg_specs item #{arg_spec} is not a ArgSpec" unless arg_spec.is_a?(ArgSpec) end hash = {} arg_specs.each do |key, arg_spec| sym = "@#{key}".to_sym raise ArgumentError, "current obj #{self} does not include instance variable #{sym}" if !self.instance_variables.include?(sym) val = self.instance_variable_get(sym) should_assign = false if arg_spec.reqd should_assign = true else if arg_spec.default.is_a?(Proc) should_assign = (val != arg_spec.default.call) else should_assign = (val != arg_spec.default) end end if should_assign if val.is_a?(Array) && arg_spec.container == Array ary = val val = [] ary.each do |item| if Hashmake::hash_makeable?(item.class) and item.class == arg_spec.type val << item.make_hash else val << item end end elsif val.is_a?(Hash) && arg_spec.container == Hash hsh = val val = {} hsh.each do |hsh_key,item| if Hashmake::hash_makeable? item.class and item.class == arg_spec.type val[hsh_key] = item.make_hash else val[hsh_key] = item end end elsif Hashmake::hash_makeable?(val.class) and val.class == arg_spec.type val = val.make_hash end hash[key] = val end end return hash end # Contains class methods to be added to a class that includes the # HashMakeable module. module ClassMethods # Helper method to make a generic new ArgSpec object def arg_spec args ArgSpec.new args end # Helper method to make a ArgSpec object where the container is an Array. # Set :default to a Proc that generates an empty array. def arg_spec_array args args = { :container => Array, :default => ->(){Array.new} }.merge(args) ArgSpec.new args end # Helper method to make a ArgSpec object where the container is an Hash. # Set :default to a Proc that generates an empty hash. def arg_spec_hash args args = { :container => Hash, :default => ->(){Hash.new} }.merge(args) ArgSpec.new args end end end end