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
, andDateTime
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.