0

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??

Animesh Pandey
  • 5,900
  • 13
  • 64
  • 130
  • Your question isn't clear. What is your goal? What is the output you're expecting, and what is the output you're getting instead? – Jordan Running Oct 28 '15 at 18:16
  • I want `temp.place` to return a hash rather than a `MyOpenConstruct` object. Overriding is not helping. The input hash can be a lot deeper then the one given. Every access should return a hash – Animesh Pandey Oct 28 '15 at 18:18
  • 1
    Why do you want create a nested OpenStruct if you are returning it as a hash? – Doguita Oct 28 '15 at 18:22
  • @Doguita How will I get a dot access to values? I mean `temp.name` and `temp[:name]` are not going to be same. As of now both are working. Its just I need a hash from both. `temp.name` returns an object but `temp[:name]` returns a hash – Animesh Pandey Oct 28 '15 at 18:24
  • If you want `place` to return something different, then you need to override it in your subclass. Likewise for `name`. – Jörg W Mittag Oct 28 '15 at 18:29
  • @JörgWMittag According to OpenStruct's [docs](http://ruby-doc.org/stdlib-2.0.0/libdoc/ostruct/rdoc/OpenStruct.html#method-i-5B-5D) `[]` should word both for brackets as well as dots. – Animesh Pandey Oct 28 '15 at 18:50
  • It doesn't say that `foo.bar` is based on `foo[:bar]`. Quite the opposite, actually: *iff* you want to construe that simple one-line sentence as any part of official guarantee *at all*, then all that it says is that `foo[:bar]` is based on `foo.bar`. But I doubt that that's what that sentence means. Maybe, if you're lucky, you'll find a conformance test in the [YARV test suite](https://github.com/ruby/ruby/blob/trunk/test/ostruct/test_ostruct.rb#L96-L109), which tells you exactly what kind of guarantees `OpenStruct` makes, but I doubt it. Test coverage is very spotty for the stdlib. – Jörg W Mittag Oct 28 '15 at 18:54
  • @JörgWMittag So is there no way to get it working? I am actually stuck at only at this part and I need both `.` & `[]` to return me the same thing i.e. a hash! Is there a function using which I can override `.`? – Animesh Pandey Oct 28 '15 at 18:58
  • If you want to override `foo.name`, you have to implement a `name` method. If you want to override `foo.place`, you need to implement a `place` method, and so on. – Jörg W Mittag Oct 28 '15 at 19:03
  • @JörgWMittag If that's not possible then can I print a hash of the whole object `temp`? without using `to_h` – Animesh Pandey Oct 28 '15 at 19:33
  • As @Doguita explained below, there's no good way to make `temp.place` return a Hash and to also be able to do `temp.place.animal`. I suspect you have an [XY problem](http://meta.stackexchange.com/a/66378/140350). Can you edit your question to explain *why* you want these methods to return a Hash? What problem are you trying to solve? – Jordan Running Oct 28 '15 at 21:06

2 Answers2

2

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
  • 15,403
  • 3
  • 27
  • 36
  • @AnimeshPandey Doguita spent the first two paragraphs explaining why that would happen. Go back and read them. If `struct.place` returns a Hash, you can't do `struct.place.animal`, because the Hash won't have an `animal` method. There's no way around that. – Jordan Running Oct 28 '15 at 20:58
1

@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
Jordan Running
  • 102,619
  • 17
  • 182
  • 182
  • Yes, @Doguita's ans seems correct but `struct.place.animal` raises a `NoMethodError`!!! – Animesh Pandey Oct 28 '15 at 20:57
  • 1
    @AnimeshPandey @Doguita spent the first two paragraphs explaining exactly why that would happen. Go back and read them. If `struct.place` returns a Hash, you can't do `struct.place.animal`, because the Hash won't have an `animal` method. There's no way around that. – Jordan Running Oct 28 '15 at 20:59
  • Yeah, okay I get the point. and he is right as well. It is what it is!! – Animesh Pandey Oct 28 '15 at 21:09
  • The answers are a great source of information for me. I think as of now I am satisfied with the whatever has been suggested. – Animesh Pandey Oct 28 '15 at 21:18