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

4 Answers 4

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.

Sign up to request clarification or add additional context in comments.

Comments

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

Comments

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

Comments

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

2 Comments

Access modifiers have no effect on class methods, only on methods of the singleton class, i.e. when inside a class << self ... end scope.
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..

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.