Explaining magic behind popular Ruby code


Ruby language allows us to easily create beautiful DSL's and design complex libraries that anybody can easily use, despite programming experience. The code often looks perfect, but sometimes it is not clear how it was achieved under the hood. In this article, I explain a few solutions that are quite popular among many gems, and you can easily use it in your projects once you understand how you can build a similar code.

The config-way of setting variables

You can see this code in almost every Ruby gem that utilizes initializers to set some variables needed for later use:

SomeGem.configure do |config| config.api_key = 'api_key' config.app_name = 'My App'
end

This way of setting variables is very readable and easily extendable. It works like any other method that yields something. While the each method yields elements of the array, in the above example, the configure method yields the instance of the Configuration class (or any other). It allows us to set attributes of this class inside the block.

You can build your configurator this way:

module SomeGem class << self attr_accessor :configuration def configure self.configuration ||= Configuration.new yield(configuration) end end class Configuration attr_accessor :api_key, :app_name end
end

Let's give it a try:

SomeGem.configure do |config| config.api_key = 'api_key' config.app_name = 'My App'
end SomeGem.configuration.api_key # => 'api_key'
SomeGem.configuration.app_name # => 'My App'

You can later access values just like for any other object.

Dynamic methods

If you ever used Ruby on Rails code, you probably saw that many methods refer to the model attributes. Since the code is not aware of the attribute names unless you define them, it dynamically creates a method, and you won't find the standard definition for them.

Let's see some examples assuming that you have the class User and first_name and last_name attributes defined on it:

user = User.new(first_name: "John", last_name: "Doe")
user.first_name_is?('John') # => true
user.last_name_is?('John') # => false

Of course, you can call user.first_name == 'John', but I decided to show a straightforward case for the demonstration purposes. Here is the definition of our class:

class User attr_reader :first_name, :last_name def initialize(first_name:, last_name:) @first_name = first_name @last_name = last_name end
end

Right now, when calling #first_name_is? or last_name_is_mike? we would get the NoMethodError error because we didn't define those methods. This error is the starting point of the implementation of dynamic methods. Ruby exposes the method_missing method to allow us to do something with the not existing method and decide if we want to raise the error:

class User attr_reader :first_name, :last_name def initialize(first_name:, last_name:) @first_name = first_name @last_name = last_name end private def method_missing(method_name, *args, &block) puts "You are missing #{method_name} method" super # raise the error anyway end
end

After rerunning the previous code, you should see the following text printed before the error is raised:

You are missing first_name_is? method

The method_missing method accepts three arguments:

  • method_name - the name of the method that doesn't exist yet
  • args - optional parameter; arguments passed to the method
  • block - optional parameter; the block executed on the method

If we want to create a dynamic method, we must first verify that it ends with _is? and starts with the existing attribute's name. Then we have to raise the error if the method name is invalid or called on a not defined attribute or compare the attribute value with the first argument and return the result:

def method_missing(method_name, *args, &block) attribute_name = method_name.to_s.match(/(.*)_is\?/)&.captures&.first if !attribute_name.nil? && instance_variable_defined?("@#{attribute_name}") instance_variable_get("@#{attribute_name}") == args.first else super end
end def respond_to_missing?(method_name, include_private = false) method_name.to_s.end_with?('_is?') || super
end

Now we can play with our class and see that it's working:

user = User.new(first_name: 'John', last_name: 'Doe')
user.first_name_is?('John') # => true
user.last_name_is?('Tom') # => false user.method(:first_name_is?).call('John') # => true # call on not existing attribute
user.age_is?('John') # => NoMethodError

Remember to always define the respond_to_missing? method when overriding method_missing.

If you want to have more complex methods, you may want to create dynamic definitions.

Duck typing

Some objects behave differently when you call to_s on them (or any other method from the standard language library). For example, you might saw the following example:

response = Request.get('some_url') puts response # => "status: 200, body: some page body"
puts response.status # => 200
puts response.body # => "some page body"

Let's create a sample definition of Response and Request class:

class Response attr_reader :status, :body def initialize(status:, body:) @status = status @body = body end
end class Request def self.get(url) Response.new(status: 200, body: "some page body") end
end

Fine, but when calling the same code as before, we would not get the same result:

response = Request.get('some_url') puts response # => #<Response:0x00007f9e3f1a0150>

It happens because each time you use puts, it calls the to_s method on the passed thing, and in our case, the default to_s method defined on an object is called. It returns the string that includes the class name.

If we want to change this behavior, we can define the to_s method:

class Response attr_reader :status, :body def initialize(status:, body:) @status = status @body = body end def to_s "status: #{@status}, body: #{@body}" end
end

Now it works as expected:

response = Request.get('some_url') puts response # => "status: 200, body: some page body"

If it quacks like a duck, it's a duck. If it implements the to_s method, you can consider it as a string and use it as a string.

There are a lot more of these

Ruby is full of nicely looking code that looks like magic, but it's simple under the hood. If you found this article fun and useful, let us know if you would like to read more or say hello on Twitter!