What if any design issues are there in this method of loading configuration data from YAML in Ruby? 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 &lt;&lt; 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 &lt;&lt; "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 &lt;&lt; "#{key.upcase} = #{value}; " 
      depth.times { expr &lt;&lt; "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.
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 ;-) )