How do I convert hash keys to method names? - ruby

This is my hash:
tempData = {"a" => 100, "here" => 200, "c" => "hello"}
I need to access the hash keys as a method like:
tempData.a #100
tempData.here # 200

You could just wrap up your hash in an OpenStruct:
require 'ostruct'
tempData = {"a" => 100, "here" => 200, "c" => "hello"}
os = OpenStruct.new tempData
os.a #=> 100
os.here #=> 200
If you really really wanted to, you could also monkey-patch the Hash class, but I'd advise against that:
class Hash
def method_missing(m, *args, &blk)
fetch(m) { fetch(m.to_s) { super } }
end
end
tempData = {"a" => 100, "here" => 200, "c" => "hello"}
tempData.a #=> 100
Update: In my personal extensions library I added a Hash#to_ostruct method. This will recursively convert a hash into an OpenStruct including all nested hashes.

There is another way to do this.
JSON.parse(tempData.to_json, object_class: OpenStruct)
that will give object
#<OpenStruct a=100, here=200, c="hello">
In this way nested hash also will be converted to OpenStruct Object
tempData = {a: { b: { c: 3}}, foo: 200, msg: 'test msg'}
obj = JSON.parse(tempData.to_json, object_class: OpenStruct)
Now we are able to call
obj.a.b.c # 3
obj.foo # 200
obj.msg # 'test msg'
Hope this will help someone.

Alternatively, if it’s just a small script it might be more convenient to just extend Hash itself
class Hash
def method_missing sym,*
fetch(sym){fetch(sym.to_s){super}}
end
end
method_missing is a magic method that is called whenever your code tries to call a method that does not exist. Ruby will intercept the failing call at run time and let you handle it so your program can recover gracefully. The implementation above tries to access the hash using the method name as a symbol, the using the method name as a string, and eventually fails with Ruby's built-in method missing error.
NB for a more complex script, where adding this behavior might break other third-party gems, you might alternatively use a module
and extend each instance
module H
def method_missing sym,*
fetch(sym){fetch(sym.to_s){super}}
end
end
the = { answer: 42 }
the.extend(H)
the.answer # => 42
and for greater convenience you can even propagate the module down to
nested hashes
module H
def method_missing sym,*
r = fetch(sym){fetch(sym.to_s){super}}
Hash === r ? r.extend(H) : r
end
end
the = { answer: { is: 42 } }
the.extend(H)
the.answer.is # => 42

If the hash is inside a module, you can define methods on that module (or class) dynamically using define_method. For example:
module Version
module_function
HASH = {
major: 1,
minor: 2,
patch: 3,
}
HASH.each do |name, value|
define_method(name) do
return value
end
end
end
This will define a Version module with major, minor, and patch methods that return 1, 2, and 3, respectively.

you can extend the Hash class in the following way.
class Hash
# return nil whenever the key doesn't exist
def method_missing(m, *opts)
if self.has_key?(m.to_s)
return self[m.to_s]
elsif self.has_key?(m.to_sym)
return self[m.to_sym]
end
return nil
# comment out above line and replace with line below if you want to return an error
# super
end
end

Related

attributes() method in Ruby

According to this post, to make attributes stick during to_json call, we need to do something like this:
def attributes
super.merge('foo' => self.foo)
end
With my beginner's knowledge in Ruby, I fail to understand the following:
Is there an attribute method present for every Ruby class?
What is super.merge doing here? Which hashmap does it append its argument to?
No, there is not an #attributes method for every Ruby class. The class you're using likely inherits from another class or mixes in a module which does (e.g. ActiveRecord::Base#attributes).
That attributes method that you're defining will override any existing #attributes method. In the case of an override, Ruby provides a #super method, which calls the original method that you're overriding. In this case, you'll be calling the original #attributes method which returns a Hash of attributes keys and their values (e.g. { attr1: 'a', attr2: 'b' }).
#merge is a Hash function that you're calling on the Hash that the original #attributes call returns (e.g { attr1: 'a', attr2: 'b' }). #merge creates a new hash consisting of the original attributes hash combined with the key-value pairs provided in the second hash.
From the Ruby 2.2 docs on Hash#merge:
merge(other_hash) → new_hash click to toggle source
merge(other_hash){|key, oldval, newval| block} → new_hash
Returns a new hash containing the contents of other_hash and the
contents of hsh. If no block is specified, the value for entries with
duplicate keys will be that of other_hash. Otherwise the value for
each duplicate key is determined by calling the block with the key,
its value in hsh and its value in other_hash.
h1 = { "a" => 100, "b" => 200 }
h2 = { "b" => 254, "c" => 300 }
h1.merge(h2) #=> {"a"=>100, "b"=>254, "c"=>300}
h1.merge(h2){|key, oldval, newval| newval - oldval}
#=> {"a"=>100, "b"=>54, "c"=>300}
h1 #=> {"a"=>100, "b"=>200}
http://ruby-doc.org/core-2.2.0/Hash.html#method-i-merge
Some notes about your example: 'foo' => self.foo
You don't need to specify self in self.foo. foo alone should suffice. That's really only needed for assignments self.foo = 'whatever' and in cases where you've defined another foo in the same scope.
Make sure that they type of the key you're providing matches the type of the keys that #attributes is returning.
Case 1: #attributes returns a Hash of Strings -> Values, so merge in a hash with String keys (ActiveRecord does this.)
{ 'attr1' => 1, 'attr2' => 'b' }.merge( { 'attr3' => '3' }
Case 2: #attributes returns a Hash of Symbols -> Values, so merge in a hash with Symbol keys:
{ :attr1 => 1, :attr2 => 'b' }.merge( { :attr3 => '3' }
Hi attributes is a method provided by ActiveRecord. If you click on source you will notice how it really just exposes the instance variable #attributes which is a hash (as it can be nil it is enforced to a hash through .to_hash).
class ActiveRecord::Base
def attributes
#attributes.to_hash
end
end
We'll call this method parent as we will extend its behaviour in our class. This is possible through inheritance:
class Person < ActiveRecord::Base
def attributes
super.merge('foo' => self.foo)
end
end
attributes is now calling the parent's method [Activerecord::Base].attributes and is adding a new key to the hash.
This code is roughly equivalent and should be easier to read.
class Person < ActiveRecord::Base
def attributes
attrs = super # eg. { name: "x", created_at: [...], [...] }
attrs[:foo] = self.foo # eg. { name: "x", foo: [...], ... }
attrs
end
end
Attributes are a synonym properties, variables etc on your class. Not all classes contain attributes but data models generally exist to encapsulate the attributes and methods they contain, as well as some behavior to modify and/or operate on them. To respond with a json representation of a model you would generally do something like this in your controller.
respond_to :json
def tags
render :json => User.find(params[:id]).tags
end

Ruby empty parameter in function

I'm trying to enqueue various functions in a generic way with this code :
{ Object.const_get(object_name).new(job[:params]||={}).delay(:queue => queue).send(method_name)}
job is a Hash where I get the name, objects parameters etc...
My problem is in this case :
class Foo
def initialize
puts 'bar'
end
end
Foo doesn't take parameters for its instanciation.
So if I use the previous line with Foo as object_name I'll get this error :
ArgumentError: wrong number pf arguments (1 for 0)
And I absolutly don't want to write something like that :
if job.has_key?[:param] then
Object.const_get(object_name).new(job[:params]||={}).delay(:queue => queue).send(method_name)
else
Object.const_get(object_name).new().delay(:queue => queue).send(method_name)
end
What could I write instead of job[:params]||={} so it works for every case?
Thanks in advance.
you can achieve this with using Foo.send and using an array.
For instance
Object.
const_get(object_name).
send(*(job.has_key?(:param) ? ['new', job[:param]] : ['new']))...
I personally think it is not worth it and an if statement is easier on the eyes.
The initialize method of your Foo class should receive a parameter with a default value. Like this:
class Foo
def initialize(params={})
# Here you do stuff like checking if params is empty or whatever.
end
end
This way you will achieve the two behaviors.
Based on your example, I think the test you accepted might be wrong. Your code suggests that you shouldn't be testing whether the :params key exists in the hash, you should be testing whether initialize takes an argument. If initialize does take an argument, then you send it an argument regardless of whether the :params key exists in the hash. The accepted answer will fail when the :params key doesn't exist in the hash, and yet the initialize method takes an argument--you'll get a 0 for 1 error. Is that a possibility?
class Dog
def initialize(params)
p params
puts "dog"
end
end
class Cat
def initialize
puts "cat"
end
end
class_names = ["Dog", "Cat"]
job = {
params: {a: 1, b: 2, c: 3}
}
class_names.each do |class_name|
class_obj = Object.const_get(class_name)
if class_obj.instance_method(:initialize).arity == 0
send_args = 'new'
else
send_args = 'new', job[:params] ||= {}
end
class_obj.send(*send_args)
end
--output:--
{:a=>1, :b=>2, :c=>3}
dog
cat

Monkey patching Ruby's Hash class

What could be the repercussions of monkey patching Hash to act like this:
class Hash
def method_missing(method,*args, &block)
if self.has_key?(method)
return self[method]
elsif self.has_key?(method.to_s)
return self[method.to_s]
else
return nil
end
end
end
My justification is the following:
Basically, when I add objects to hashes, I make sure their keys.to_s are unique.
Should I be concerned, Am I missing something?
h = { :foo => "bar", "hello" => "bar" }
h.foo => "bar"
h.hello => "bar"
There is a native ruby object for this: OpenStruct.
You can instantiate an OpenStruct with a hash.
o = OpenStruct.new(hello: 'world')
Once it is initialized, you can use all the keys as methods.
o.hello #=> "world"
Therefore you don't need to monkey-patch your hash (which can lead to weird behaviors indeed).
And OpenStruct manages internally the duplicates between string and symbol entries.
OpenStruct.new(:hello => 'world', 'hello' => 'world2') #=> #<OpenStruct hello="world2">
BAD IDEA!!
If you misspell any of the valid methods of hash, you will get a nil instead of the real method missing. Eg: {}.count => 0, {}.counts => nil instead of NoMethodError: undefined method `counts' for {}:Hash.

Using custom to_json method in nested objects

I have a data structure that uses the Set class from the Ruby Standard Library. I'd like to be able to serialize my data structure to a JSON string.
By default, Set serializes as an Array:
>> s = Set.new [1,2,3]
>> s.to_json
=> "[1,2,3]"
Which is fine until you try to deserialize it.
So I defined a custom to_json method:
class Set
def to_json(*a)
{
"json_class" => self.class.name,
"data" => {
"elements" => self.to_a
}
}.to_json(*a)
end
def self.json_create(o)
new o["data"]["elements"]
end
end
Which works great:
>> s = Set.new [1,2,3]
>> s.to_json
=> "{\"data\":{\"elements\":[1,2,3]},\"json_class\":\"Set\"}"
Until I put the Set into a Hash or something:
>> a = { 'set' => s }
>> a.to_json
=> "{\"set\":[1,2,3]}"
Any idea why my custom to_json doesn't get called when the Set is nested inside another object?
The first chunk is for Rails 3.1 (older versions will be pretty much the same); the second chunk is for the standard non-Rails JSON. Skip to the end if tl;dr.
Your problem is that Rails does this:
[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass|
klass.class_eval <<-RUBY, __FILE__, __LINE__
# Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info.
def to_json(options = nil)
ActiveSupport::JSON.encode(self, options)
end
RUBY
end
in active_support/core_ext/object/to_json.rb. In particular, that changes Hash's to_json method into just an ActiveSupport::JSON.encode call.
Then, looking at ActiveSupport::JSON::Encoding::Encoder, we see this:
def encode(value, use_options = true)
check_for_circular_references(value) do
jsonified = use_options ? value.as_json(options_for(value)) : value.as_json
jsonified.encode_json(self)
end
end
So all the Rails JSON encoding goes through as_json. But, you're not defining your own as_json for Set, you're just setting up to_json and getting confused when Rails ignores something that it doesn't use.
If you set up your own Set#as_json:
class Set
def as_json(options = { })
{
"json_class" => self.class.name,
"data" => { "elements" => self.to_a }
}
end
end
then you'll get what you're after in the Rails console and Rails in general:
> require 'set'
> s = Set.new([1,2,3])
> s.to_json
=> "{\"json_class\":\"Set\",\"data\":{\"elements\":[1,2,3]}}"
> h = { :set => s }
> h.to_json
=> "{\"set\":{\"json_class\":\"Set\",\"data\":{\"elements\":[1,2,3]}}}"
Keep in mind that as_json is used to prepare an object for JSON serialization and then to_json produces the actual JSON string. The as_json methods generally return simple serializable data structures, such as Hash and Array, and have direct analogues in JSON; then, once you have something that is structured like JSON, to_json is used to serialize it into a linear JSON string.
When we look at the standard non-Rails JSON library, we see things like this:
def to_json(*a)
as_json.to_json(*a)
end
monkey patched into the basic classes (Symbol, Time, Date, ...). So once again, to_json is generally implemented in terms of as_json. In this environment, we need to include the standard to_json as well as the above as_json for Set:
class Set
def as_json(options = { })
{
"json_class" => self.class.name,
"data" => { "elements" => self.to_a }
}
end
def to_json(*a)
as_json.to_json(*a)
end
def self.json_create(o)
new o["data"]["elements"]
end
end
And we include your json_create class method for the decoder. Once that's all properly set up, we get things like this in irb:
>> s = Set.new([1,2,3])
>> s.as_json
=> {"json_class"=>"Set", "data"=>{"elements"=>[1, 2, 3]}}
>> h = { :set => s }
>> h.to_json
=> "{"set":{"json_class":"Set","data":{"elements":[1,2,3]}}}"
Executive Summary: If you're in Rails, don't worry about doing anything with to_json, as_json is what you want to play with. If you're not in Rails, implement most of your logic in as_json (despite what the documentation says) and add the standard to_json implementation (def to_json(*a);as_json.to_json(*a);end) as well.
Here is my approach to getting to_json method for custom classes which most probably wouldn't contain to_a method (it has been removed from Object class implementation lately)
There is a little magic here using self.included in a module. Here is a very nice article from 2006 about module having both instance and class methods http://blog.jayfields.com/2006/12/ruby-instance-and-class-methods-from.html
The module is designed to be included in any class to provide seamless to_json functionality. It intercepts attr_accessor method rather than uses its own in order to require minimal changes for existing classes.
module JSONable
module ClassMethods
attr_accessor :attributes
def attr_accessor *attrs
self.attributes = Array attrs
super
end
end
def self.included(base)
base.extend(ClassMethods)
end
def as_json options = {}
serialized = Hash.new
self.class.attributes.each do |attribute|
serialized[attribute] = self.public_send attribute
end
serialized
end
def to_json *a
as_json.to_json *a
end
end
class CustomClass
include JSONable
attr_accessor :b, :c
def initialize b: nil, c: nil
self.b, self.c = b, c
end
end
a = CustomClass.new(b: "q", c: 23)
puts JSON.pretty_generate a
{
"b": "q",
"c": 23
}
Looking for a solution on the same problem, i found this bug report on the Rails issue tracker. Besides it was closed, i suppose it still happens on the earlier versions. Hope it could help.
https://github.com/rails/rails/issues/576

Empty hash as default parameter turning into an Array?

A parameter defaulted to an empty hash attrs={} returns an error:
can't convert Array into Hash
(TypeError)
I've tried this on Ruby versions 1.8.6, 1.8.7 and 1.9.1. A hash will be passed to attrs.
class Category < Object
attr_accessor :attributes
attr_accessor :rel_attrs
attr_accessor :cls_str
def initialize (term='',title='', attrs={}, scheme = '', rel=[], cls_str='')
#attributes ={}
#attributes['scheme'] = scheme
#attributes['term'] = term
#attributes['title'] = title
#attributes['related'] = rel
#cls_str = cls_str
if not attrs.empty?
#attributes.update attrs
end
end
end
What am I doing wrong?
Some notes:
You don't have to inherit from Object.
if not can more idiomatically be expressed as unless.
Making the attrs hash the last argument has the advantage that you can leave off the {} around the hash elements when calling Category.new. You can not do this if the hash is the middle, so it would make sense to show us your call to Category.new.
I changed your code accordingly:
class Category
attr_accessor :attributes
attr_accessor :rel_attrs
attr_accessor :cls_str
def initialize (term='',title='', scheme = '', rel=[], cls_str='', attrs={})
#attributes ={}
#attributes['scheme'] = scheme
#attributes['term'] = term
#attributes['title'] = title
#attributes['related'] = rel
#cls_str = cls_str
#attributes.update(attrs) unless attrs.empty?
end
end
Here's how you call it:
>> c = Category.new("term", "title", "scheme", [1,2,3], 'cls_string', :foo => 'bar', :baz => 'qux')
#=> #<Category:0x00000100b7bff0 #attributes={"scheme"=>"scheme", "term"=>"term", "title"=>"title", "related"=>[1, 2, 3], :foo=>"bar", :baz=>"qux"}, cls_str"cls_string"
>> c.attributes
#=> {"scheme"=>"scheme", "term"=>"term", "title"=>"title", "related"=>[1, 2, 3], :foo=>"bar", :baz=>"qux"}
This obviously has the problem that all other optional arguments have to be specified in order to be able to specify attrs. If you don't want this, move attrs back to the middle of the argument list and make sure to include the {} in the call. Or even better, make the whole argument list a hash and merge it with the defaults args. Something like
class Category(opts = {})
# stuff skipped
#options = { :bla => 'foo', :bar => 'baz'}.merge(opts)
# more stuff skipped
end

Resources