I've written a method to turn a hash (nested if necessary) of values into a chain that can be used with eval to dynamically return values from an object.
E.g. passed a hash like { :user => { :club => :title }}, it will return "user.club.title", which I can then eval. (The point of this is to write a method for views that will allow me to dump the contents of objects rapidly, by passing in the object and a list of attributes that I want to display, e.g.: item_row(@user, :name, :email, { :club => :title })
Here's what I've got. It works, but I know it can be improved. Curious to see how you'd improve it.
# hash = { :user => { :club => :title }}
# we want to end up with user.club.title
def hash_to_eval_chain(hash)
raise "Hash cannot contain multiple key-value pairs unless they are nested" if hash.size > 1
hash.each_pair do |key, value|
chain = key.to_s + "."
if value.is_a? String or value.is_a? Symbol
chain += value.to_s
elsif value.is_a? Hash
chain += hash_to_eval_chain(value)
else
raise "Only strings, symbols, and hashes are allowed as values in the hash."
end
# returning from inside the each_pair block only makes sense because we only ever accept hashes
# with a single key-value pair
return chain
end
end
puts hash_to_eval_chain({ :club => :title }) # => club.title
puts hash_to_eval_chain({ :user => { :club => :title }}) # => user.club.title
puts hash_to_eval_chain({ :user => { :club => { :owners => :name }}}) # => user.club.owners.name
puts ({ :user => { :club => { :owners => :name }}}).to_s # => userclubownersname (close, but lacks the periods)
<codegolfing mode=on>
def hash_to_arr(h)
arr = []
while h.kind_of?(Hash)
# FIXME : check h.size ?
k = h.keys[0]
arr.push(k)
h = h[k]
end
arr.push h
end
puts hash_to_arr(:club).join('.') #=> "club"
puts hash_to_arr(:club => :title).join('.') #=> "club.title"
puts hash_to_arr(:user => {:club => :title}).join('.') #=> "user.club.title"
puts hash_to_arr(:user => {:club => {:owners => :name}}).join('.') #=> "user.club.owners.name"
The biggest change is to avoid iteration since we are interested in 1 element hashes. Btw, an array like [:club, :title, :owners] would maybe more straightforward for your usage.
Cheers,
zimbatm
Inspired by some of the other answers, here's a way to do it using recursive send() rather than eval():
def extract(obj, hash)
k, v = hash.to_a[0]
v.is_a?(Hash) ? extract(obj.send(k), v) : obj.send(k).send(v)
end
In the case mentioned in the question, extract(@user, {:club => :title}) would result in @user.send(:club).send(:title).
EDIT: as mentioned by zimbatm, an array like [:club, :title, :owner] might be cleaner. If you used that instead (and are running in an environment that supports Symbol#to_proc), you could just do:
def extract2(obj, method_array)
method_array.inject(obj, &:send)
end
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With