Duck typing magic: How to figure out what method(s) are being called?

Background: I am a rails beginner who is trying to establish a development process/environment that helps me, among other things, figure out how to deal with the duck typing approach in ruby and rails, notably what methods are being called when I am writing rails code.

FWIW I am already aware of some tools like pry and tricks like show_source MyModel.my_custom_method or @MyModel.method(:my_custom_method).source_location, and I have also come across the search at https://edgeapi.rubyonrails.org/, which I find more effective than https://api.rubyonrails.org/. I am using IntelliJ IDEA with the Ruby plugin as my IDE (think: it’s like RubyMine), which gives me a rather comfortable out-of-the-box setup that allows me to e.g. “go to definition” for rails code most of the time.

However, there are still situations where I am at a loss and can’t find my way around the code. Here’s an example code snippet straight from the rails tutorial. I am talking about @article.save:

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render :new, status: :unprocessable_entity
    end
  end

The rails tutorial tells me that @article.save will attempt to save the record. Hence I expected that it would call sth like ActiveRecord::Persistence’s save() method. But that’s not what’s happening. Using the “tooling I am aware of” mentioned above, I tried to figure out what @article.save actually does by myself. I found out that @article.save is calling ActiveRecord::Suppressor’s save() method (source). That code doesn’t do anything like saving or persisting a record! The sources in suppressor.rb don’t provide further clues to me that, eventually, ActiveRecord::Persistence would be involved, and my (limited) interactive debugging skills in ruby/rails haven’t helped me either.

What am I missing here? What are the motions that an experienced rails developer would go through to understand what’s going on in the call chain of a gem or other people’s rails code?

Any pointers would be much appreciated!

I believe you’re actually on the right track. ActiveRecord::Base includes the Suppressor module, who’s #save method is defined as such:

def save(**) # :nodoc:
  Suppressor.registry[self.class.name] ? true : super
end

The Suppressor module is in the ancestor chain of ActiveRecord::Base so it’s #save is invoked. That method calls super which sends it down the chain of ancestors until another module that implements #save (like Persistence) handles it.

This thread seems to explain a bit how the mechanics of it work.

1 Like

Thanks, @McSquare. Given your info, it seems that step debugging is the best bet for the example of the save() method. I find it difficult to figure out the chain of ancestors just by investigating the source code—IntelliJ IDEA is confused, too, as it does bring me to supressor.rb and its save() method (as you quoted above), but it doesn’t know where to go from here as the call to super is ambiguous. The same ambiguity is my problem. The resolution and order of the method calls is probably only really deducible when the code is executing.

def save(**) # :nodoc:
  Suppressor.registry[self.class.name] ? true : super
end

Your right, most IDEs are not going to make heads or tails of Rails level of meta programming internally.

In most cases though it’s easy to follow once you know the structural layout and understanding that the super calls in the include chain are basically like a middle ware stack passing up and back down a call chain.

For example with AdtiveRecord start with AR::Base

You’ll notice it has no methods defined, even though that is what inherit from with every model. When you call AR methods, like save, the last module in that list to define a save method will be the first receiver. Persistence is near the top, and you’ll notice that its methods never call “super”, because it is where the chain stops. The other modules get a chance to modify the call before Persistence gets it, and and on the return path on the way back out.

A similar pattern exists with ActionController::Base, though there’s nuanced differences in the middleware stack and other features. But similar idea, a big base that has a call chain of methods that operate quite similar to a middleware stack.

3 Likes

This is very useful and helps to navigate the code / call chains. Thanks a lot, @chris.covington !

1 Like