Convert nested HashWithIndifferentAccess to nested Hash - ruby-on-rails

Is there a nice way to transform an instance of a HashWithIndifferentAccess (with nested instances of class HashWithIndifferentAccess) to an instance of a Hash (with nested instances of class Hash)?
It seems easy to convert a nested Hash to a nested HashWithIndifferentAccess. Just use the with_indifferent_access method that ActiveSupport provides. This converts all hashes, no matter how deeply nested.
hash = { late: { package: 2, trial: 100, penalty: { amount: 1 } },
no_show: { package: 1, trial: 100, penalty: { amount: 2 } } }
hash_wid = hash.with_indifferent_access
hash_wid.class
# ActiveSupport::HashWithIndifferentAccess #great
hash_wid [:no_show][:penalty].class
# ActiveSupport::HashWithIndifferentAccess #great
The reverse seems not so easy:
hash = hash_wid.to_h
hash.class
# Hash # OK
hash[:no_show][:penalty].class
# ActiveSupport::HashWithIndifferentAccess # want this to be Hash
Hash#to_h method only converts the top level hash, not the nested hashes.
I tried the (Rails/ActiveSupport) deep_transform_values! method that extends the Hash class:
hash_wid.deep_transform_values! do |value|
value.class == HashWithIndifferentAccess ? value.to_h : value
end
hash_wid.class
# ActiveSupport::HashWithIndifferentAccess # want this to be Hash
hash_wid[:no_show][:penalty].class
# ActiveSupport::HashWithIndifferentAccess # want this to be Hash
But looking to the source code of the deep_transform_values! method (and transform_values! method that it depends upon), these can transform hashes of class Hash, but not hashes of class HashWithIndifferentAccess.
So is there a nice way to transform a nested HashWithIndifferentAccess to a nested Hash?
Thanks
Daniel

Your example indicates that you are using keys and values whose types are compatible with JSON. If your hash can be converted to JSON then a simple way to do it is:
hash = JSON.load(JSON.dump({ foo: { bar: 'baz' }.with_indifferent_access }.with_indifferent_access))
=> {"foo"=>{"bar"=>"baz"}}
hash.class
=> Hash
hash['foo'].class
=> Hash
deep_transform_values won't work in your case because of how that method is written:
def _deep_transform_values_in_object(object, &block)
case object
when Hash
object.transform_values { |value| _deep_transform_values_in_object(value, &block) }
when Array
object.map { |e| _deep_transform_values_in_object(e, &block) }
else
yield(object)
end
end
If your object is a Hash (or Array) then the method calls itself recursively until it finds a non-Hash and non-Array value onto which it can apply the transformation. It should be pretty trivial to write your own implementation using that example though:
def _deep_transform_values_in_object(object, &block)
case object
when Hash
# something less ugly than but similar to this
object = object.to_h if object.is_a?(ActiveSupport::HashWithIndifferentAccess)
object.transform_values { |value| _deep_transform_values_in_object(value, &block) }
when Array
object.map { |e| _deep_transform_values_in_object(e, &block) }
else
yield(object)
end
end

Related

How to trasform all values in a nested hash?

I want to convert all the values in a nested hash to a utf8 compatible string. I initially thought this would be easy and something like deep_apply should be available for me to use, but I am unable to find anything this simple on a quick google and SO search.
I do not want to write (maintain) a method similar to the lines of Change values in a nested hash . Is there a native API implementation or a shorthand available for this or do I have to write my own method?
I ended up implementing my own approach, that is in no way perfect but works well for my use case and should be easy to maintain. Posting it here for reference to anyone who wants to try it out
def deep_apply object, klasses, &blk
if object.is_a? Array
object.map { |obj_ele| deep_apply(obj_ele, klasses, &blk) }
elsif object.is_a? Hash
object.update(object) {|_, value| deep_apply(value, klasses, &blk) }
elsif klasses.any? { |klass| object.is_a? klass }
blk.call(object)
else
object
end
end
usage:
=> pry(main)> deep_apply({a: [1, 2, "sadsad"]}, [String, Integer]) { |v| v.to_s + "asd" }
=> {:a=>["1asd", "2asd", "sadsadasd"]}
Interesting to learn of the deep_merge approach taken in the answer by "The F". Here is another approach which requires adding a few helper methods.
First, the helper methods:
From the top answer here (converting-a-nested-hash-into-a-flat-hash):
def flat_hash(h,f=[],g={})
return g.update({ f=>h }) unless h.is_a? Hash
h.each { |k,r| flat_hash(r,f+[k],g) }
g
end
From a Github repo called ruby-bury (this functionality was proposed to Ruby core, but rejected)
class Hash
def bury *args
if args.count < 2
raise ArgumentError.new("2 or more arguments required")
elsif args.count == 2
self[args[0]] = args[1]
else
arg = args.shift
self[arg] = {} unless self[arg]
self[arg].bury(*args) unless args.empty?
end
self
end
end
And then a method tying it together:
def change_all_values(hash, &blk)
# the next line makes the method "pure functional"
# but can be removed otherwise.
hash = Marshal.load(Marshal.dump(hash))
flat_hash(hash).each { |k,v| hash.bury(*(k + [blk.call(v)])) }
hash
end
A usage example:
irb(main):063:0> a = {a: 1, b: { c: 1 } }
=> {:a=>1, :b=>{:c=>1}}
irb(main):064:0> b = change_all_values(a) { |val| val + 1 }
=> {:a=>2, :b=>{:c=>2}}
irb(main):066:0> a
=> {:a=>1, :b=>{:c=>1}}
There is deep_merge
yourhash.deep_merge(yourhash) {|_,_,v| v.to_s}
Merge the hash with itself, inspect the value and call to_s on it.
This method requires require 'active_support/core_ext/hash' at the top of file if you are not using ruby on rails.
Obviously, you may handle the conversion of v inside the deep_merge as you like to meet your requirements.
In rails console:
2.3.0 :001 > h1 = { a: true, b: { c: [1, 2, 3] } }
=> {:a=>true, :b=>{:c=>[1, 2, 3]}}
2.3.0 :002 > h1.deep_merge(h1) { |_,_,v| v.to_s}
=> {:a=>"true", :b=>{:c=>"[1, 2, 3]"}}
Well, it's quite simple to write it - so why don't write your own and be absolutely sure how does it behave in all situations ;)
def to_utf8(h)
if h.is_a? String
return h.force_encoding('utf-8')
elsif h.is_a? Symbol
return h.to_s.force_encoding('utf-8').to_sym
elsif h.is_a? Numeric
return h
elsif h.is_a? Array
return h.map { |e| to_utf8(e) }.to_s
else
return h.to_s.force_encoding('utf-8')
end
return hash.to_a.map { |e| result.push(to_utf8(e[0], e[1])) }.to_h
end
You may want to check if all behavior and conversions are correct - and change it if necessary.

Convert array of JSON to array of Activerecord models?

I have an array of JSON strings. How do I convert them to an array of Activerecord models?
My current code looks like this, and I'd to not iterate it one by one:
jsons = ['{"id": 1, "field1" : "value1"}'] #this is an array of jsons
models = [] #i want an array of models back
jsons.each do |json|
if(json == nil)
next
end
begin
hash = JSON.parse(json)
rescue
next
end
model = className.new
model.attributes = hash
model.id = hash["id"]
models << model
end
You should do:
models = jsons.compact.map { |json| Klass.new(JSON.parse(json)) }
where Klass is the ActiveRecord model class
EDIT
Based on the comments, you don't want to mass assign ID's, it could really get clumsy, it's better to leave rails to generate it for you, non mass assignment is:
models = jsons.compact.map { |json| Klass.new(JSON.parse(json).except(:id, "id")) }
:id, "id" is because I am not sure if the parsed JSON uses symbol or strings as keys

Ruby convert all values in a hash to string

I have the following snippet of code to fetch certain columns from the db:
#data = Topic.select("id,name").where("id in (?)",#question.question_topic.split(",")).map(&:attributes)
In the resulting Array of Hashes which is :
Current:
#data = [ { "id" => 2, "name" => "Sports" }]
To be changed to:
#data = [ { "id" => "2", "name" => "Sports" }]
I want to convert "id" to string from fixnum. Id is integer in the db. What is the cleanest way to do this?
Note: After using .map(&:attributes) it is not an active record relation.
You can do it with proper map usage:
topics = Topic.select("id,name").where("id in (?)",#question.question_topic.split(","))
#data = topics.map do |topic|
{
'id' => topic.id.to_s,
'name' => topic.name
}
end
What you're looking for is simply
#data.each { |obj| obj["id"] = obj["id"].to_s }
There isn't really a simpler way (I think that's straightforward enough anyway).
Going by the title which implies a different question - converting every value in the hash to a string you can do this:
#data.each do |obj|
obj.map do |k, v|
{k => v.to_s}
end
end
Just leaving that there anyway.
You can use Ruby's #inject here:
#data.map do |datum|
new_datum = datum.inject({}) do |converted_datum, (key, value)|
converted_datum[key] = value.to_s
converted_datum
end
end
This will work to convert all values to strings, regardless of the key.
If you are using Rails it can be even cleaner with Rails' #each_with_object:
#data.map do |datum|
datum.each_with_object({}) do |(key, value), converted_datum|
converted_datum[key] = value.to_s
end
end
This will iterate all the key names in the hash and replace the value with the #to_s version of the datum associated with the key. nil's converted to empty strings. Also, this assumes you don't have complex data within the hash like embedded arrays or other hashes.
def hash_values_to_string(hash)
hash.keys.each {|k| hash[k]=hash[k].to_s}; hash
end

Is there a better Ruby or Rails idiom for checking for the presence of values in a nested hash?

If the value:
myhash['first_key']['second_key']
exists, then I need to get it. But 'second_key' may not be present at all in my_hash, and I don't want that line to throw an exception if it is not.
Right now I am wrapping the whole thing in an ugly conditional like so:
if myhash['first_key'].present? and myhash['first_key']['second_key'].present?
...
end
I'm sure there must be something simpler.
You can always use try:
hsh.try(:[], 'first_key').try(:[], 'second_key')
FYI: if you're doing a lot of these checks, you might want to refactor your code to avoid these situations.
When in doubt, write a wrapper:
h = {
first_key: {
second_key: 'test'
}
}
class Hash
def fetch_path(*parts)
parts.reduce(self) do |memo, key|
memo[key] if memo
end
end
end
h.fetch_path(:first_key, :second_key) # => "test"
h.fetch_path(:first_key, :third_key) # => nil
h.fetch_path(:first_key, :third_key, :fourth_key) # => nil
h.fetch_path(:foo, :third_key) # => nil
Try this neat and clean solution. Hash default values:
h = Hash.new( {} ) # sets a hash as default value
Now do what you like:
h[:some_key] # => {}
h[:non_existent_key][:yet_another_non_existent_key] # => nil
Nice?
Say you have an existing hash, which is already populated:
h = { a: 1, b: 2, c: 3 }
So you just set its default to return a new hash:
h.default = {}
And there you go again:
h[:d] # => {}
h[:d][:e] # => nil
I'd point you to the excellent Hashie::Mash
An example:
mash = Hashie::Mash.new
# Note: You used to be able to do : `mash.hello.world` and that would return `nil`
# However it seems that behavior has changed and now you need to use a `!` :
mash.hello!.world # => nil # Note use of `!`
mash.hello!.world = 'Nice' # Multi-level assignment!
mash.hello.world # => "Nice"
# or
mash.hello!.world # => "Nice"
You could set up some default values before processing the hash. Something like:
myhash[:first_key] ||= {}
if myhash[:first_key][:second_key]
# do work
end
Why not define a method for this?
class Hash
def has_second_key?(k1,k2)
self[k1] ? self[k1][k2] : nil
end
end
new_hash = {}
new_hash["a"] = "b"
new_hash["c"] = {"d"=>"e","f"=>"g"}
new_hash[:p] = {q:"r"}
new_hash.has_second_key?("r","p")
# =>nil
new_hash.has_second_key?("c","f")
# =>"g"
new_hash.hash_second_key?(:p,:q)
# =>"r"
To modify your code, it would be:
if myhash.has_second_key?('first-key','second-key')
...
end
This method will return nil, which is Falsey in Ruby, or will return the value of the second key which is Truthy in Ruby.
Obviously you do not have to modify the Hash class if you don't want to. You could have the method except the hash as an argument too. has_second_key?(hash,k1,k2). Then call it as:
has_second_key?(myhash,'first-key','second-key')

what is the best way to convert a json formatted key value pair to ruby hash with symbol as key?

I am wondering what is the best way to convert a json formatted key value pair to ruby hash with symbol as key:
example:
{ 'user': { 'name': 'foo', 'age': 40, 'location': { 'city' : 'bar', 'state': 'ca' } } }
==>
{ :user=>{ :name => 'foo', :age =>'40', :location=>{ :city => 'bar', :state=>'ca' } } }
Is there a helper method can do this?
using the json gem when parsing the json string you can pass in the symbolize_names option. See here: http://flori.github.com/json/doc/index.html (look under parse)
eg:
>> s ="{\"akey\":\"one\",\"bkey\":\"two\"}"
>> JSON.parse(s,:symbolize_names => true)
=> {:akey=>"one", :bkey=>"two"}
Leventix, thank you for your answer.
The Marshal.load(Marshal.dump(h)) method probably has the most integrity of the various methods because it preserves the original key types recursively.
This is important in case you have a nested hash with a mix of string and symbol keys and you want to preserve that mix upon decode (for instance, this could happen if your hash contains your own custom objects in addition to highly complex/nested third-party objects whose keys you cannot manipulate/convert for whatever reason, like a project time constraint).
E.g.:
h = {
:youtube => {
:search => 'daffy', # nested symbol key
'history' => ['goofy', 'mickey'] # nested string key
}
}
Method 1: JSON.parse - symbolizes all keys recursively => Does not preserve original mix
JSON.parse( h.to_json, {:symbolize_names => true} )
=> { :youtube => { :search=> "daffy", :history => ["goofy", "mickey"] } }
Method 2: ActiveSupport::JSON.decode - symbolizes top-level keys only => Does not preserve original mix
ActiveSupport::JSON.decode( ActiveSupport::JSON.encode(h) ).symbolize_keys
=> { :youtube => { "search" => "daffy", "history" => ["goofy", "mickey"] } }
Method 3: Marshal.load - preserves original string/symbol mix in the nested keys. PERFECT!
Marshal.load( Marshal.dump(h) )
=> { :youtube => { :search => "daffy", "history" => ["goofy", "mickey"] } }
Unless there is a drawback that I'm unaware of, I'd think Method 3 is the way to go.
Cheers
There isn't anything built in to do the trick, but it's not too hard to write the code to do it using the JSON gem. There is a symbolize_keys method built into Rails if you're using that, but that doesn't symbolize keys recursively like you need.
require 'json'
def json_to_sym_hash(json)
json.gsub!('\'', '"')
parsed = JSON.parse(json)
symbolize_keys(parsed)
end
def symbolize_keys(hash)
hash.inject({}){|new_hash, key_value|
key, value = key_value
value = symbolize_keys(value) if value.is_a?(Hash)
new_hash[key.to_sym] = value
new_hash
}
end
As Leventix said, the JSON gem only handles double quoted strings (which is technically correct - JSON should be formatted with double quotes). This bit of code will clean that up before trying to parse it.
Recursive method:
require 'json'
def JSON.parse(source, opts = {})
r = JSON.parser.new(source, opts).parse
r = keys_to_symbol(r) if opts[:symbolize_names]
return r
end
def keys_to_symbol(h)
new_hash = {}
h.each do |k,v|
if v.class == String || v.class == Fixnum || v.class == Float
new_hash[k.to_sym] = v
elsif v.class == Hash
new_hash[k.to_sym] = keys_to_symbol(v)
elsif v.class == Array
new_hash[k.to_sym] = keys_to_symbol_array(v)
else
raise ArgumentError, "Type not supported: #{v.class}"
end
end
return new_hash
end
def keys_to_symbol_array(array)
new_array = []
array.each do |i|
if i.class == Hash
new_array << keys_to_symbol(i)
elsif i.class == Array
new_array << keys_to_symbol_array(i)
else
new_array << i
end
end
return new_array
end
Of course, there is a json gem, but that handles only double quotes.
Another way to handle this is to use YAML serialization/deserialization, which also preserves the format of the key:
YAML.load({test: {'test' => { ':test' => 5}}}.to_yaml)
=> {:test=>{"test"=>{":test"=>5}}}
Benefit of this approach it seems like a format that is better suited for REST services...
The most convenient way is by using the nice_hash gem: https://github.com/MarioRuiz/nice_hash
require 'nice_hash'
my_str = "{ 'user': { 'name': 'foo', 'age': 40, 'location': { 'city' : 'bar', 'state': 'ca' } } }"
# on my_hash will have the json as a hash
my_hash = my_str.json
# or you can filter and get what you want
vals = my_str.json(:age, :city)
# even you can access the keys like this:
puts my_hash._user._location._city
puts my_hash.user.location.city
puts my_hash[:user][:location][:city]
If you think you might need both string and symbol keys:
JSON.parse(json_string).with_indifferent_access

Resources