4

it should be easy, but I couldn't find a proper solution. for the first level keys:

resource.public_send("#{key}=", value)

but for foo.bar.lolo ?

I know that I can get it like the following:

'foo.bar.lolo'.split('.').inject(resource, :send)

or

resource.instance_eval("foo.bar.lolo")

but how to set the value to the last variable assuming that I don't know the nesting level, it may be second or third.

is there a general way to do that for all levels ? for my example I can do it like the following:

resource.public_send("fofo").public_send("bar").public_send("lolo=", value)
user181452
  • 545
  • 10
  • 25
  • possible duplicates: https://stackoverflow.com/questions/34620469/safely-assign-value-to-nested-hash-using-hashdig-or-lonely-operator https://stackoverflow.com/questions/41639364/how-to-set-dynamically-value-of-nested-key-in-ruby-hash – Rene Wooller Jan 06 '22 at 01:17

5 Answers5

5

Answer for hashes, just out of curiosity:

hash = { a: { b: { c: 1 } } }
def deep_set(hash, value, *keys)
  keys[0...-1].inject(hash) do |acc, h|
    acc.public_send(:[], h)
  end.public_send(:[]=, keys.last, value)
end

deep_set(hash, 42, :a, :b, :c)
#⇒ 42
hash
#⇒ { a: { b: { c: 42 } } }
Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
  • 1
    isn't this basically the same as hash#bury? i.e. [this](https://github.com/dam13n/ruby-bury) – max pleaner Jan 13 '17 at 17:49
  • @maxple I have no idea what `bury` is. This is pure ruby. – Aleksei Matiushkin Jan 13 '17 at 17:52
  • it's like the ancillary to dig. Like the gem I linked. Just saying people commonly call this functionality bury. – max pleaner Jan 13 '17 at 17:54
  • @maxple I think you should add that link, and an example using it, as an answer. It's off topic to ask for off-site resources, but it's not off-topic to give them in an answer. – Wayne Conrad Jan 13 '17 at 23:46
  • @mudasobwa many thanks, I modified to meet my hash, and it works `keys[0...-1].inject(resource) { |acc, key| acc.public_send(key) }.public_send("#{keys.last}=", value)` – user181452 Jan 17 '17 at 12:10
  • @maxpleaner it is not the same as your implementation of bury because it can't handle setting values on hash keys that don't exist – Rene Wooller Jan 06 '22 at 00:54
  • NOTE: if you want this to work on a hash that does not have the keys predefined, you need to modify the default_proc of the hash eg add this to the first line of the function above: ```hash.default_proc = proc { |h,k| h[k] = Hash.new(&h.default_proc) }``` – Rene Wooller Jan 06 '22 at 01:14
2

Hashes in ruby don't by default give you these dot methods.

You can chain send calls (this works on any object, but you can't access hash keys in this way normally):

  "foo".send(:downcase).send(:upcase)

When working with nested hashes the tricky concept of mutability is relevant. For example:

  hash = { a: { b: { c: 1 } } }
  nested = hash[:a][:b]
  nested[:b] = 2
  hash
  # => { a: { b: { c: 2 } }

"Mutability" here means that when you store the nested hash into a separate variable, it's still actually a pointer to the original hash. Mutability is useful for a situation like this but it can also create bugs if you don't understand it.

You can assign :a or :bto variables to make it 'dynamic' in a sense.

There are more advanced ways to do this, such as dig in newer Ruby

versions.

  hash = { a: { b: { c: 1 } } }
  keys_to_get_nested_hash = [:a, :b]
  nested_hash = hash.dig *keys_to_get_nested_hash
  nested_hash[:c] = 2
  hash
  # => { a: { b: { c: 2 } } }

If you use OpenStruct then you can give your hashes dot-method accessors. To be honest chaining send calls is not something I've used often. If it helps you write code, that's great. But you shouldn't be sending user-generated input, because it's insecure.

max pleaner
  • 26,189
  • 9
  • 66
  • 118
  • 2
    `hash.public_send(:[], :a).public_send(:[]=, :b, 42)` perfectly works, there is nothing tricky. – Aleksei Matiushkin Jan 13 '17 at 17:33
  • @AlekseiMatiushkin on version 3.0.3 here I'm finding it only works if you have predefined the keys in the hash eg: ```{}.public_send(:[], :a).public_send(:[]=, :b, 42)``` gets ```undefined method `[]=' for nil:NilClass``` – Rene Wooller Jan 06 '22 at 01:01
0

Although you could implement some methods to do things the way you have them set up now, I'd strongly recommend that you reconsider your data structures.

To clarify some of your terminology, the key in your example is not a key, but a method call. In Ruby, when you have code like my_thing.my_other_thing, my_other_thing is ALWAYS a method, and NEVER a key, at least not in the proper sense of the term.

It's true that you can create a hash-like structure by chaining objects in this way, but there's a real code smell to this. If you conceive of foo.bar.lolo as being a way to lookup the nested lolo key in a hash, then you should probably be using a regular hash.

x = {foo: {bar: 'lolo'}}
x[:foo][:bar] # => 'lolo'
x[:foo][:bar] = 'new_value' # => 'new_value'

Also, although the send/instance_eval methods can be used this way, it's not the best practice and can even create security problems.

Glyoko
  • 2,071
  • 1
  • 14
  • 28
0

if you want to allow for initializing missing nested keys, I recommend the following refactor to Aleksei Matiushkin' solution

I didn't want to make a change to the actual answer as it is perfectly valid as is and this is introducing something extra.

hash = { a: {} } # missing some nested keys
def deep_set(hash, value, *keys)
  keys[0...-1].inject(hash) do |acc, h|
    acc[h] ||= {} # initialize the missing keys (ex: b in this case) 
    acc.public_send(:[], h)
  end.public_send(:[]=, keys.last, value)
end

deep_set(hash, 42, :a, :b, :c)
#⇒ 42
hash
#⇒ { a: { b: { c: 42 } } }
SMAG
  • 652
  • 6
  • 12
0

Assuming that the keys are known to exist, then Hash#dig gives a cleaner solution:

hash = { a: { b: { c: 1 } } }

def deep_set(hash, value, *keys)
  hash.dig(*keys[0..-2])[keys[-1]] = value
end

deep_set(hash, 42, :a, :b, :c)
#⇒ 42

hash
#⇒ { a: { b: { c: 42 } } }

This is just example code. It will not work if the keys are not known or if deep_set receives less than two keys. Both of those issues are solvable, but beyond the OP's question.