Explaining magic behind popular Ruby on Rails code

This article is a continuation of a popular article about the magic behind the popular Ruby code. This time, I will explain the logic behind popular Ruby on Rails code used by thousands of developers worldwide but not available out of the box in pure Ruby.

Checking for presence

I would risk writing that the most popular Rails’ methods are present? and blank?. Along with .presence, they are used to verify if a given thing (variable, object, attribute) has any value. They are universal and work with every type of value, and under the hood, they are straightforward.

I sometimes ask developers if those methods are part of the core Ruby, and they are often surprised that they are only implemented in Rails.

blank?

This method ends with the quotation mark because it always returns a boolean value: true or false. As I mentioned before, the logic for it is elementary. Let's take a closer look at the blank? source code for different objects type.

Object

def blank?
  respond_to?(:empty?) ? !!empty? : !self
end

The above definition is very flexible as Ruby objects like arrays, hashes, and strings implement empty? method but you can also use duck typing and implement this method in your classes:

class Garage
  delegate :empty?, to: :@cars
  def initialize
    @cars = []
  end

  def park_car(car)
    @cars << car
  end
end

you can test it when no cars are parked inside the garage and when you would park one car:

garage = Garage.new
garage.blank? # => true

garage.park_car("Tesla")
garage.blank? # => false

String

Even though the string implements the empty? method, it is not always recommended to use that method. It won’t work if the string contains white spaces; that’s why Rails implements the blank? method for strings a little bit different:

def blank?
  empty? ||
    begin
      !!/\A[[:space:]]*\z/.match(self, 0)
    rescue Encoding::CompatibilityError
      ENCODED_BLANKS[self.encoding].match?(self)
    end
end

Because the regexp is expensive, empty? is used in the first place, and it is enough in most cases where the string does not contain anything.

Integer

For integers blank? will always return false:

def blank?
  false
end

Other objects

Rails implements the blank? method for other objects as well including:

  • ActiveRecord relation and errors
  • Database configurations
  • TimeWithZone, Time, Date, and DateTime

The method definitions are straightforward; usually, they return false or are delegation to a different method, so there is no need to show their sources.

present?

This method is just a simple inversion of blank? method for different objects:

def present?
  !blank?
end

presence

Before I show you the source of this method, let’s take a look at a simple example to understand why you need to use it:

class User < ApplicationRecord
  def full_address
    return address.full_address if address.full_address.present?

    "No location"
  end
end

The above method is quite simple, but we can make it a one-liner with presence:

class User < ApplicationRecord
  def full_address
    address.full_address.presence || "No location"
  end
end

When the value is present, the method returns the value; otherwise, it returns nil; that’s why the additional value is returned. Let’s take a look at the method’s source:

def presence
  self if present?
end

Again, the source code is simple, but it will work perfectly with any type of object.

Manipulating dates with 1.day, 2.months.ago, etc.

Rails makes it easy to manipulate dates by providing an interface with more human-readable values:

  • 1.second
  • 1.minute
  • 1.hour
  • 1.day
  • 1.week
  • 1.month
  • 1.year

With pure Ruby, to add 5 hours to the current time, you would have to do the following:

Time.now + (60 * 60 * 5)

while with Rails, you can simply do:

Time.now + 5.hours

This is perfectly simple and readable at the same time. Let’s take a closer look under the hood of that expression.

Active Support’s Duration

The class responsible for the mentioned logic is ActiveSupport::Duration. Those two calls return the same value:

60.seconds
ActiveSupport::Duration.new(60, seconds: 60)

The first argument is the time representation in seconds, and the second is a key argument where the key is the name of units, and the value is the number of seconds, hours, days, etc.

Since Rails extends the numeric classes, methods like days, months, etc., are available on the standard integer or float. You can replicate that behavior by opening the Integer class and adding some custom logic:

class Car; end

class Integer
  def cars
    Array.new(self) { Car.new }
  end
end

Now, if we would like to create two cars, we can do the following:

2.cars # => [#<Car:0x00007ff2b01cae80>, #<Car:0x00007ff2b01cae58>]

Some time ago

With Rails, we can get the past date with the following call:

6.hours.ago

Since we know what the logic sits behind 6.hours, we can now take a look at the ago method. This method uses another Active Support module called TimeWithZone.

The method since is called which accepts the number of seconds and returns a time in past or future. If the number of seconds is negative, it will return the time in the past:

1.hour.ago
# the same as
Time.current.since(-1 * (60 * 60))

1.hour.from_now
# the same as
Time.current.since(60 * 60)

Addition and subtraction

One thing has left to explain in terms of playing with dates with the help of Rails. You can usually spot the following pattern in many applications:

Time.at(value) + 5.hours

So what happens if you would like to increase a time value by the given number of hours? Since + is just a method invoked on a Time instance, we can check where the method is defined by executing the following code:

Time.instance_method(:+).source_location

Again, ActiveSupport overwrote the default method, so it’s possible to pass ActiveSupport::Duration as the argument. In pure Ruby, you are limited to the Time instance. Let’s check the definition as it’s a tricky one and not obvious when you look at it for the first time:

def plus_with_duration(other) #:nodoc:
  if ActiveSupport::Duration === other
    other.since(self)
  else
    plus_without_duration(other)
  end
end

alias_method :plus_without_duration, :+
alias_method :+, :plus_with_duration

When you call +, the plus_with_duration method is called. When the argument is the instance of ActiveSupport::Duration, then the since method is called, which we discussed before. Otherwise, the plus_without_duration method is called, which is an alias for +, so the standard method for Time is executed.

Thanks to this trick, we can support both Duration instance and Time instance as the argument.

Delegation

Delegation is a simple process of executing methods on a given class executed with a different object that lives inside the object’s instance on which the method is executed. This description sounds a little bit complicated so let’s consider the following example:

class Profile
  def image_url
    'image_url'
  end

  def nickname
    'nick'
  end
end

class User
  def initialize
    @profile = Profile.new
  end

  def image_url
    @profile.image_url
  end

  def nickname
    @profile.nickname
  end
end

user = User.new
user.nickname # => 'nick'
user.image_url # => 'image_url'

This the explicit delegation as we explicitly use other object and call the desired method on it. If we would like to delegate more methods, we can easily repeat ourselves and make the class definition longer and longer.

Ruby standard delegation

Core Ruby provides a way to delegate methods as well:

require 'forwardable'

class User
  extend Forwardable

  def initialize
    @profile = Profile.new
  end

  def_delegators :@profile, :nickname, :image_url
end

You have to remember to extend your class with the Forwardable module; otherwise, you won’t be able to use the def_delegators method. Let’s take a look at the alternative delegation provided by Rails.

Rails delegation

If you are using Rails, you can delegate the methods using the following way:

class User
  delegate :nickname, :image_url, to: :@profile

  def initialize
    @profile = Profile.new
  end
end

How it’s implemented in Rails? Let’s find the source of the method first:

User.method(:delegate).source_location

Again, it’s ActiveSupport module and its core extension for module:

def delegate(*methods, to: nil, prefix: nil, allow_nil: nil, private: nil)
  # definition
end

As you can see the Rails’ delegation is way more flexible as you can achieve the following things.

Add prefix

We can simply use a prefix to make the delegation more explicit:

class User
  delegate :nickname, to: :@profile, prefix: :profile
end

User.new.profile_nickname # => 'nick'

Allow for nil values

class User
  delegate :nickname, to: :@profile, allow_nil: true

  def initialize
    @profile = nil
  end
end

User.new.nickname # => nil

Make the delegation private

If you would to delegate methods but make them private, you can add the private option:

class User
  delegate :nickname, to: :@profile, private: true
end

If you would call now User.new.nickname, you would receive NoMethodError as the error is only available inside the class, and to invoke it inside, you have to use send - User.new.send(:nickname).

Explaining magic behind delegate from Rails

Investigation time. The way the delegation is working in Rails is simple and may be a bit surprising for you. If you are calling the following delegation:

delegate :nickname, to: :@profile, allow_nil: true, prefix: :profile

then the Rails is building the following definition as a text (and repeat it for every delegated method):

def profile_nickname
  _ = @profile
  if !_.nil? || nil.respond_to?(:nickname)
    _.nickname
  end
end

then the Rails finds the place where the method should be defined by calling a method from the Thread::Backtrace::Location module provided by pure Ruby:

location = caller_locations(1, 1).first
path = location.path
line = location.lineno

and then uses the module_eval method to execute the string in the context of the module:

module_eval(method_def.join(";"), file, line)

The method_def variable is an array with methods definition. The file and line arguments are passed, but they are used only for error messages.

That’s it.