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 🙂