Ruby metaprogramming to achieve dynamic methods? - ruby-on-rails

Want to achieve the following code using metaprogramming.
#resource = {}
#voters = {}
#is_upvoted = {}
def resource(comment)
#resource[comment.id]
end
def voters(comment)
#voters[comment.id]
end
def is_upvoted(comment)
#is_upvoted[comment.id]
end
How can I create these methods using ruby metaprogramming and access the hash?
Can you tell me what is wrong in my code ?
['resource', 'voters', 'is_upvoted'].each do |attribute|
define_method("#{attribute}") do |comment|
instance_variable_set("##{attribute}", comment.id)
end
end

This bit seems redundant:
#resource = {}
#voters = {}
#is_upvoted = {}
Since you're already looping an array to do your metaprogramming.
You might try something like:
class Foo
%w(
resource
voters
is_upvoted
).each do |attr_sym|
define_method attr_sym do |comment|
instance_variable_set("##{attr_sym}", {}) unless instance_variable_get("##{attr_sym}")
instance_variable_get("##{attr_sym}")[comment.id]
end
end
end
Which I believe will give you methods roughly like:
class Foo
def resource(comment)
#resource ||= {}
#resource[comment.id]
end
end
Personally, it seems not great to me to have comment.id in your method. Because what if someday you want to use a different attribute (or something else altogether) as the key?
So, I think I would do:
class Foo
%w(
resource
voters
is_upvoted
).each do |attr_sym|
define_method attr_sym do |key|
instance_variable_set("##{attr_sym}", {}) unless instance_variable_get("##{attr_sym}")
instance_variable_get("##{attr_sym}")[key]
end
end
end
Now, it seems like you're going to want an easy way to set key-value pairs on your instance variable, so I guess I would try something like:
class Foo
%w(
resource
voters
is_upvoted
).each do |attr_sym|
define_method attr_sym do |key=nil|
instance_variable_set("##{attr_sym}", {}) unless instance_variable_get("##{attr_sym}")
hsh = instance_variable_get("##{attr_sym}")
return hsh[key] if key
hsh
end
end
end
In which case you should be able to do (assuming you have a #comment variable that responds to id):
#comment.id
=> 1
foo = Foo.new
=> #<Foo:0x000056536d7504b0>
foo.resource
=> {}
foo.resource[#comment.id] = :bar
=> :bar
foo.resource
=> {1=>:bar}
foo.resource[#comment.id]
=> :bar

Can you tell me what is wrong in my code ?
It's doing the equivalent of this:
def resource(comment)
#resource = comment.id
end
instance_variable_get would be a better choice.

This is how I used it and it works
['resource', 'voters', 'is_upvoted'].each do |attribute|
define_method("#{attribute}") do |comment|
instance_variable_get("##{attribute}")[comment.id]
end
end

Related

How to refactor this Rails code?

Consider this helper method:
module SomeHelper
def display_button
Foo.find_by_id params[:id] and Foo.find(params[:id]).organizer.name != current_name and Foo.find(params[:id]).friends.find_by_name current_name
end
end
How to refactor into something more readable?
Rails 3.2.2
Something like this?
module SomeHelper
def display_button?
if foo = Foo.find(params[:id])
foo.organizer.name != current_name if foo.friends.find_by_name(current_name)
end
end
end
Note: if the helper method is returning a boolean, I would append the name with a ? ... ruby convention.
You can factorize the call to Foo.find(params[:id]) and use exists? for the third condition
module SomeHelper
def display_button
foo = foo.find_by_id params[:id]
foo and foo.organizer.name != current_name and foo.friends.where(:name => current_name).exists?
end
end
You can also create several methods to gain on reusability (and will save trouble if you model changes):
module SomeHelper
def display_button
foo = foo.find_by_id params[:id]
foo && !is_organizer?(foo, current_name) && has_friend?(foo, current_name)
end
def is_organizer?(foo, name)
foo.organizer.name == name
end
def has_friend?(foo, name)
foo.friends.where(:name => name).exists?
end
end
try invokes the passed block on non-nil objects. Returns nil otherwise. So the return will be nil,true,false depending on your data.
def display_button
Foo.find_by_id(params[:id]).try do |foo|
foo.organizer.name != current_name &&
foo.friends.find_by_name current_name
end
end

How do I make module methods available to a constant Proc variable? (Ruby)

This is in my application helper:
def call_me
"blah"
end
P = Proc.new { call_me }
def test_me
P.call
end
If I then do this in my view:
<%= test_me %>
I get an error saying that call_me is an undefined method.
How do I get the Proc to be able to call call_me? require doesn't do it. Prefixing with ApplicationHelper::call_me doesn't either. :(
This works, but I really don't like it since test_me will be called lots of times and in reality there are many many more Procs:
def test_me
p = Proc.new { call_me }
p.call
end
It should work as is in Ruby 1.9.2, but in earlier versions you can pass your helper as an argument to the Proc:
P = Proc.new { |helper| helper.call_me }
def test_me
P.call self
end
Since call_me is an instance method on your helper, you need to invoke it with an instance.
I think you were looking for this?
def call_me
Proc.new
end
proc = call_me { "blah" }
proc.call #=> "blah"
you have to pass call_me in. gets kind of convoluted...
p = Proc.new { |m| m.call }
def test_me
p.call(call_me)
end
I'm not entirely sure what your aim here is in the larger sense so this is more or less a stab in the dark...
The most pragmatic way to do this is to make the module methods static, and then simply using them wherever you want:
module Sample::SillyModule
def self.say_hello(my_cool_var)
puts "Hello #{my_cool_var}!"
end
end
proc do
Sample::SillyModule.say_hello("stackoverflow")
end

Ruby method with argument hash with default values: how to DRY?

I have an initialize method that accepts a hash to set some instance variables. This is basically what I'm trying to do:
class Ad
DEFAULT_PAGE = 'index'.freeze
DEFAULT_CHANNEL = 'general'.freeze
DEFAULT_AREA = 'general'.freeze
attr_accessor :page, :area, :channel
def initialize args={}
#page = args[:page] || DEFAULT_PAGE
#area = args[:area] || DEFAULT_AREA
#channel = args[:channel] || DEFAULT_CHANNEL
end
# ...
end
I saw a tip to allow dynamic setting of instance variables, but I'm not sure how to also include the default values...
def initialize args={}
args.each do |attr,val|
instance_variable_set("##{attr}", val) unless val.nil?
end
end
Can I refer to a constant dynamically? Or any better ways of doing this sort of thing are welcome!
... I also realize that attr_accessor variables can be set individually. But I just want to do it like this. :)
This one also only creates the instance variables if they are in your defaults hash, so that you don't accidentally create/overwrite other instance variables.
I'm assuming you meant to say unless val.nil?:
def initialize(args={})
defaults = {
:page => DEFAULT_PAGE,
:area => DEFAULT_AREA,
:channel => DEFAULT_CHANNEL
}.merge(args).each do |attr, val|
instance_variable_set("##{attr}", val) if defaults.has_key?(attr) && (not val.nil?)
end if args
end
Try this:
def initialize args={}
defaults = {
:page => DEFAULT_AREA,
:area => DEFAULT_AREA,
:channel => DEFAULT_CHANNEL
}
args.reverse_merge(defaults).each do |attr,val|
instance_variable_set("##{attr}", val) unless val.nil?
end
end

to_xml with extra method that returns a hash in Rails/ActiveRecord

I have a class something like this:
class Product < ActiveRecord::Base
# .... some stuff
def prices
# Make hash like { "Regular" => 10, "Discount" => 8 }
end
end
I grab this from the database and try to_xml on it:
Product.find(id).to_xml(:methods => [:prices])
But if fails at the prices hash
... some XML
<prices>Regular10Discount8</prices>
... some more XML
to_json works as expected.
What's the easiest way to alter the format so it ends up as something like this:
<prices>
<price name="Regular">10</price>
<price name="Discount">8</price>
</prices>
I think You're left with doing the to_xml formatting Yourself :
class Product < ActiveRecord::Base
def prices
...
end
def to_xml(options = {})
super(options) do |xml|
if prices.empty?
xml.tag! 'prices' # empty tag
else
xml.prices do
prices.each do |name, val|
xml.price val, 'name' => name
end
end
end
yield(xml) if block_given?
end
end
end
than just to a Product.find(id).to_xml

refactor a method with a block that contains multiple blocks itself

I'm using Ruby 1.9.2
I have a class method called search that takes a block
e.g.
class MyClass
def self.search do
if criteria1
keywords "abcde", fields: :c1 do
minimum_match(1)
end
end
if criteria2
keywords "defghi", fields: :c2 do
minimum_match(1)
end
end
end
end
What I'd like to do is refactor the MyClass.search method and have a simple one-line method for each if/end statement
e.g. it would look something like this:
class MyClass
def self.search do
c1_method
c2_method
end
def self.c1_method
if criteria1
return keywords "abcde", fields: :c1 do
minimum_match(1)
end
end
end
def self.c2_method
if criteria2
return keywords "defghi", fields: :c2 do
minimum_match(1)
end
end
end
end
But the refactoring that I show above doesn't quite work. It looks like the "blocks" that I'm returning in c1_method and c2_method aren't really being returned and evaluated in the search method, but I'm not sure how to do that.
Well, you can use the method(sym) call in order to get at the body of a method.
>> def foo(bar); bar * 2; end
=> nil
>> def baz(bleep); method(:foo).call(bleep); end
=> nil
>> baz(6)
=> 12

Resources