1
\$\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.

asked Sep 22, 2011 at 23:21
\$\endgroup\$

1 Answer 1

3
\$\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 ;-) )
answered Nov 14, 2011 at 22:36
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.