1

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
NegaMorgan
  • 107
  • 1
  • 6

4 Answers4

3

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.

Holger Just
  • 52,918
  • 14
  • 115
  • 123
2

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
Stefan
  • 109,145
  • 14
  • 143
  • 218
1

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
0

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
Community
  • 1
  • 1
Saurabh
  • 71,488
  • 40
  • 181
  • 244
  • Access modifiers have no effect on class methods, only on methods of the singleton class, i.e. when inside a `class << self ... end` scope. – Holger Just Apr 29 '14 at 14:42
  • Also, the private/public scope wouldn't even be an issue as the call happens on the same class and would be possible even for a public method if properly defined.. – Holger Just Apr 29 '14 at 14:43