Ruby: Convert nested hash to object? - ruby

I am trying to convert a hash that includes nested hashes to object, such that attributes (including nested attributes) can be accessed using dot syntax.
So far first hash object is converted successfully by this code:
class Hashit
def initialize(hash)
hash.each do |k,v|
self.instance_variable_set("##{k}", v)
self.class.send(:define_method, k, proc{self.instance_variable_get("##{k}")})
self.class.send(:define_method, "#{k}=", proc{|v| self.instance_variable_set("##{k}", v)})
end
end
end
The problem is, this approach doesn't work for nested hashes:
h = Hashit.new({a: '123r', b: {c: 'sdvs'}})
=> #<Hashit:0x00000006516c78 #a="123r", #b={:c=>"sdvs"}>
Note that in the output, #b={:c=>"sdvs"} wasn't converted; it's still a hash.
How can I convert a nested hash to an object?

You can use OpenStruct
http://ruby-doc.org/stdlib-2.0.0/libdoc/ostruct/rdoc/OpenStruct.html
user = OpenStruct.new({name: "Jimmy Cool", age: "25"})
user.name #Jimmy Cool
user.age #25

Another way is to use JSON and OpenStruct, which are standard ruby libs:
irb:
> require 'JSON'
=> true
> r = JSON.parse({a: { b: { c: 1 }}}.to_json, object_class: OpenStruct)
=> #<OpenStruct a=#<OpenStruct b=#<OpenStruct c=1>>>
> r.a.b.c
=> 1

You need to add recursivity:
class Hashit
def initialize(hash)
hash.each do |k,v|
self.instance_variable_set("##{k}", v.is_a?(Hash) ? Hashit.new(v) : v)
self.class.send(:define_method, k, proc{self.instance_variable_get("##{k}")})
self.class.send(:define_method, "#{k}=", proc{|v| self.instance_variable_set("##{k}", v)})
end
end
end
h = Hashit.new({a: '123r', b: {c: 'sdvs'}})
# => #<Hashit:0x007fa6029f4f70 #a="123r", #b=#<Hashit:0x007fa6029f4d18 #c="sdvs">>

Ruby has an inbuilt data structure OpenStruct to solve something like this. Still, there is a problem. It is not recursive. So, you can extend OpenStruct class like this:
# Keep this in lib/open_struct.rb
class OpenStruct
def initialize(hash = nil)
#table = {}
if hash
hash.each_pair do |k, v|
k = k.to_sym
#table[k] = v.is_a?(Hash) ? OpenStruct.new(v) : v
end
end
end
def method_missing(mid, *args) # :nodoc:
len = args.length
if mname = mid[/.*(?==\z)/m]
if len != 1
raise ArgumentError, "wrong number of arguments (#{len} for 1)", caller(1)
end
modifiable?[new_ostruct_member!(mname)] = args[0].is_a?(Hash) ? OpenStruct.new(args[0]) : args[0]
elsif len == 0 # and /\A[a-z_]\w*\z/ =~ mid #
if #table.key?(mid)
new_ostruct_member!(mid) unless frozen?
#table[mid]
end
else
begin
super
rescue NoMethodError => err
err.backtrace.shift
raise
end
end
end
end
and remember to require 'open_struct.rb' next time you want to use OpenStruct.
Now you can do something like this:
person = OpenStruct.new
person.name = "John Smith"
person.age = 70
person.more_info = {interests: ['singing', 'dancing'], tech_skills: ['Ruby', 'C++']}
puts person.more_info.interests
puts person.more_info.tech_skills

You could check the type on v when you initialize the object and call new to get a new Hashit when it is a another hash.
class Hashit
def initialize(hash)
hash.each do |k,v|
self.instance_variable_set("##{k}", v.is_a?(Hash) ? Hashit.new(v) : v)
self.class.send(:define_method, k, proc{self.instance_variable_get("##{k}")})
self.class.send(:define_method, "#{k}=", proc{|v| self.instance_variable_set("##{k}", v)})
end
end
end
and the resulting snippet from before would be:
h = Hashit.new({a: '123r', b: {c: 'sdvs'}})
=> #<Hashit:0x007fa71421a850 #a="123r", #b=#<Hashit:0x007fa71421a5a8 #c="sdvs">>

If I understand the question correctly, this should do it:
class Hashit
def initialize(hash)
convert_to_obj(hash)
end
private
def convert_to_obj(h)
h.each do |k,v|
self.class.send(:attr_accessor, k)
instance_variable_set("##{k}", v)
convert_to_obj(v) if v.is_a? Hash
end
end
end
h = Hashit.new( { a: '123r',
b: { c: 'sdvs', d: { e: { f: 'cat' }, g: {h: 'dog'} } } })
#=> #<Hashit:0x000001018eee58 #a="123r",
# #b={:c=>"sdvs", :d=>{:e=>{:f=>"cat"}, :g=>{:h=>"dog"}}},
# #c="sdvs", #d={:e=>{:f=>"cat"}, :g=>{:h=>"dog"}},
# #e={:f=>"cat"}, #f="cat", #g={:h=>"dog"}, #h="dog">
h.instance_variables
#=> [:#a, :#b, :#c, :#d, :#e, :#f, :#g, :#h]
Hashit.instance_methods(false)
#=> [:a, :a=, :b, :b=, :c, :c=, :d, :d=, :e, :e=, :f, :f=, :g, :g=, :h, :h=]
h.d
#=> {:e=>{:f=>"cat"}}
h.d = "cat"
h.d
#=> "cat"

Related

Apply a method to an array and return array value as a key

I would like to know if ruby has a builtin method to do the following.
from this array:
array = [:foo, :bar]
and this method:
def content_for_key key
return :baz if key == :foo
return :qux if key == :bar
end
and this call:
array.some_built_in_ruby_method(&:content_for_key)
we get:
{
:foo => :baz,
:bar => :qux,
}
You could use map to convert each element to a [key, value] pair:
array.map { |key| [key, content_for_key(key)] }
#=> [[:foo, :baz], [:bar, :qux]]
followed by to_h to transform the nested array into a hash:
array.map { |key| [key, content_for_key(key)] }.to_h
#=> {:foo=>:baz, :bar=>:qux}
Or you could use each_with_object to populate a hash while traversing the array:
array.each_with_object({}) { |key, hash| hash[key] = content_for_key(key) }
#=> {:foo=>:baz, :bar=>:qux}
You could go with something like this. Although it lacks readability in comparison to the answer you already have
array = [:foo, :bar, :mnky]
def content_for_key key
return :baz if key == :foo
return :qux if key == :bar
end
array.zip(array.map(&method(:content_for_key))).to_h
#=> {:foo=>:baz, :bar=>:qux, :mnky => nil}
# or
[array,array.map(&method(:content_for_key))].transpose.to_h
#=> {:foo=>:baz, :bar=>:qux, :mnky=>nil}

Automatically generate 'attr_reader' for symbols

I have the following code where I set attr_reader and attr_writer manually.
class Pairs
attr_reader :pair, :asks, :bids, :isFrozen, :seq, :main_currency, :sub_currency
attr_writer :pair, :asks, :bids, :isFrozen, :seq
def initialize (key, args)
#pair = key
#main_currency, #sub_currency = key.split('_')
args.each {|k,v|
if numeric?(v) then v=v.to_f end
self.instance_variable_set("##{k}".to_sym, v)
}
end
private
def numeric?(string)
Float(string) != nil rescue false
end
end
Is there a way to automatically set them based on the keys of the arguments, like I'm automatically filling #k with v? Can I set attr_reader for each #k?
I suppose something like:
self.attr_reader("##{k}")
or even better for all objects of the class, something like:
Pairs << attr_reader("##{k}")
I am going to assume that you may be creating this with many keys specific to different Hash if this is the case then rather than clutter the individual instances with unneeded readers for non existent keys let's use the singleton_class for this.
So your final Pairs class could look something like
class Pairs
attr_reader :main_currency, :sub_currency
attr_accessor :pair, :asks, :bids, :isFrozen, :seq
def initialize (key, args)
#pair = key
#main_currency, #sub_currency = key.split('_')
args.each do |k,v|
singleton_class.send(:attr_reader,k)
instance_variable_set("##{k}", convert_numeric(v))
end
# Alternatively:
# args.each do |k,v|
# val = convert_numeric(v)
# define_singleton_method(k) {val}
# end
end
private
def convert_numeric(val)
Float(Rational(val)) rescue val
end
end
TL;DR
For Example: (using #mudasobwa's approach)
class C
def extend_self_with_reader name
self.class.send :attr_reader, name
end
def initialize *keys
keys.each(&method(:extend_self_with_reader))
end
end
This causes subsequent readers to clutter the instance and bleed across instances:
a = C.new(:a,:b)
a.a #=> nil
b = C.new
b.a #=> nil
c = C.new(:r)
c.a #=> nil
c.r #=> nil
a.methods.sort - Object.methods
#=> [:a, :b, :extend_self_with_reader, :r]
a.r #=> nil (hmmmmm)
Instead localize these readers buy using the singleton_class of the instance like:
class C
def initialize *keys
singleton_class.send(:attr_reader, *keys)
end
end
Then
a = C.new(:a,:b)
a.a #=> nil
b = C.new
b.a #=> NoMethodError: undefined method `a'
c = C.new(:r)
c.a #=> NoMethodError: undefined method `a'
c.r #=> nil
a.r #=> NoMethodError: undefined method `r'
a.methods.sort - Object.methods
#=> [:a,:b]
b.methods.sort - Object.methods
#=> []
Using the singleton_class localizes these readers to the instance of the object rather than bleeding them into the Class definition. If attr_reader is not a requirement then this would also be sufficient:
keys.each {|k| define_singleton_method(k) {}}
I doubt I understood the question, but from what I get you want to dynamically extend your class with attribute readers at runtime.
This method would do:
def extend_self_with_reader name
self.class.send :attr_reader, name
end
Test:
class C
def extend_self_with_reader name
self.class.send :attr_reader, name
end
def initialize *keys
puts keys.inspect
keys.each(&method(:extend_self_with_reader))
end
end
cc = C.new(*%i|a b c|)
cc.a #⇒ nil
Perhaps look into define_method.
I'm not 100% sure that I understand the problem, but check this out:
hash = { a: 1, b: 2, c: 3 }
hash.keys.each do |key|
define_method(key) do
hash[key]
end
end
There are now methods for a, b, and c:
a => 1
b => 2
c => 3
That essentially makes an attr_reader for all the keys in the hash. You could do something similar for an attr_writer.

Getting #with_index when inheriting from Array

I'd like my SomeArray#map to return an "array" of the SomeArray class.
class SomeArray < Array
def map
SomeArray.new(super)
end
end
some_array = SomeArray.new(["foo", "bar", "baz"])
p some_array.class #=> SomeArray
p some_array.map { |e| e }.class #=> SomeArray
Except now I also want to be able to use the Enumerator#with_index instance method. So ideally something like this would work:
some_array.map.with_index { |e, i| e }.class #=> SomeArray
How would that work?
I've tried:
class SomeArray < Array
def map
SomeArray.new(super)
end
def with_index(offset = 0)
super
end
end
some_array = SomeArray.new(["foo", "bar", "baz"])
p some_array.class #=> SomeArray
p some_array.map.with_index { |e, i| e }.class #=> no implicit conversion of Enumerator into Integer (TypeError)
But it's not working.
I think the problem here is that you're treating enumerable and array as if they're the same, which they're not.
Specifically, this in the map call: SomeArray.new(super).
I can reproduce your error:
[6] pry(main)> Array.new [1].map
TypeError: no implicit conversion of Enumerator into Integer
Now, when you pass a block to map, it works:
Array.new([1].map { |x| x })
=> [1]
But in your map.with_index you're not doing this.
You can do something like this:
module Foo
include Enumerable
def map
puts "calling my map"
super
end
def with_index
puts "calling my with_index"
super
end
end
class MyArr < Array
include Foo
end
puts MyArr.new([1]).map.with_index { |x, y| [x,y] }
# calling my map
# calling my with_index
# 1
# 0
Which kind of begs the question of why you're writing this class that just calls super. But in the case that you want to modify the default functionality of enumerable, this is one way.
The main problem is that map is an Array method while with_index is an Enumerator method.
Array methods are defined to just call super, and convert the output array to SomeArray.
Enumerator methods are defined with a to_enum first, convert the output to an Array and then to SomeArray.
It probably isn't the best structure for what you want to do, and isn't very efficient either. It's just a proof a concept!
class SomeArray < Array
# Array methods are overwritten here :
[:map, :select, :reject].each do |array_method_name|
define_method array_method_name do |*p, &block|
SomeArray.new(super(*p, &block).to_a)
end
end
# Enumerator methods are defined for SomeArray here :
[:with_index, :with_object].each do |enum_method_name|
define_method enum_method_name do |*p, &block|
SomeArray.new(to_enum.public_send(enum_method_name, *p, &block).to_a)
end
end
end
some_array = SomeArray.new(%w(foo bar baz biz))
p some_array.map { |s| s * 2 }.with_index.select { |_, i| i.even? }
#=> [["foofoo", 0], ["bazbaz", 2]]
p some_array.map { |s| s * 2 }.with_index.select { |_, i| i.even? }.class
#=> SomeArray

Overriding getter of OpenStruct in order to print it as a Hash

GOAL: Values of an OpenStruct object should be printed as a hash rather than an object
POSSIBLE SOLUTION: Override getter of the OpenStruct class
MyOpenStruct overrides new, to_h and [] of OpenStruct.
class MyOpenStruct < OpenStruct
def initialize(object=nil)
#table = {}
#hash_table = {}
if object
object.each do |k,v|
if v.is_a?(Array)
other = Array.new()
v.each { |e| other.push(self.class.new(entry)) }
v = other
end
#table[k.to_sym] = (v.is_a?(Hash) ? self.class.new(v) : v)
#hash_table[k.to_sym] = v
new_ostruct_member(k)
end
end
end
def [](val)
#hash_table[val.to_sym]
end
end
But overriding [] is not making any difference. E.g.
irb(main):007:0> temp = MyOpenStruct.new({"name"=>"first", "place"=>{"animal"=>"thing"}})
=> #<MyOpenStruct name="first", place=#<MyOpenStruct animal="thing">>
irb(main):008:0> temp.name
=> "first"
irb(main):009:0> temp.place
=> #<MyOpenStruct animal="thing">
irb(main):010:0> temp["place"]
=> {"animal"=>"thing"}
irb(main):011:0> temp[:place]
=> {"animal"=>"thing"}
irb(main):012:0> temp
=> #<MyOpenStruct name="first", place=#<MyOpenStruct animal="thing">>
Only when I access the keys using [] a hash is returned!!
How can I correct this??
I don't believe create a nested OpenStruct makes sense if you are returning it as a Hash. That's the way OpenStruct works:
require 'ostruct'
struct = OpenStruct.new(name: 'first', place: { animal: 'thing' })
struct.place
# => {:animal=>"thing"}
struct.place[:animal]
# => "thing"
struct.place.animal
# => NoMethodError: undefined method `animal' for {:animal=>"thing"}:Hash
So, if you want to use the dot notation to get struct.place.animal, you need to create nested OpenStruct objects like you did.
But, as I said, you don't need to override the [] method. Using your class without override the [] I get this:
struct = MyOpenStruct.new(name: 'first', place: { animal: 'thing' })
# => #<MyOpenStruct name="first", place=#<MyOpenStruct animal="thing">>
struct.place
# => #<MyOpenStruct animal="thing">
struct.place.animal
# => "thing"
Anyway, if you really want to make the dot notation work as you asked, you can override the new_ostruct_member method, which is used internally to create dynamic attributes when setting the OpenStruct object.
You can try something like this, but I don't recommend:
class MyOpenStruct < OpenStruct
def initialize(object=nil)
#table = {}
#hash_table = {}
if object
object.each do |k,v|
if v.is_a?(Array)
other = Array.new()
v.each { |e| other.push(self.class.new(entry)) }
v = other
end
#table[k.to_sym] = (v.is_a?(Hash) ? self.class.new(v) : v)
#hash_table[k.to_sym] = v
new_ostruct_member(k)
end
end
end
def [](val)
#hash_table[val.to_sym]
end
protected
def new_ostruct_member(name)
name = name.to_sym
unless respond_to?(name)
# use the overrided `[]` method, to return a hash
define_singleton_method(name) { self[name] }
define_singleton_method("#{name}=") { |x| modifiable[name] = x }
end
name
end
end
struct = MyOpenStruct.new(name: 'first', place: { animal: 'thing' })
# => #<MyOpenStruct name="first", place=#<MyOpenStruct animal="thing">>
struct.place
# => {:animal=>"thing"}
struct.place[:animal]
# => "thing"
#Doguita's answer is correct in all respects. I just wanted to answer your question "If that's not possible then can I print a hash of the whole object temp?" Yes, you can. You just need to override to_h to recursively walk your keys and values and convert instances of MyOpenStruct to a Hash:
def to_h
#table.each_with_object({}) do |(key, val), table|
table[key] = to_h_convert(val)
end
end
private
def to_h_convert(val)
case val
when self.class
val.to_h
when Hash
val.each_with_object({}) do |(key, val), hsh|
hsh[key] = to_h_convert(val)
end
when Array
val.map {|item| to_h_convert(item) }
else
val
end
end

can I pass a hash to an instance method to create instance variables?

I have a has like this:
reminder_hash = {"bot_client_id"=>"test-client-id", "recurring"=>true, "recurring_natural_language"=>"everyday", "time_string"=>"10AM", "time_zone"=>"America/Los_Angeles", "via"=>"slack", "keyword"=>"test-keyword", "status"=>"active", "created_time"=>1444366166000}
I would like to pass to method like this (from Rspec):
let(:reminder) { Reminder.new( reminder_hash ) }
I would like the instance variables for Reminder to be based on the hash.
expect(reminder.bot_client_id).to eq('test-client-id')
I can't get it to work. I tried the following:
class Reminder
attr_accessor :bot_client_id
def initiate(hash)
# http://stackoverflow.com/questions/1615190/declaring-instance-variables-iterating-over-a-hash
hash.each do |k,v|
instance_variable_set("##{k}",v)
# if you want accessors:
eigenclass = class<<self; self; end
eigenclass.class_eval do
attr_accessor k
end
end
end
Rspec gives the following error:
Failure/Error: let(:reminder) { Reminder.new( reminder_hash ) }
ArgumentError:
wrong number of arguments (1 for 0)
Question: How do I pass a hash to an instance method for an object, so that the hash values are the instance variables for that newly created object?
Sure you can do something like this in your initialize method:
hash.each do |k, v|
instance_variable_set("##{k}", v)
self.class.send(:attr_reader, k)
end
Here's an example using your input hash:
class Reminder
def initialize(hash)
hash.each do |k, v|
instance_variable_set("##{k}", v)
self.class.send(:attr_reader, k)
end
end
end
reminder_hash = {"bot_client_id"=>"test-client-id", "recurring"=>true, "recurring_natural_language"=>"everyday", "time_string"=>"10AM", "time_zone"=>"America/Los_Angeles", "via"=>"slack", "keyword"=>"test-keyword", "status"=>"active", "created_time"=>1444366166000}
reminder = Reminder.new(reminder_hash)
puts reminder
puts reminder.bot_client_id
Output:
#<Reminder:0x007f8a48831498>
test-client-id
Ruby has OpenStruct to do that.
require 'ostruct'
reminder_hash = {"bot_client_id"=>"test-client-id", "recurring"=>true, "recurring_natural_language"=>"everyday", "time_string"=>"10AM", "time_zone"=>"America/Los_Angeles", "via"=>"slack", "keyword"=>"test-keyword", "status"=>"active", "created_time"=>1444366166000}
reminder = OpenStruct.new(reminder_hash)
p reminder.bot_client_id # => "test-client-id"
Use a Struct for a full blown Class:
Reminder = Struct.new(*reminder_hash.keys.map(&:to_sym))
r = Reminder.new(*reminder_hash.values)
p r.bot_client_id # => "test-client-id"

Resources