Reading Ruby Metaprogramming inside Devise

It's hard to avoid using Devise in a Rails project. That's mostly because the ease of using it is hard to beat.

Devise is incredibly configurable and that often means having code that can be hard to follow. Providing places in a library for configuration options and hooks for adding features is a difficult job.

But metaprogramming in Ruby can be a flexible way to make a library easy to configure.

Knowing metaprogramming helps you understand tools better, and if you don't know metaprogramming, you just might feel lost. Iterating over arrays and building up strings with define_method and other things going on requires a lot of mental effort to follow.

The good news is that it's not as difficult to understand as you might expect when first reading it.

So let's dip our toes into the water to get a better understanding of metaprogramming and Devise and start with one of the first things you'll add to your rails project: devise_for :users. We'll see how it builds the helpers that become available to your controllers and views.

We're going to walk through the file in lib/devise/controllers/url_helpers.rb as of commit 9aa17eec07719a97385dd40fa05c4029983a1cd5 but later commits are probably similar.

When you add that line to your routes.rb file to generate the routes for your :users, Devise begins generating the code for your controllers.

When I want to understand how something works in a library I'll start searching for the definition of a particular method. In this case I searched the source code for "def devise_for" which brought me to lib/devise/rails/routes.rb.

I find the method I want in there and start reading through for anything that refers to "helpers". Unfortunately nothing stands out. But I do see several references to "mapping" and it seems important to this method so maybe I'll find what I need if I follow that.

The first reference to mapping looks like this:

resources.each do |resource|
  mapping = Devise.add_mapping(resource, options)

So we should probably find out what add_mapping does.

My first search fo "def add_mapping" returned nothing. So I looked for just "add_mapping" instead and saw a result listing def self.add_mapping.

Of course, a class method. That's one of the challenges with searching through a Ruby code base. That same method could have been defined as:

class << self
  def add_mapping

Or even as

def Devise.add_mapping

And other possibilities as well. But we found it and the "add_mapping" search would have been good enough to find those other options too.

def self.add_mapping(resource, options)
  mapping = Devise::Mapping.new(resource, options)
  @@mappings[mapping.name] = mapping
  @@default_scope ||= mapping.name
  @@helpers.each { |h| h.define_helpers(mapping) }
  mapping
end

There's a Mapping constant referenced in there which looks interesting, but this line is what I'm after:

@@helpers.each { |h| h.define_helpers(mapping) }

At this point, I'm going to assume that Mapping has some details on it for when we setup routes for :users or whatever else we choose. That's good enough for now and I can search for more detail later if I need it.

So what is @@helpers?

It's defined earlier in the file like this:

mattr_reader :helpers
@@helpers = Set.new
@@helpers << Devise::Controllers::Helpers

That means that h.define_helpers(mapping) is calling define_helpers on Devise::Controllers::Helpers. So let's go look at that method...

def self.define_helpers(mapping) #:nodoc:
  mapping = mapping.name

  class_eval <<-METHODS, __FILE__, __LINE__ + 1
    def authenticate_#\{mapping\}!(opts=\{\})
      opts[:scope] = :#\{mapping\}
      warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
    end
    def #\{mapping\}_signed_in?
      !!current_\{mapping\}#
    end
    def current_\{mapping\}#
      @current_\{mapping\}# ||= warden.authenticate(scope: :#\{mapping\})
    end
    def #_session
      current_#\{mapping\} && warden.session(:#\{mapping\})
    end
  METHODS

  ActiveSupport.on_load(:action_controller) do
    if respond_to?(:helper_method)
      helper_method "current_#\{mapping\}", "#\{mapping\}_signed_in?", "#\{mapping\}_session"
    end
  end
end

The first part of this is simple. define_helpers receives a mapping object and the local variable mapping is assigned to the mapping.name. A bit confusing, but simple: mapping = mapping.name

Then it gets into the metaprogramming.

If you're new to metaprogramming, the next line looks weird:

class_eval <<-METHODS, __FILE__, __LINE__ + 1

All of that can be easily explained but when I am trying to figure out what's going on, I look for things I recognize and go from that. I prefer to skip over what I don't understand in favor of starting with something more comfortable.

The bits I recognize are the lines defining methods:

def authenticate_#\{mapping\}!(opts=\{\})

def #\{mapping\}_signed_in?

def current_#\{mapping\}

def #\{mapping\}_session

I'm familiar with string interpolation and this looks exactly like it. Every instance of #\{mapping\} will be replaced with the string representation of :users which we specified in our routes_for.

When the rails app boots up, the methods I need will be created for me. I put routes_for :users in my routes.rb file and I'll get:

def authenticate_user!(opts={})

def user_signed_in?

def current_user

def user_session

Stepping back up we'll see the class_eval call which evaluates a block or string in the context of the current class.

The content between <<-METHODS and METHODS is seen as as a string by Ruby. We call this a "heredoc". So the heredoc builds up what the methods should look like if you typed them out yourself, and then is given to class_eval which will evaluate the string and turn it in to real live Ruby methods.

A heredoc marks the start and end of a string. But as you see in this example, the code immediately after the start of the heredoc isn't really a part of it. class_eval receives the heredoc start, and then a reference to the file being evaluated, and the line number where the evaluation starts.

So __FILE__ provides the file name, and __LINE__ provides the current line. So what's with that +1 in there? Well if there's ever an error in your code this helps it report the correct line.

If, for example, an error is raised it the warden.authenticate!(opts) code within the def authenticate_#\{mapping\}) definition, you'll want to see the correct line. That code (as of the commit referenced above) is on line 118. The class_eval line is on line 115 but the code in the heredoc doesn't really begin until the next line, line 116.

So __LINE__ will return 115 and the +1 just bumps it to 116. An error in the warden.authenticate! line will properly report line 118. If we didn't add 1 to the starting line given to class_eval, we'd get an error reporting on line 117. That would be confusing.

To clarify this, try playing with it yourself. Create a file called class_eval_error.rb and paste this into it:

class Tester
  class_eval <<-METHODS, __FILE__, __LINE__+1
    def problem
      raise "oopsie!"
    end
  METHODS
end
Tester.new.problem

Then run ruby class_eval_error.rb in your terminal from the directory where you created the file. If your like me you'll see something like:

class_eval_error.rb:4:in `problem': oopsie! (RuntimeError)`

If you remove that +1 you'll see the error reported on line 3 instead of 4.

Let's jump back to Devise.

After these methods are creating using class_eval there's a block of code that tells your rails application controller to make some of the generated methods available to your views:

ActiveSupport.on_load(:action_controller) do
  if respond_to?(:helper_method)
    helper_method "current_#\{mapping\}", "#\{mapping\}_signed_in?", "#\{mapping\}_session"
  end
end

The helper_method method will receive (after string interpolation) three arguments of "current_user","user_signed_in?", and "user_session".

So that's how the methods are created for your controllers and become available in your views.

If you've read the Ruby DSL Handbook you may be familiar with some of this. But I'm working on building out a deep dive with the Ruby Metaprogramming MasterClass at master-class.saturnflyer.com.

To get more Ruby tips and find out when the MasterClass will be complete, hop on my mailing list at www.saturnflyer.com/subscribe

Stay tuned for more on that in the future. Until then, don't be afraid to dive into the source code to get familiar with how your dependencies work.