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