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
Related
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
Instead of defining a scope in a class like this:
scope :first_user, -> { first }
And calling it like this: User.first_user
I would like to define a block in another class, that can be called on the user class and works like a Scope:
This code is not working but it should signalize what behaviour I want to achieve:
class Manage
def get_first_user
User.&first_added
end
def first_added
Proc.new { first }
end
end
When I run this code:
a = Manage.new
a.get_first_user
it says me, & undefined method for User. How can I execute the Block defined in first_added on the User model?
How can I in general call a block on a class? Thanks
If I understand your question correctly, you can use class_eval:
>> foo = Proc.new { count }
=> #<Proc:0x007f1aa7cacfd8#(pry):30>
>> Buyer.class_eval(&foo)
(30.6ms) SELECT COUNT(*) FROM "buyers"
=> 1234
Or with your example:
class Manage
def get_first_user
User.class_eval(&first_added)
end
def first_added
Proc.new { first }
end
end
It is not what you want but maybe this will be helpful.
I am not sure if one can call proc on something. I think one can only call proc with something, i.t. in your case passing User as parameter.
def get_first_user
wrap User, &first_added
end
def first_added
Proc.new { |model| model.where(...) }
end
private
def wrap(model, &block)
yield model
end
Here's three ways to call first on User from Manager using:
Object#send:
http://ruby-doc.org/core-2.3.1/Object.html#method-i-send
Module#class_eval:
http://ruby-doc.org/core-2.3.1/Module.html#method-i-class_eval
File manage.rb:
class User
def self.first
puts 'record would probably go here'
end
def self.yielder
print "via yielder => "
self.send(yield) if block_given?
end
def self.blocker(&block)
print "via blocker => "
self.send(block.call) if block_given?
end
def self.evaller(&block)
print "via evaller => "
self.class_eval(block.call) if block_given?
end
end
class Manage
def get_first_user
User.yielder(&first_added)
User.blocker(&first_added)
User.evaller(&first_added)
end
def first_added
Proc.new {"first"}
end
end
a = Manage.new
a.get_first_user
Output:
$ ruby manage.rb
via yielder => record would probably go here
via blocker => record would probably go here
via evaller => record would probably go here
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
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
In one particular Railcasts episode Ryan talks about advanced search and in that he uses some code so as to find the conditions for the search. As its working isn't explained I wanted some clarification regarding it.
def products
#products ||= find_products
end
private
def find_products
Product.find(:all, :conditions => conditions)
end
def keyword_conditions
["products.name LIKE ?", "%#{keywords}%"] unless keywords.blank?
end
def minimum_price_conditions
["products.price >= ?", minimum_price] unless minimum_price.blank?
end
def maximum_price_conditions
["products.price <= ?", maximum_price] unless maximum_price.blank?
end
def category_conditions
["products.category_id = ?", category_id] unless category_id.blank?
end
def conditions
[conditions_clauses.join(' AND '), *conditions_options]
end
def conditions_clauses
conditions_parts.map { |condition| condition.first }
end
def conditions_options
conditions_parts.map { |condition| condition[1..-1] }.flatten
end
def conditions_parts
private_methods(false).grep(/_conditions$/).map { |m| send(m) }.compact
end
I would welcome any information as to how this works especially the method products as he even calls it as products.name etc.
He defines some methods for the conditions in his search form: keyword_conditions, minimum_price_conditions ans so on. products.name means the field name from the table products.
The method
def conditions_parts
private_methods(false).grep(/_conditions$/).map { |m| send(m) }.compact
end
uses reflection to look at the private methods of this class which have the name that ends with _conditions (The regex /_conditions$/) and joins only those that don't return null (compact)
The method
def conditions
[conditions_clauses.join(' AND '), *conditions_options]
end
adds a AND keyword between the conditions and passes the result to Product.find which makes the SQL query and returns the result set.