Insert a record into a Ruby hash file at a specific location
Given a hash with, say, a nested hash in it:
hash = {"some_key" => "value", "nested" => {"key1" => "val1", "key2" => "val2"}} and the key path in String format:
path = "nested.key2" How to add a new key-value pair before writing key2? Thus, the expected result should be something like this:
hash = {"some_key" => "value", "nested" => {"key1" => "val1", "new_key" => "new_value"}, "key2" => "val2"}} EDITED
My goal is to add some shortcut in front of some key in order to discard the hash in the form of Yaml text and then process the text to replace the added key / value with Yaml comment. AFAIK, there is no other way to add a comment to a specific key in YAML programmatically.
This is easiest with a Hash array view:
subhash = hash['nested'].to_a insert_at = subhash.index(subhash.assoc('key2')) hash['nested'] = Hash[subhash.insert(insert_at, ['new_key', 'new_value'])] It can be wrapped in a function:
class Hash def insert_before(key, kvpair) arr = to_a pos = arr.index(arr.assoc(key)) if pos arr.insert(pos, kvpair) else arr << kvpair end replace Hash[arr] end end hash['nested'].insert_before('key2', ['new_key', 'new_value']) p hash # {"some_key"=>"value", "nested"=>{"key1"=>"val1", "new_key"=>"new_value", "key2"=>"val2"}} I often create YAML generators for large configurations for applications. For maintenance, I need to sort the fields.
The solution I use when generating YAML in sorted order is to add the keys as needed to get them in the correct hash or subache. Then I create a new hash, sorting the key / value pairs and doing to_yaml on it.
It makes no sense to sort the hash, but sort the temporary hash for output before letting YAML work, and leads to a more easily maintained file.
require 'yaml' some_hash = { 'z' => 1, 'a' => 3 } puts some_hash.to_yaml What outputs:
--- z: 1 a: 3 Sort before creating YAML output:
puts Hash[some_hash.merge('x' => 2).sort_by{ |k, v| k }].to_yaml Outputs:
--- a: 3 x: 2 z: 1 Instead of puts use File.write or insert a line into the block passed to File.open .
Regarding comments in YAML files: YAML does not support adding comments programmatically to the output. Comments are for people, and # cannot be replaced with a Ruby variable or an object. Think of it this way: if we start with this YAML in a file called test.yaml :
--- # string a: 'fish' # array b: - 1 - 2 # hash c: d: 'foo' e: 'bar' # integer z: 1 And download it:
require 'pp' require 'yaml' obj = YAML.load_file('test.yaml') pp obj I get obj like:
{"a"=>"fish", "b"=>[1, 2], "c"=>{"d"=>"foo", "e"=>"bar"}, "z"=>1} There are no "comment" objects, and in Ruby there is not one that fits into the hash that exists in the YAML specification. We could arbitrarily select a class, which we call a comment, and try to inject it into the object as a key, but YAML does not accept it as a comment, because the specification does not allow it. It will define it as a Ruby class and recreate it as this class, but it will not display as a comment # :
require 'yaml' class Comment def initialize(some_text) @comment = "# #{some_text}" end end some_hash = { 'a' => 1, Comment.new('foo') => 'bar', 'z' => 'z' } puts some_hash.to_yaml Withdrawal:
--- a: 1 ? !ruby/object:Comment comment: ! '# foo' : bar z: z When I need comments in my emitted YAML configurations, I manually configure them to add them later. For what you want to do, and not for manual configuration, I would recommend using more mnemonic or unique variable names that you can scan in your document. You can even put dummy entries that do not provide anything useful other than acting as the owner of the place:
require 'yaml' some_hash = { 'a' => 1, '__we_are_here__' => '', 'b' => 2, '__we_are_now_here__' => '', 'z' => 'z' } puts some_hash.to_yaml The result in a YAML file, for example:
--- a: 1 __we_are_here__: '' b: 2 __we_are_now_here__: '' z: z As for inserting the key into the hash, I would most likely change my βkeychainβ a bit to show the path where I want to insert it and the name of the new key. Again, I would count on sorting to make sure everything was in the correct order before saving YAML:
require 'pp' # this changes the incoming hash def insert_embedded_hash_element(hash, key_path, new_value) keys = key_path.split('.') new_key = keys.pop sub_hash = hash keys.each do |k| sub_hash = sub_hash[k] end sub_hash[new_key] = new_value end # the sub-hash to insert into + new key name insert_key = 'nested.key2' insert_value = 'new_value' hash = { "some_key" => "value", "nested" => { "key1" => "val1", "key3" => "val2" } } insert_embedded_hash_element(hash, insert_key, insert_value) pp hash Result:
{"some_key"=>"value", "nested"=>{"key1"=>"val1", "key3"=>"val2", "key2"=>"new_value"}} This meets the needs of the OP, but can be changed at any time according to the needs of:
require 'yaml' hash = {"some_key" => "value", "nested" => {"key1" => "val1", "key2" => "val2"}} new_hash = %w(nested key2).inject(hash) do |h,i| next h[i] unless h.has_key? "key2" ind = h.to_a.index{|m| m[0] == i } Hash[h.to_a.insert(ind,["new_key","new_value"])] end hash["nested"] = new_hash # this part is to be taken care of for deep hash. puts hash.to_yaml Output:
some_key: value nested: key1: val1 new_key: new_value key2: val2 UPDATE:
I found a more efficient code that will reduce the overhead of hash["nested"] = new_hash in my previous code:
require 'yaml' hash = {"some_key" => "value", "nested" => {"key1" => "val1", "key2" => "val2"}} new_hash = %w(nested key2).inject(hash) do |h,i| # !> assigned but unused variable - new_hash next h[i] unless h.has_key? "key2" ind = h.to_a.index{|m| m[0] == i } h1 = Hash[h.to_a.insert(ind,["new_key","new_value"])] h.replace(h1) end hash # => {"some_key"=>"value", # "nested"=>{"key1"=>"val1", "new_key"=>"new_value", "key2"=>"val2"}} puts hash.to_yaml # >> --- # >> some_key: value # >> nested: # >> key1: val1 # >> new_key: new_value # >> key2: val2 I don't think Ruby provides this feature for free. You could do something like this, where you create an array of existing hash keys, insert your new key into the array, and then create a new hash with the new ordered keys.
keys = original_hash.keys keys.insert(new_key_position, new_key) new_hash = {} keys.each do |key| new_hash[key] = key == new_key ? new_value : original_hash[key] end