0

I have an Array of strings which represent a path of nested hash keys e.g.

["foo", "bar", "baz"]

I want to create a function which takes in the Array and a value and sets the value for the key provided in the Array, so when I call it with the example, it sets the value for

Hash["foo"]["bar"]["baz"].

Is there a method that can do that. Is there a way to chain the elements of an Array into calls for hash keys with #inject ?

Any help is highly appreciated.

To specify the question:

I have the following code:

def anonymize_data_hash(data, path=[])
  if data.is_a?(Hash)
    data.each do |key, value|
      anonymize_data_hash(value, path + [key])
    end
  elsif data.is_a?(Array)
    data.each do |value|
      anonymize_data_hash(value, path)
    end
  else
    path = path.clone
    key = path.shift
    path = (path + [data]).map(&:to_s)

    send("#{key}")[path] = "xxxxxx"
    save
  end
end

the anonymize_data_hash method sends an method(attribute) call to a mode which is a serialized hash. Path is an array of strings. In order make the above function work I need to turn the array of string into a nested Hash call.

The Hash already exists I need to access it with the values given in the Array. Thank You for Your help.

2 Answers 2

9

You could make use of the newer method Hash#dig (ruby 2.3+) to access the nested hash, and then set the value on that:

# ideally this might be a method on Hash, so you wouldn't need to pass it in
def deep_set(hash, path, value)
  *path, final_key = path
  to_set = path.empty? ? hash : hash.dig(*path)

  return unless to_set
  to_set[final_key] = value
end

hash = {
  "foo" => {
    "bar" => { }
  }
}

deep_set(hash, ["foo", "bar", "baz"], "xxxxxx")
deep_set(hash, ["baz"], "yyyyyy")
deep_set(hash, ["foo", "baz", "bar"], "zzzzzz")
puts hash
# => {"foo"=>{"bar"=>{"baz"=>"xxxxxx"}}, "baz"=>"yyyyyy"}
Sign up to request clarification or add additional context in comments.

8 Comments

Looks very nice. I will try it and accept it when it works in my rails app.
You need to deconstruct the array, hash.dig(*path), notice the asterisk * that turns the array into an argument list that works for dig
So in my example I would retrieve the hash with send("#{key}") and save it in a locale variable an then call your deep_set method on it.
yeah if send("#{key}") returns the hash object (which btw, could just be send(key), unless key might not be a string or symbol), then you would call it with deep_set(send(key), path, "xxxxxx"), or you could, of course, save it into a local variable and pass that into the method, but it's not needed
Yeah thats true. Another thing I am adding is that I will only set the key if it is present. The serialized hash I am anonymizing is submitted form_data. So I don't want create and anonymize keys which aren't present.
|
2

Simple Lime's solution requires a well-formatted hash to begin with. If addition to the hash depends on that, you highly depend on the input data and its order to determine if a value is added or not (e.g. "zzzzzz" in the example). You might want to (re-)define the hash's default_proc to create the structure of the hash on the fly. Here is an example:

def nest(array, value)
  hash = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
  hash.dig(*array[0..-2])[array.fetch(-1)] = value
  hash
end

nest ["foo", "bar", "baz"], :test
# => {"foo"=>{"bar"=>{"baz"=>:test}}}

Since the hash already exists, I suggest proper initialisation of its default_proc.

hash = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }

If that's not possible, you can still overwrite it later as well

hash = {}  # unchange initialisation 

hash.default_proc = -> (h, k) { h[k] = Hash.new(&h.default_proc) }

def nest(hash, array, value)
  hash.dig(*array[0..-2])[array.fetch(-1)] = value
  hash
end

nest hash, ["foo", "bar", "baz"], :test
# => {"foo"=>{"bar"=>{"baz"=>:test}}}

I hope you find this helpful too.

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.