Skip to content

Travel To The Core Rails 3 Methods Without Leaving IRB Prompt

In the previous post I introduced Reflexive gem which lets you browse the classes and modules for running application with Web UI. But we’re really often just goofing off at the IRB prompt, aren’t we? For this case I have extracted some portions of Reflexive to help with code navigation right from the interactive prompt.

Let’s see how these may help us look under the hoods of Rails 3 framework. Disclaimer: this posts is intended as the demonstration of navigation techniques that can be used on any code base, not necessary Ruby on Rails, also if you’re looking for detailed explanation of the architecture of particular Rails components Maxim Chernyak compiled Rails 3 Reading Material list is the great place to start.

Side note: it may look like things demonstrated below could be done with debugger and more accurately so. My idea is that debugger basically follows push model so program state is pushed to you and you just follow along. On the other hand navigating manually is more like a pull model which is much more engaging and more effective when you try to understand and remember patterns in foreign code (well at least that’s how it is for me :) )

ActiveRecord find method

Suppose we want to find out what happens when we call ActiveRecord find method. That’s the easy one. First install method_extensions gem, add it to Gemfile and start console

    root@ubuntu:~/ > rvm use ruby-1.9.2-head 
    root@ubuntu:~/ > rvm gemset create rails3 && rvm gemset use rails3 # sandbox our Rails 3 experiments in rails3 Gemset (h/t to Mark Carey for this note)
    root@ubuntu:~/ > gem install rails --pre
    root@ubuntu:~/ > gem install method_extensions
    root@ubuntu:~/ > rails rails3_test_app
    root@ubuntu:~/ > cd rails3_test_app
    root@ubuntu:~/rails3_test_app/ > echo 'gem "method_extensions"' >> Gemfile
    root@ubuntu:~/rails3_test_app/ > bundle install
    root@ubuntu:~/rails3_test_app/ > rails c
    ruby-1.9.2-head >
    

Now ActiveRecord find is a class method so to retrieve corresponding Method object we’re using method method

    ruby-1.9.2-head > ActiveRecord::Base.method(:find)
    => #<Method: ActiveRecord::Base.find> 
    
Let’s try to get method source with source method
    ruby-1.9.2-head > puts ActiveRecord::Base.method(:find).source
    ArgumentError: failed to find method definition around the lines:

      delegate :find, :first, :last, :all, :destroy, :destroy_all, :exists?, :delete, :delete_all, :update, :update_all, :to => :scoped
      delegate :find_each, :find_in_batches, :to => :scoped
    

Not very impressive, but we can see at least that find is delegated method, and it’s delegated to the result of scoped method. That’s how delegate works, but what if we had no idea about delegate method and how it works?

source_with_doc method will also return the comment preceding the method definition

    ruby-1.9.2-head > puts ActiveRecord::Base.method(:delegate).source_with_doc
    # Provides a delegate class method to easily expose contained objects' methods
    # as your own. Pass one or more methods (specified as symbols or strings)
    # ... snip
    def delegate(*methods)
      options = methods.pop
      unless options.is_a?(Hash) && to = options[:to]
        raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, :to => :greeter)."
      end
    # ... snip
    end
    

Now that we know that find is delegated to scoped indeed, what does scoped return?

    ruby-1.9.2-head > puts ActiveRecord::Base.method(:scoped).source_with_doc
    # Returns an anonymous scope.
    # ... snip
    def scoped(options = {}, &block)
      if options.present?
        relation = scoped.apply_finder_options(options)
        block_given? ? relation.extending(Module.new(&block)) : relation
      else
        current_scoped_methods ? unscoped.merge(current_scoped_methods) : unscoped.clone
      end
    end
    

Here we need to stop for a moment and consider what this piece of code may potentially do. We can see that in presence of options hash function recursive call is made without options passed which takes us to the second branch which ultimately calls unscoped. Let’s see what it does.

    ruby-1.9.2-head > puts ActiveRecord::Base.method(:unscoped).source
    def unscoped
      @unscoped ||= Relation.new(self, arel_table)
      finder_needs_type_condition? ? @unscoped.where(type_condition) : @unscoped
    end
    
Plug for Reflexive: if we want to get more context and have Reflexive installed there is the shorthand to generate link like this one (which shows the whole file where the method is defined, can lookup constants and some method calls and is otherwise neat :) )
    ruby-1.9.2-head > ActiveRecord::Base.method(:unscoped).reflexive_url
    => "http://localhost:3000/reflexive/constants/ActiveRecord::Base/class_methods/unscoped" 
    

Rewinding a little back to the scoped definition we see that find is in the end called on an instance of ActiveRecord::Relation. Let’s confirm that using full_inspect method also added by method_extensions gem. It is just the plain inspect, source_location, and source_with_doc put together

    ruby-1.9.2-head > puts ActiveRecord::Relation.instance_method(:find).full_inspect
    #<UnboundMethod: ActiveRecord::Relation(ActiveRecord::FinderMethods)#find>
    ["~/rails3_test_app/vendor/rails/activerecord/lib/active_record/relation/finder_methods.rb", 90]
    # Find operates with four different retrieval approaches:
    # ... snip
    def find(*args, &block)
      return to_a.find(&block) if block_given?
      # ... snip
    end
    
So we just got information on the class hierarchy (plain inspect on the first line says that FinderMethods is mixed-in into Relation), the place where method is defined and method code with documentation. Not that bad I think considering we didn’t have to go to the code directly (either on the web or in editor). Now we can move to something slightly more complicated.

ActionController render method

Let’s get into it

    ruby-1.9.2-head > puts ActionController::Base.instance_method(:render).full_inspect
    #<UnboundMethod: ActionController::Base(ActionController::Instrumentation)#render>
    ["~/rails3_test_app/vendor/rails/actionpack/lib/action_controller/metal/instrumentation.rb", 36]
    def render(*args)
      render_output = nil
      self.view_runtime = cleanup_view_runtime do
        Benchmark.ms { render_output = super }
      end
      render_output
    end
    
D’oh! Not much again, what we get is generic code from ActionController::Instrumentation used to measure view rendering time. In other words – boring stuff and all it looks like the whole job is handled in super. But how we get there?

    ruby-1.9.2-head > require "method_extensions/method/super"
    ruby-1.9.2-head > puts ActionController::Base.instance_method(:render).super.source_with_doc
    # Check for double render errors and set the content_type after rendering.
    def render(*args) #:nodoc:
      raise ::AbstractController::DoubleRenderError if response_body
      super
      self.content_type ||= Mime[formats.first].to_s
      response_body
    end
    
require "method_extensions/method/super" adds super method but since it does so in a hacky/core methods monkey-patching way it’s not loaded by default. super supports chaining we need in this case since we’re not getting the real rendering action yet
    ruby-1.9.2-head > puts ActionController::Base.instance_method(:render).super.super.full_inspect
    # Normalize arguments, options and then delegates render_to_body and
    # sticks the result in self.response_body.
    def render(*args, &block)
      self.response_body = render_to_string(*args, &block)
    end
    
So what we actually need is render_to_string. Now just keep going using the super trick where needed
    ruby-1.9.2-head > puts ActionController::Base.instance_method(:render_to_string).source_with_doc
    # Raw rendering of a template to a string. Just convert the results of
    # render_to_body into a String.
    # :api: plugin
    def render_to_string(*args, &block)
      options = _normalize_args(*args, &block)
      _normalize_options(options)
      render_to_body(options)
    end
    ruby-1.9.2-head > puts ActionController::Base.instance_method(:render_to_body).source_with_doc
    def render_to_body(options)
      options[:template].sub!(/^\//, '') if options.key?(:template)
      super || " "
    end
    ruby-1.9.2-head > puts ActionController::Base.instance_method(:render_to_body).super.source_with_doc
    def render_to_body(options)
      _handle_render_options(options) || super
    end
    ruby-1.9.2-head > puts ActionController::Base.instance_method(:render_to_body).super.super.source_with_doc
    # Raw rendering of a template to a Rack-compatible body.
    # :api: plugin
    def render_to_body(options = {})
      _process_options(options)
      _render_template(options)
    end
    ruby-1.9.2-head > puts ActionController::Base.instance_method(:_render_template).source_with_doc
    # Find and renders a template based on the options given.
    # :api: private
    def _render_template(options) #:nodoc:
      view_context.render(options)
    end
    
All that to find out that render is basically delegated to the view_context. But we’re already comfortable with that thing and can proceed further
    ruby-1.9.2-head > puts ActionController::Base.instance_method(:view_context).source_with_doc
    # An instance of a view class. The default view class is ActionView::Base
    #
    # The view class must have the following methods:
    # View.new[lookup_context, assigns, controller]
    #   Create a new ActionView instance for a controller
    # View#render[options]
    #   Returns String with the rendered template
    #
    # Override this method in a module to change the default behavior.
    def view_context
      view_context_class.new(lookup_context, view_assigns, self)
    end
    ruby-1.9.2-head > puts ActionController::Base.instance_method(:view_context_class).source_with_doc
    def view_context_class
      @view_context_class || self.class.view_context_class
    end
    
And finally we’re there
    ruby-1.9.2-head > puts ActionController::Base.method(:view_context_class).source_with_doc
    def view_context_class
      @view_context_class ||= begin
        controller = self
        Class.new(ActionView::Base) do
          if controller.respond_to?(:_helpers)
            include controller._helpers

            if controller.respond_to?(:_router)
              include controller._router.url_helpers
            end

            # TODO: Fix RJS to not require this
            self.helpers = controller._helpers
          end
        end
      end
    end
    
This method illustrates how helpers declared by controller and routing helpers (methods like post_url, etc. generated from routes) are made available in views. We also get the source and options documentation for render method
    ruby-1.9.2-head > puts ActionView::Base.instance_method(:render).source_with_doc
    # Returns the result of a render that's dictated by the options hash. The primary options are:
    #
    # * <tt>:partial</tt> - See ActionView::Partials.
    # * <tt>:update</tt> - Calls update_page with the block given.
    # * <tt>:file</tt> - Renders an explicit template file (this used to be the old default), add :locals to pass in those.
    # * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller.
    # * <tt>:text</tt> - Renders the text passed in out.
    def render(options = {}, locals = {}, &block)
      case options
      # ... snip
    end
    

While this won’t replace proper docs/debugger in any way it was nice to see how self-reflection capabilities of Ruby can be improved with just a few hacky helper methods. Hopefully the travel wasn’t too boring :) , comments/suggestions/patches are welcome indeed!

Added note: I’ve just started using this for real and it holds up pretty well so I decided to add some shortcuts to my .irbrc and method_extensions which will make the IRB print result of full_inspect verbatim by default. Check this method and comment

  • Because our operations support companies around the world and can operate 24/7/365, scheduling flexibility makes it possible for students pursuing a wide range of majors to accommodate other jobs
  • I love it -- Russian ingenuity at it's finest! I haven't looked at the code, but tell me, does this library depend on Ruby 1.9.2 exclusively?
  • Haha, sadly - yes. One thing it relies on is Method#source_location method, another is Ripper parser and these are not available in 1.8
  • Mark Carey
    One thing to add is the recommendation to create a rails3 gemset so as to not pollute your global gem space until rals3 is released:
    rvm 1.9.2-head
    rvm create rails3
    rvm 1.9.2-head@rails3
    gem install rails --pre

    See here for more details about gemsets
    http://rvm.beginrescueend.com/...
  • Good catch indeed, I do that all the time, I'll update the code snippet accordingly
  • Mark Carey
    should gave been rvm gemset create rails3
blog comments powered by Disqus