Multiple arguments with block in FactoryGirl - ruby-on-rails

I have a method in spec\factories\campaigns.rb:
def campaign_trait(name, *callback_attrs, &block)
trait name do
association :campaign_type, factory: [:campaign_type, name]
after(:build) do |campaign, evaluator|
eval_str = ""
callback_attrs.each do |arg|
arg = [arg] unless arg.is_a? Array
method_name = arg.shift
method_args = arg
method_name = "add_#{method_name}" unless respond_to? method_name
eval_str << method_name.to_s
eval_str << "(campaign"
eval_str << ", evaluator" if method_name == "add_campaign_scopes"
if method_args.any?
method_args.map! { |i| i.is_a?(Symbol) ? ":#{i}" : i }
eval_str << ", " << method_args.map(&:to_s).join(', ')
end
eval_str << ")\n"
end
eval eval_str
end
yield(block) if block_given?
end
end
I call it here:
FactoryGirl.define do
campaign_trait :basket, :campaign_scopes, [:banner, :basket] do
initialize_with { Campaigns::Basket.new(attributes, without_protection: true) }
emitent_article 'emitent'
emitent_name 'Emitent'
end
end
The problem that I face is that in the method campaign_trait I get callback_attrs that equals [:campaign_scopes, []] instead of expected [:campaign_scopes, [:banner, :basket]].
If I call campaign_trait without the block, everything is OK and I get [:campaign_scopes, [:banner, :basket]] as expected.
Could you please help me?

The problem was that I was calling campaign_trait number of times through many tests and somehow (I don't know why), callback_attrs are shared between tests. And method_name = arg.shift breaks my code modifying callback_attrs.
Thanks, BroiSatse! Deep debugging helped me.

Related

Conditionally choose method

I have this:
def some_method()
if auto_pagination
customers_list.auto_paging_each do |customer|
customer_hash = importer.extract(customer)
arr << customer_hash
end
else
customers_list.each do |customer|
customer_hash = importer.extract(customer)
arr << customer_hash
end
end
end
I would like to refactor it in a shorter version. I though about send method but with the block I turns out a bit tricky. Any ideas?
I though about send method but with the block I turns out a bit tricky
I'm not sure what you mean by "tricky", but:
def some_method()
method_name = auto_pagination ? :auto_paging_each : :each
customers_list.send(method_name) do |customer|
customer_hash = importer.extract(customer)
arr << customer_hash
end
end

How do I create accessors for ActiveRecord models from an array of symbols? [duplicate]

When I run code below it raise error:
implicit argument passing of super from method defined by define_method() is not supported. Specify all arguments explicitly. (RuntimeError).
I am not sure what is the problem.
class Result
def total(*scores)
percentage_calculation(*scores)
end
private
def percentage_calculation(*scores)
puts "Calculation for #{scores.inspect}"
scores.inject {|sum, n| sum + n } * (100.0/80.0)
end
end
def mem_result(obj, method)
anon = class << obj; self; end
anon.class_eval do
mem ||= {}
define_method(method) do |*args|
if mem.has_key?(args)
mem[args]
else
mem[args] = super
end
end
end
end
r = Result.new
mem_result(r, :total)
puts r.total(5,10,10,10,10,10,10,10)
puts r.total(5,10,10,10,10,10,10,10)
puts r.total(10,10,10,10,10,10,10,10)
puts r.total(10,10,10,10,10,10,10,10)
The error message is quite descriptive. You need to explicitly pass arguments to super when you call it inside of define_method block:
mem[args] = super(*args)

Test-first-ruby 13_xml_document

I am working on test-first-ruby-master (you can find it at https://github.com/appacademy/test-first-ruby).
The 13_xml_document_spec.rb is the Rspec test that my code must pass. This test has several tasks, but it is the last one (called "indents") that my code doesn't accomplish.
Here is the Rspec test:
require "13_xml_document"
describe XmlDocument do
before do
#xml = XmlDocument.new
end
it "renders an empty tag" do
expect(#xml.hello).to eq("<hello/>")
end
it "renders a tag with attributes" do
expect(#xml.hello(:name => "dolly")).to eq('<hello name="dolly"/>')
end
it "renders a randomly named tag" do
tag_name = (1..8).map{|i| ("a".."z").to_a[rand(26)]}.join
expect(#xml.send(tag_name)).to eq("<#{tag_name}/>")
end
it "renders block with text inside" do
expect(#xml.hello { "dolly" }).to eq("<hello>dolly</hello>")
end
it "nests one level" do
expect(#xml.hello { #xml.goodbye }).to eq("<hello><goodbye/></hello>")
end
it "nests several levels" do
xml = XmlDocument.new
xml_string = xml.hello do
xml.goodbye do
xml.come_back do
xml.ok_fine(:be => "that_way")
end
end
end
expect(xml_string).to eq('<hello><goodbye><come_back><ok_fine
be="that_way"/></come_back></goodbye></hello>')
end
it "indents" do
#xml = XmlDocument.new(true)
xml_string = #xml.hello do
#xml.goodbye do
#xml.come_back do
#xml.ok_fine(:be => "that_way")
end
end
end
expect(xml_string).to eq(
"<hello>\n" +
" <goodbye>\n" +
" <come_back>\n" +
" <ok_fine be=\"that_way\"/>\n" +
" </come_back>\n" +
" </goodbye>\n" +
"</hello>\n"
)
end
end
And here is my code:
class XmlDocument
def initialize(indentation = false)
#indentation = indentation
#counter = 0
end
def method_missing(method, *args, &block)
hash = {}
if block
if #indentation == false
"<#{method}>#{yield}</#{method}>"
elsif #indentation == true
string = ""
string << indent1
string << "<#{method}>\n"
(###)
add_indent
string << indent1
string << yield + "\n"
sub_indent
string << indent2
string << "</#{method}\>"
string
end
elsif args[0].is_a?(Hash)
args[0].map { |key,value| "<#{method.to_s} #{key.to_s}=\"#{value.to_s}\"/>" }.join(" ")
elsif hash.empty?
"<#{method.to_s}/>"
end
end
def indent1
" " * #counter
end
def indent2
" " * #counter
end
def add_indent
#counter += 1
end
def sub_indent
#counter -= 1
end
end
This is the output I get for the "indents" part:
<hello>
<goodbye>
<come_back>
+ <ok_fine be="that_way"/>
</come_back>
</goodbye>
</hello>
Contrary to the right answer, the 4th line ('ok_fine be="that_way"/') seems be two indents closer to the left than it is supposed to be. As opposed to the rest of the lines, the 4th line is not a block, but an argument of the called method 'come_back'.
I cannot see where my mistake is. Even writing an exception in the code (where the (###) is in my code) doesn't seem to have any effect on the 4th line.
Here is the exception (###):
if args[0].is_a?(Hash)
add_indent
string << indent
arg[0].map{|key, value| string << "<#{method.to_s} #{key.to_s}=\"#{value.to_s}\"/>"}
end
NOTE: I assume that if I manage to give the 4th line the right numbers of indents, that also will increase the number of indents of the lines after it, so the method 'indent2' will need to be modified.
I figured out what the problem was. As I said in my question, in the Rspec test they have the following input:
xml_string = xml.hello do
xml.goodbye do
xml.come_back do
xml.ok_fine(:be => "that_way")
end
end
end
where the 4th line (xml.ok_fine(:be => "that_way")) doesn't have a block nested, but an argument. In my code I established a condition (if block) for when there is a block present and inside this first condition, a second condition (if #indentation == true) for when #indentation is true:
if block
if #indentation == false
"<#{method}>#{yield}</#{method}>"
elsif #indentation == true
...
It is inside this second condition that I create the variable 'string' where I shovel in the different parts:
elsif #indentation == true
string = ""
string << indent1
string << "<#{method}>\n"
(###)
add_indent
string << indent1
string << yield + "\n"
sub_indent
string << indent2
string << "</#{method}\>"
string
end
But because the 4th line doesn't carry a block, the first condition (if block) doesn't return true for it and therefore this 4th line is skipped.
I've re-written my code so now it passes the Rspec test:
class XmlDocument
def initialize(indentation = false)
#indentation = indentation
#counter = 0
end
def method_missing(method, args = nil, &block)
string = ""
arguments = args
if #indentation == false
if (arguments == nil) && (block == nil)
"<#{method.to_s}/>"
elsif arguments.is_a?(Hash)
arguments.map { |key,value| "<#{method.to_s} #{key.to_s}=\"#{value.to_s}\"/>" }.join(" ")
elsif block
"<#{method}>#{yield}</#{method}>"
end
elsif #indentation == true
if (block) || (arguments.is_a?(Hash))
string << indent1
string << "<#{method}>\n" unless !block
add_indent
string << indent1 unless !block
if block
string << yield + "\n"
elsif arguments.is_a?(Hash)
arguments.map { |key,value| string << "<#{method.to_s} #{key.to_s}=\"#{value.to_s}\"/>" }
end
sub_indent
string << indent2 unless !block
string << "</#{method}\>" unless !block
if indent2 == ""
string << "\n"
end
end
string
end
end
def indent1
" " * #counter
end
def indent2
" " * #counter
end
def add_indent
#counter += 1
end
def sub_indent
#counter -= 1
end
end
In contrast to the code I wrote in my question, in this one, the two main conditions are #indentation == false and #indentation == true and inside these two conditions I establish different exceptions for the different cases (block or no block, argument or no argument...). Specifically for elsif #indentation == true I created a condition that accepts the 4th line: if (block) || (arguments.is_a?(Hash)), or in other words, it accepts methods that have a block or an argument (especifically a a hash).
Now, I shovel in the different parts in 'string', and when I reach a block to yield there is a bifurcation:
if block
string << yield + "\n"
elsif arguments.is_a?(Hash)
arguments.map { |key,value| string << "<#{method.to_s} #{key.to_s}=\"#{value.to_s}\"/>" }
if there is a block I "yield" it, and if there is and argument that is a hash I shovel it into 'string'.
Also, there is this exception unless !block either when I indent or I shovel a method because otherwise it can introduce unwanted indents and '\n' if there is a method that doesn't have a block (as line 4th).
Finally, I had to add at the end
if indent2 == ""
string << "\n"
end
because the solution requires a '\n' at the end.
I hope this answer can help other
NOTE: I wrote a 'NOTE' in my question where I assumed I would have to modify 'indent2'. That, obviously I didn't have to do because the output I was getting did not considered the 4th line (because it doesn't have a block), so the bigger indentation (" ") of 'indent2' is all right.

"method missing" error on Rails/Ruby metaprogramming attempt

I'm trying my first foray into metaprogramming and it's not going very well! It's a Rails 4.1 application and I'm trying to refactor an active record model (User) to combine two methods that are very similar. The original methods are slightly complex DB calls and work as expected.
The original code:
def retweet_count(league)
celebrity_ids = Roster.
where("user_id = ? and league_id = ?", self.id, league.id).
select(:celebrity_id).map { |r| r.celebrity_id }
Tweet.where({
tweet_date: league.start_date..league.end_date,
celebrity_id: celebrity_ids
}).select(:retweet_count).inject(0) do |sum, n|
sum + ( n.retweet_count || 0 )
end
end
def favorite_count(league)
celebrity_ids = Roster.
where("user_id = ? and league_id = ?", self.id, league.id).
select(:celebrity_id).map { |r| r.celebrity_id }
Tweet.where({
tweet_date: league.start_date..league.end_date,
celebrity_id: celebrity_ids
}).select(:favorite_count).inject(0) do |sum, n|
sum + ( n.favorite_count || 0 )
end
end
The new code:
twitter_stats_count :retweet, :favorite
private
def twitter_stats_count(*stats)
stats.each do |statistic|
stat = send(statistic).to_s
define_method "#{stat}_count" do |league|
celebrity_ids = Roster.
where("user_id = ? and league_id = ?", self.id, league.id).
select(:celebrity_id).map { |r| r.celebrity_id }
Tweet.where({
tweet_date: league.start_date..league.end_date,
celebrity_id: celebrity_ids
}).select("#{stat}_count").inject(0) do |sum, n|
sum + ( n.send("#{stat}_count") || 0 )
end
end
end
end
The error the new code produces when I try to start my rails server:
/Users/kiddo/.rvm/gems/ruby-2.1.0/gems/activerecord-4.1.0.rc2/lib/active_record/dynamic_matchers.rb:26:in `method_missing': undefined method `twitter_stats_count' for User (call 'User.connection' to establish a connection):Class (NoMethodError)
I can't seem to figure out what I'm doing wrong, so any pointers would be much appreciated!
FYI, here's the final code I got working. I mainly went with Holger Just's suggestions, but incorporated aspects from several others, so upvotes all around!
def team_ids(league)
Roster.where(user_id: self.id, league_id: league.id).pluck(:celebrity_id)
end
def self.twitter_stats_count(*stats)
stats.each do |statistic|
stat = statistic.to_s
define_method "#{stat}_count" do |league|
Tweet.where({
tweet_date: league.start_date..league.end_date,
celebrity_id: self.team_ids(league)
}).sum("#{stat}_count")
end
end
end
twitter_stats_count :retweet, :favorite
There are a couple of issues with your approach:
You call the twitter_stats_count directly on the class, not an instance of the class. As such, the method needs to be a class method. You can define it as a class method with
def self.twitter_stats_count(*stats)
# ...
end
Additionally, you call the method before having it defined. In Ruby, everything (even method definitions) are executed. As such, you can only call methods after they have been defined. Thus, you need to put the call to your twitter_stats_count method after its definition.
That looks quite complicated. If I'm not mistaken, you can reduce the duplication by refactoring your code:
def retweet_count(league)
league_tweets(league).sum(:retweet_count)
end
def favorite_count(league)
league_tweets(league).sum(:favorite_count)
end
def celebrity_ids(league)
Roster.where(user_id: self.id, league_id: league.id).pluck(:celebrity_id)
end
def league_tweets(league)
Tweet.where(
tweet_date: league.start_date..league.end_date,
celebrity_id: celebrity_ids(league)
)
end
twitter_stats_count should be a class method, but what you did is make it a instance method, maybe you can try this:
# no private here
def self.twitter_stats_count(*status)
#your codes here
end
You are getting this error because, you have define twitter_stats_count as a private method, You can't call this on self. You have to put it in a instance method, than call it.
Check this.
For example following gives same error:
class Foo
baz
private
def baz
puts "baz called"
end
end
However this will work:
class Foo
def dummy
baz
end
private
def baz
puts "baz called"
end
end
foo = Foo.new
foo.dummy

Why I can not call super in define_method with overloading method?

When I run code below it raise error:
implicit argument passing of super from method defined by define_method() is not supported. Specify all arguments explicitly. (RuntimeError).
I am not sure what is the problem.
class Result
def total(*scores)
percentage_calculation(*scores)
end
private
def percentage_calculation(*scores)
puts "Calculation for #{scores.inspect}"
scores.inject {|sum, n| sum + n } * (100.0/80.0)
end
end
def mem_result(obj, method)
anon = class << obj; self; end
anon.class_eval do
mem ||= {}
define_method(method) do |*args|
if mem.has_key?(args)
mem[args]
else
mem[args] = super
end
end
end
end
r = Result.new
mem_result(r, :total)
puts r.total(5,10,10,10,10,10,10,10)
puts r.total(5,10,10,10,10,10,10,10)
puts r.total(10,10,10,10,10,10,10,10)
puts r.total(10,10,10,10,10,10,10,10)
The error message is quite descriptive. You need to explicitly pass arguments to super when you call it inside of define_method block:
mem[args] = super(*args)

Resources