Ruby Metaprogramming: Real-World Examples

You may have read about Ruby metaprogramming before.

But…

It can be a bit confusing if you don’t have a few specific examples.

That’s why in this article:

We’re going to look at some popular open-source projects using Ruby metaprogramming.

Projects like:

  • Rails
  • Sinatra
  • Paperclip gem

All of them use some form of metaprogramming.

Let’s go over the code & find out exactly what they are doing!

Rails Example

Rails makes heavy use of metaprogramming, so it’s a good place to start looking.

For example:

When you want to check the current environment in your Rails app, you do something like the following.

Rails.env.production?

But what is env? And how does that work?

The answers are in the StringInquirer class, which is a subclass of String. This class uses method_missing so that you can call env.production? instead of env == production.

This is what the code looks like:

def method_missing(method_name, *arguments)
  if method_name[-1] == '?'
    self == method_name[0..-2]
  else
    super
  end
end

This is saying:

“If the method name ends with a question mark then do the comparison, otherwise keep going up the ancestors chain”.

The original code can be found here.

Sinatra Delegation

If you have used Sinatra before you may know that there are two ways to define your routes:

  • By using the get / post methods directly, outside of any class.
  • By defining a class that inherits from Sinatra::Application.

The Sinatra DSL (Domain-Specific Language) methods are defined inside Sinatra::Application, so how can you use them outside of this class?

Well, there are two things going on here:

  • Metaprogramming
  • Module extension

Sinatra defines a Sinatra::Delegator module inside base.rb, which is used to delegate method calls to a target.

The target is set to Sinatra::Application by default.

This is a simplified version of the delegate class method, defined in Sinatra::Delegator:

def self.delegate(*methods)
  methods.each do |method_name|
   define_method(method_name) do |*args, &block|
     Delegator.target.send(method_name, *args, &block)
   end
  end
end

delegate :get, :patch, :put, :post, :delete

This code is creating a set of methods (with define_method) that will forward the method calls to Sinatra::Application (the default value for Delegator.target).

Then the main object is extended with the methods defined in Sinatra::Delegator, which makes the Sinatra DSL available outside the Sinatra::Application class.

It’s worth noting that Ruby has two built-in classes for method delegation, in case you need them: Delegator & Forwardable.

The Paperclip Gem

Paperclip is a gem which allows your application to handle file uploads. Files are associated with ActiveRecord models.

You can define an attached file on a model using the has_attached_file method.

Like this:

class User
  has_attached_file :avatar, :styles => { :normal => "100x100#" }
end

This method is defined on paperclip.rb and it uses metaprogramming to define a method on the Model.

This method can be used to access the file attachment (which is an instance of the Paperclip::Attachment class).

For example, if the attached file is :avatar, Paperclip will define an avatar method on the model.

Here is the define_instance_getter method, which is responsible for that:

# @name  => The name of the attachment
# @klass => The ActiveRecord model where this method is being defined

def define_instance_getter
  name    = @name
  options = @options

  @klass.send :define_method, @name do |*args|
    ivar = "@attachment_#{name}"
    attachment = instance_variable_get(ivar)

    if attachment.nil?
      attachment = Attachment.new(name, self, options)
      instance_variable_set(ivar, attachment)
    end
  end
end

From this code we can see that the attachment object is stored under the @attachment_#{name} instance variable.

On our example that would be @attachment_avatar.

Then it checks if this attachment already exists, and if it doesn’t, it creates a new Attachment object and sets the instance variable.

Here is the original source code.

Conclusion

As you have seen, metaprogramming is a very powerful and flexible technique, but whenever you want to reach for it remember that quote that says: “With great power comes great responsibility”.

If you enjoyed this post don’t forget to subscribe to my newsletter 🙂