0

I've got a class, like this one:

class A
  attr_accessor(:field2)
  attr_accessor(:field1)
end

What's the best way to produce a Hash out of it with keys and values taken from the class instance?

And what's the best way to populate the instance of class A with values from that Hash?

=====

I'm probably looking for something similar to JavaBeans introspection that would give me the names of the data object fields, then execute logic based on this info. Ruby is a very modern flexible and dynamic language and I refuse to admit that it will not let me do things that I can easily do with Java ;-)

=====

In the end I found out that Struct is the best option:

a = {:a => 'qwe', :b => 'asd'}

s = Struct.new(*a.keys).new(*a.values) # Struct from Hash

h = Hash[*s.members.zip(s.values).flatten] # Hash from Struct
Oleg Mikheev
  • 17,186
  • 14
  • 73
  • 95
  • you must define somewhere that field1 and field2 are the fields you're interested in, a class has a lot of methods... – tokland Nov 10 '11 at 20:40
  • Yep, a lot of methods, but I'm interested in fields, is there really no way to tell if a class has just two fields in it? Something similar to JavaBeans introspection? – Oleg Mikheev Nov 10 '11 at 20:44
  • AFAIK attr_accessor adds transparently the getter/setter, so you have no way to tell them from "normal" methods/instance variables. – tokland Nov 10 '11 at 20:47
  • JavaBeans introspection detects fields in a bean by analyzing its getter/setter combinations, it's a convention, the same approach could be applied to any language including Ruby right? – Oleg Mikheev Nov 10 '11 at 20:51
  • Use the `instance_variables` method, as two of the solutions below do. – Dave Newton Nov 10 '11 at 20:54
  • oh, I see. Of course you can check this yourself, see my answer. – tokland Nov 10 '11 at 20:54
  • 1
    @Oleg It *could*, but if you're interested *only* in fields, and want a general solution, you can't rely on methods. The same is true in Java: getters can exist without a corresponding property (and often do, for example, to expose data to the view layer via JSP EL). – Dave Newton Nov 10 '11 at 20:56
  • 1
    I'm not sure what your ultimate goal is: if it's to serialize/deserialize, why not use JSON or YAML and use stuff that already exists? – Dave Newton Nov 10 '11 at 20:59
  • @Dave Newton I'm just playing with MongoDB and need a Hash to persist objects, I'm not willing to use any mappers at the moment – Oleg Mikheev Nov 10 '11 at 21:05
  • Often, the good Java solution is not the good Ruby solution... – Marnen Laibow-Koser Nov 10 '11 at 21:28
  • yes, I'm just learning the Ruby philosophy yet, to get better understanding of what's good and what's not so good – Oleg Mikheev Nov 10 '11 at 21:32

3 Answers3

1

Something to start playing with:

a = A.new
a.field1 = 1
a.field2 = 2
methods = a.public_methods(false).select { |s| s.end_with?("=") }
attributes = Hash[methods.map { |m| [m, a.send(m)] }]
=> {"field1"=>1, "field2"=>2}

If you want a more fine-grained detection of pairs getter/setter:

methods = a.public_methods(false).group_by { |s| s.split("=")[0] }.
  map { |k, vs| k if vs.size == 2 }.compact

Regarding the second question:

attributes = {"field1"=>1, "field2"=>2}
a = A.new
a.each { |k, v| a.send(k+"=", v) }
=> #<A:0x7f9d7cad7bd8 @field1=1, @field2=2>

However, it would appear you want to use something like Struct or OpenStruct.

tokland
  • 66,169
  • 13
  • 144
  • 170
  • but will OpenStruct allow me to get a Hash representation of its values? – Oleg Mikheev Nov 10 '11 at 20:57
  • Yes, I can use **OpenStruct.marshal_dump**, OpenStruct is what I really need, thanks! (hope it's not slow) – Oleg Mikheev Nov 10 '11 at 21:01
  • Yeah, marshal_dump. Check this recent question: http://stackoverflow.com/questions/8082423/returning-struct-data-as-a-hash-in-ruby – tokland Nov 10 '11 at 21:02
  • OpenStructs appear to be 100 times slower than Structs http://stackoverflow.com/questions/1177594/ruby-struct-vs-openstruct so I'm not sure now that it's a good practice to use them – Oleg Mikheev Nov 10 '11 at 21:20
1

Class to hash. Could write this as a method in A, of course, if desired.

foo = A.new
foo.field1 = "foo"
foo.field2 = "bar"
hash = {}
foo.instance_variables.each {|var| hash[var.to_s.delete("@")] = foo.instance_variable_get(var) }
p hash
 => {"field1"=>"foo", "field2"=>"bar"} 

Hash to class: extend A's initialize. Borrowed from http://pullmonkey.com/2008/01/06/convert-a-ruby-hash-into-a-class-object/ .

class A
  def initialize(hash)
    hash.each do |k,v|
      self.instance_variable_set("@#{k}", v)
    end
  end
end

Then you can:

hash = { :field1 => "hi" }
foo = A.new(hash)
 => #<A:0x00000002188c40 @field1="hi"> 
tkrajcar
  • 1,702
  • 1
  • 17
  • 34
1
 f.instance_variables.inject({}) { |m, v| m[v] = f.instance_variable_get v; m }

Although that gives you the @ in the attribute symbols; you could strip it off in the assignment if it's important. The reverse is just the opposite; iterate over the keys and use instance_variable_set.

You could also interrogate for methods ending in =, which would be more robust if you've added logic to any of them instead of relying on those created by attr_accessor.

Dave Newton
  • 158,873
  • 26
  • 254
  • 302
  • It's a reasonable solution, though eventually the OP will have more instance variables than the ones created by attr_accessor. – tokland Nov 10 '11 at 20:56
  • @tokland I don't know how you know that. – Dave Newton Nov 10 '11 at 20:57
  • Sorry, I meant "may have", not "will have", of course I don't know how his complete class looks like. – tokland Nov 10 '11 at 21:03
  • One question, Dave, I've seen in your answers that you seem to favor inject to build hashes over Hash::[]... why? efficiency? – tokland Nov 10 '11 at 21:04
  • @tokland With `[]` you either need a map, an array of tuples, or a flat, ordered array--for the above I find `inject` marginally easier to read than `Hash[f.instance_variables.collect { |v| [v, f.instance_variable_get(v)] }]`, even though the inject is a few chars longer-the closing `)]}]` makes my eyes water. Mind you, there may be an even cleaner way; haven't thought about it. – Dave Newton Nov 10 '11 at 21:12
  • IMHO the better, cleaner solution is an enumerable method that builds a hash from pairs. Alas, no luck for now: http://redmine.ruby-lang.org/issues/666 – tokland Nov 10 '11 at 21:14
  • @tokland Yeah but we've been waiting for something built-in for... awhile now; I've found that (a) I don't need it very often, and (b) when I do, it's generally been something I needed to customize anyway :/ – Dave Newton Nov 10 '11 at 21:19