0

I'm using Rails 4 and trying to access a hash variable via string names.

For example, let's say I have policy model with a member hash who has the fields name and address.

I would like to be able to convert policy_member_name into policy[:member][:name].

However, this string may be longer than just 3 sections. I was able to access the value, but not be able to set it using the following:

  ret = obj
  keys.each do |key|
    ret = ret[key.to_sym]
  end
  ret

where keys would be an array such as ['member', 'name'] and obj would be the object, such as Policy.first. However, this method only would return what value is at policy[:member][:name] and does not allow me to do policy[:member][:name]=.

Any help is appreciated, thanks.

EDIT:

I'd like to be able to call policy.member_name to get policy[:member][:name] and have policy.member_name= "Something" to set policy[:member][:name]="Something"

3
  • can you give us the known input and the desired output? Commented May 24, 2015 at 22:17
  • 1
    Side note: seems to me you are sacrificing performance for something that might not be as important as you might think. Even the OpenStruct Ruby class - which is very close to what you want - suffers a performance cost because it relies on method_missing ... also, it might be more confusing than helpful - for instance, what happens when: policy.member = Users.find(1); policy.member_name = 'Joe'; puts policy.member ...? Commented May 25, 2015 at 5:11
  • The reason I need this is I'm using the gem best_in_place in order to edit fields in place. Are you suggesting not using OpenStructs at all or simply that the dot notation isn't necessary? Or are you aware of another way to edit OpenStructs in place? Thanks! Commented May 26, 2015 at 0:02

2 Answers 2

2

This is pretty easy to achieve with method_missing:

class HashSnake

  def initialize(hash)
    @hash = hash
  end

  def method_missing(method, *args, &block)
    parts = method.to_s.split('_').map(&:to_sym)
    pointer = @hash
    parts.each_with_index do |key, i|             
      if pointer.key? key
        if pointer[key].is_a? Hash
            if (i +1 == parts.length)
                return pointer[key]
            else
                pointer = pointer[key]
            end
        else
          return pointer[key]
        end
      # Checks if method is a setter
      elsif key[-1] == '=' && pointer.key?(key[0..-2].to_sym)
        pointer[key[0..-2].to_sym] = args[0]
      end
    end
  end
end

obj = HashSnake.new(
  member: {
    foo: 'bar',
    child: {
      boo: 'goo'
    }
  }
)

obj.member_foo = 'bax'
puts obj.member_foo.inspect
# "bax"
puts obj.member_child_boo.inspect
# goo
Sign up to request clarification or add additional context in comments.

1 Comment

@Myst's comment sums it up pretty well. Do you really need this? There is pretty big cost in terms of performance and I don't even want to think about what the stack trace looks like.
0

As you will only give users read or write access to certain hashes, there would be no difficulty creating a hash that maps the names of those hashes (strings) to the hash objects. For example:

h0 = { a: 1, b: 2 }
h1 = { c: 3, d: { e: 4, f: { g: 5 } } }

hash = { "h0"=>h0, "h1"=>h1 }

Once that is done, it's just a matter of creating a domain specific language ("DSL") that meets your needs. There may well be gems that would do that out of the box, or could be modified to suit your requirements.

Here is an example of one possibility. (I suggest you look at the examples before reading through the code.)

Code

class Array
  def h(hash)
    h = hash[first]
    if last.is_a? Array
      seth(h, *self[1..-2].map(&:to_sym), *last)
    else
      geth(h, *self[1..-1].map(&:to_sym))
    end
  end

  private

  def geth(h, *keys)
    keys.reduce(h) { |h,k| h[k] }
  end

  def seth(h, *keys, last_key, type, val)
    val =
      case type
      when "STR" then val.to_str
      when "INT" then val.to_int
      when "SYM" then val.to_sym
      when "FLT" then val.to_f
      end
    geth(h, *keys)[last_key] = val
  end
end

Examples

The following is for h0, h1 and hash presented at the outset.

["h0", "b"].h(hash)
   #=> 2
["h1", "d", "f", "g"].h(hash)
   #=> 5

["h0", "b", ["INT", 3]].h(hash)
   #=> 3
h0 #=> {:a=>1, :b=>3}
["h1", "d", "f", "g", ["STR", "dog"]].h(hash)
   #=> "dog"
h1 #=> {:c=>3, :d=>{:e=>4, :f=>{:g=>"dog"}}, :g=>"dog"} 

Comments

Your Answer

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.