0
\$\begingroup\$

I am actually pretty excited about this approach, but for sanity's sake I wanted to hear some thoughts on others on my strategy here. My basic goal is to parse a YAML file and recursively create module constants using eval. The core business logic is here:

#
#   configure application based on an easily-managed .yml file -- use 'eval' 
#   to generate constants based on the structure of the hash we get from the yaml.
#   most of the business logic for recursively parsing a hash and creating module 
#   constants basically just involves tracking module names properly.
#
def parse_and_recursively_create_constants!(hash, module_context = [])
  hash.each do |key, value|
    if value.is_a? Hash
      module_name = convert_string_to_camel_case(key)
      context = module_context.dup << module_name
      parse_and_recursively_create_constants! hash[key], context
    else
      value_is_a_module = false

      if value.is_a? String
        if value.include? "::"
          value_is_a_module = true
        else
          value = "\"#{value}\""          
        end
      end

      if value_is_a_module
        # explicitly build out this module (since it doesn't exist) so that we can talk about it
        expression = value.split('::').map { |module_name| "module #{module_name}; "}.join(' ')
        value.split('::').count.times { expression << "end; "}
        eval(expression, $__global_scope)
      end

      # define the given constant
      depth = module_context.count
      expr = module_context.map { |module_name| "module #{module_name}; " }.join(" ")
      expr << "#{key.upcase} = #{value}; " 
      depth.times { expr << "end; "}
      expr = "module #{@project_name}; #{expr}; end"
      eval(expr, $__global_scope)
    end
  end

Note I am having to bind the default scope to a global $__global_scope which occurs just before the class definition containing this method. I was wondering if there were some cleaner way of handling that, but either way the behavior certainly is what I want it to be. Here's an example configuration file:

key: value
numeric_data: 3.221
sample_module:
    some_key: another value
key_should_be_module: I::Am::A::Module

And the associated test case:

configure! "Sample", "spec/resources/sample.yml"

Sample::KEY.should == 'value'
Sample::NUMERIC_DATA.should == 3.221
Sample::SampleModule::SOME_KEY.should == 'another value'
Sample::KEY_SHOULD_BE_MODULE.should be_a_kind_of(Module)
Sample::KEY_SHOULD_BE_MODULE.should == I::Am::A::Module

I have released this as a gem, and if you'd like more context the full source is on Github here.

\$\endgroup\$
2
\$\begingroup\$

You use eval, but eval is evil.

I tried a solution without eval. For this I used:

  • Module.const_set to define constants
  • Module.const_defined to check if a constant is defined
  • Module.const_get to navigate inside modules
  • Module.new to define new modules.

My solution (including a little test):

def def_constants!(hash, context)

  #context should be a module. If not, then get (or build) it.
  if context.is_a? String 
      lcontext = Object
      context.split('::').each{ |module_name| 
        lcontext.const_set(module_name, Module.new) unless lcontext.const_defined?(module_name)
        lcontext = lcontext.const_get(module_name)
      }
    context = lcontext
  end

  raise ArgumentError unless context.is_a?(Module)

  hash.each do |key, value|
    case value
      when Hash
        module_name = key.upcase #fixme --  convert_string_to_camel_case(key)
        def_constants!( hash[key], [context.name, module_name].join('::'))
      when /::/  # explicitly build out this module (since it doesn't exist) so that we can talk about it
        context.const_set(key.upcase, Module.new)
        lmod = context.const_get(key.upcase)
        value.split('::').each{ |module_name| 
          lmod = lmod.const_set(module_name, Module.new )
        }
      else
        # define the given constant
        context.const_set(key.upcase, value)
    end
  end #hash
end

module AA #define a start
end

require 'yaml'
def_constants!(YAML.load(DATA), AA)
p AA.constants
p AA::KEY
p AA::SAMPLE_MODULE::SOME_KEY
p AA::KEY_SHOULD_BE_MODULE
p AA::KEY_SHOULD_BE_MODULE::I
p AA::KEY_SHOULD_BE_MODULE::I.constants
p AA::KEY_SHOULD_BE_MODULE::I::Am
p AA::KEY_SHOULD_BE_MODULE::I::Am::A
p AA::KEY_SHOULD_BE_MODULE::I::Am::A::Module


__END__
key: value
numeric_data: 3.221
sample_module:
    some_key: another value
key_should_be_module: I::Am::A::Module

I haven't tested it for each usecase. I expect problems, when you try to define modules, where you have already constants with the same name.

Some changes, to use it in your code:

  • I skipped convert_string_to_camel_case(key) and replaced it with upcase
  • Your @project_name is my initial context.
  • (and sure: I renamed the method ;-) )
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.