Dynamically assigning attributes using strings as attribute setters in Ruby - ruby

I have a hash of name / value pairs:
attr_hash = {"attr1"=>"val1","attr2=>"val2"}
I want to cycle through each one of these values and assign them to an object like so:
thing = Thing.new
attr_hash.each do |k,v|
thing.k = v
end
class Thing
attr_accessor :attr1, :attr2
end
The problem of course being that attr1 is and attr2 are strings.. So I can't do something like thing."attr1"
I've tried doing:
thing.send(k,v) but that doesn't work

Use thing.send("#{k}=", v) instead.

You need to call the setter method, which for an attribute called name would be name=.
Following from your example:
attr_hash.each do |k,v|
thing.send("#{k}=", v)
end
Also, if this hash is coming from the user somehow, it might be a good idea to test if the setter exists before calling it, using respond_to?:
attr_hash.each do |k,v|
setter = "#{k}="
thing.send(setter, v) if thing.respond_to?(setter)
end

OpenStruct does it for you.
require 'ostruct'
attr_hash = {"attr1"=>"val1", "attr2"=>"val2"}
d = OpenStruct.new(attr_hash)
p d.attr1 #=> "val1"

Related

class inheritiance, changing a class type in ruby from a parent to a child

I have a regular hash
myhash = { :abc => 123 }
and a class
class SpecialHash < Hash
def initialize arg_hash
# how do I say
self = arg_hash
end
end
or
Is there some way of doing:
myhash.class = SpecialHash?
-daniel
The best solution depends on the library you want to extend and on what you are trying to achive.
In case of a Hash, it's quite hard to extend it in that way because there is no initializer you can override when you use the Ruby hash syntax.
However, because creating a new hash with some value is the same of merging an empty hash with the given values, you can do
class SpecialHash < Hash
def initialize(hash)
self.merge!(hash)
end
end
myhash = SpecialHash.new(:abc => 123)
myhash will then be an instance of SpecialHash with the same properties of a Hash.
Notice the use of merge! that is actually changing the value of self. Using self.merge will not have the same effect.

Creating generic constructor in ruby

I found this interesting answer :
https://stackoverflow.com/a/2348854/169277
This is ok when you're trying to set instance variables it works really great.
Is there a way to apply the same logic or better one to create generic constructor like :
def initialize(obj)
obj.each do |k,v|
#find the setter for each k and set the value v to and return newly created object
end
end
If I had object TestObject:
class TestObject
attr_accessor :name, :surname, :sex
end
I was thinking to create it something like this:
TestObject.new({:name => 'Joe', :surname => 'Satriani'})
How would one achieve this?
So doing this would be a shorthand of :
t = TestObject.new
t.name = 'Joe'
t.surname = 'Satriani'
Sure, you can use send to send arbitrary messages to an object. Since we're operating on self here, we can just invoke send directly.
def initialize(obj)
obj.each do |k,v|
send(:"#{k}=", v)
end
end
For example, TestObject.new({:name => 'Joe'}) will call send "name=", "Joe".
You can inherit from Struct to make a simple object, and then pass in the attributes to the initializer:
class TestObject < Struct.new(:name, :surname, :sex)
end
TestObject.new('Joe', 'Satriani') #=> sex will be nil
You can use OpenStruct to make quick value objects with arbitrary attributes:
t = OpenStruct(name: 'Joe', surname: 'Satriani')
You can include a module like Virtus: https://github.com/solnic/virtus
Or you can do what Chris Heald said.
I think it would be better to use keyword arguments for this. After all, the Hash keys are guaranteed to be valid Ruby identifier Symbols since they need to match up with method names. You don't need the capability to pass in arbitrary Ruby objects as keys of the Hash.
def initialize(**attrs)
attrs.each do |attr, value| send(:"#{attr}=", value) end
end
TestObject.new(name: 'Joe', surname: 'Satriani')

Elegant way to add to_hash (or to_h) method to Struct?

I'm using a Struct as opposed to a simple Hash in a project to provide a semantic name to a collection of key value pairs. Once I've built the structure, however, I need to output a hash value. I'm in Ruby 1.9.3. Example:
MyMeaninfulName = Struct.new(:alpha, :beta, :gamma) do
def to_hash
self.members.inject({}) {|h,m| h[m] = self[m]; h}
end
end
my_var = MyMeaningfulName.new
my_var.to_hash # -> { :alpha=>nil, :beta=>nil, :gamma=>nil }
Is there a reason why Struct does not include a to_hash method? It seems like a natural fit, but perhaps there's an underlying reason why it's not included.
Second, is there a more elegant way to build a generic to_hash method into Struct (either generally, via monkeypatching, or through a module or inheritance).
I know the question is about ruby 1.9.3, but starting from ruby 2.0.0, Struct has a to_h method which does the job.
MyMeaningfulName = Struct.new(:alpha, :beta, :gamma)
my_var = MyMeaningfulName.new
my_var.to_h # -> { :alpha=>nil, :beta=>nil, :gamma=>nil }
or this:
class Struct
def to_hash
self.class.members.inject({}) {|h,m| h[m] = self[m]}
end
end
(note the extra class to get to the members)
I don't know why, it does seem obvious. Fortunately, you can use it as a hash in many places since it implements bracket operators.
Anyway, this is fairly elegant:
MyMeaningfulName = Struct.new :alpha, :beta, :gamma do
def to_hash
Hash[members.zip values]
end
end
my_var = MyMeaningfulName.new 1, 2, 3
my_var.to_hash # => {:alpha=>1, :beta=>2, :gamma=>3}
Try this:
class Struct
old_new = self.method(:new)
def self.new(*args)
obj = old_new.call(*args)
obj.class_exec do
def to_hash
self.members.inject({}) {|h,m| h[m] = self[m]; h}
end
end
return obj
end
end

How can I control which fields to serialize with YAML

For instance,
class Point
attr_accessor :x, :y, :pointer_to_something_huge
end
I only want to serialize x and y and leave everything else as nil.
In Ruby 1.9, to_yaml_properties is deprecated; if you're using Ruby 1.9, a more future proof method would be to use encode_with:
class Point
def encode_with coder
coder['x'] = #x
coder['y'] = #y
end
end
In this case that’s all you need, as the default is to set the corresponding instance variable of the new object to the appropriate value when loading from Yaml, but in more comple cases you could use init_with:
def init_with coder
#x = coder['x']
#y = coder['y']
end
After an inordinate amount of searching I stumbled on this:
class Point
def to_yaml_properties
["#x", "#y"]
end
end
This method is used to select the properties that YAML serializes. There is a more powerful approach that involves custom emitters (in Psych) but I don't know what it is.
This solution only works in Ruby 1.8; in Ruby 1.9, to_yaml has switched to using Psych, for which Matt's answer using encode_with is the appropriate solution.
If you want all fields but a few, you could do this
def encode_with(coder)
vars = instance_variables.map{|x| x.to_s}
vars = vars - ['#unwanted_field1', '#unwanted_field2']
vars.each do |var|
var_val = eval(var)
coder[var.gsub('#', '')] = var_val
end
end
This stops you from manually having to manage the list. Tested on Ruby 1.9
If you have a plenty of instance variables, you could use a short version like this one
def encode_with( coder )
%w[ x y a b c d e f g ].each { |v| coder[ v ] = instance_variable_get "##{v}" }
end
You should use #encode_with because #to_yaml_properties is deprecated:
def encode_with(coder)
# remove #unwanted and #other_unwanted variable from the dump
(instance_variables - [:#unwanted, :#other_unwanted]).each do |var|
var = var.to_s # convert symbol to string
coder[var.gsub('#', '')] = eval(var) # set key and value in coder hash
end
end
or you might prefer this if eval is too dangerous and you only need to filter out one instance var. All other vars need to have an accessor:
attr_accessor :keep_this, :unwanted
def encode_with(coder)
# reject #unwanted var, all others need to have an accessor
instance_variables.reject{|x|x==:#unwanted}.map(&:to_s).each do |var|
coder[var[1..-1]] = send(var[1..-1])
end
end
I'd recommend adding a custom to_yaml method in your class that constructs the specific yaml format you want.
I know that to_json accepts parameters to tell it what attributes to serialize, but I can't find the same for to_yaml.
Here's the actual source for to_yaml:
# File activerecord/lib/active_record/base.rb, line 653
def to_yaml(opts = {}) #:nodoc:
if YAML.const_defined?(:ENGINE) && !YAML::ENGINE.syck?
super
else
coder = {}
encode_with(coder)
YAML.quick_emit(self, opts) do |out|
out.map(taguri, to_yaml_style) do |map|
coder.each { |k, v| map.add(k, v) }
end
end
end
end
So it looks like there may be an opportunity to set opts so that it includes specific key/value pairs in the yaml.

Ruby create methods from a hash

I have the following code I am using to turn a hash collection into methods on my classes (somewhat like active record). The problem I am having is that my setter is not working. I am still quite new to Ruby and believe I've gotten myself turned around a bit.
class TheClass
def initialize
#properties = {"my hash"}
self.extend #properties.to_methods
end
end
class Hash
def to_methods
hash = self
Module.new do
hash.each_pair do |key, value|
define_method key do
value
end
define_method("#{key}=") do |val|
instance_variable_set("##{key}", val)
end
end
end
end
end
The methods are created and I can read them on my class but setting them does not work.
myClass = TheClass.new
item = myClass.property # will work.
myClass.property = item # this is what is currently not working.
If your goal is to set dynamic properties then you could use OpenStruct.
require 'ostruct'
person = OpenStruct.new
person.name = "Jennifer Tilly"
person.age = 52
puts person.name
# => "Jennifer Tilly"
puts person.phone_number
# => nil
It even has built-in support to create them from a hash
hash = { :name => "Earth", :population => 6_902_312_042 }
planet = OpenStruct.new(hash)
Your getter method always returns the value in the original hash. Setting the instance variable won't change that; you need to make the getter refer to the instance variable. Something like:
hash.each_pair do |key, value|
define_method key do
instance_variable_get("##{key}")
end
# ... define the setter as before
end
And you also need to set the instance variables at the start, say by putting
#properties.each_pair do |key,val|
instance_variable_set("##{key}",val)
end
in the initialize method.
Note: I do not guarantee that this is the best way to do it; I am not a Ruby expert. But it does work.
It works just fine for me (after fixing the obvious syntax errors in your code, of course):
myClass.instance_variable_get(:#property) # => nil
myClass.property = 42
myClass.instance_variable_get(:#property) # => 42
Note that in Ruby instance variables are always private and you never define a getter for them, so you cannot actually look at them from the outside (other than via reflection), but that doesn't mean that your code doesn't work, it only means that you cannot see that it works.
This is essentially what I was suggesting with method_missing. I'm not familiar enough with either route to say why or why not to use it which is why I asked above. Essentially this will auto-generate properties for you:
def method_missing sym, *args
name = sym.to_s
aname = name.sub("=","")
self.class.module_eval do
attr_accessor aname
end
send name, args.first unless aname == name
end

Resources