`ActiveSupport::StringInquirer` magic

Rails magic is a widespread term among developers working with Ruby on Rails application. Is it an overstatement? Well, it depends. During day-to-day work, there isn’t usually enough time to study deeply Ruby on Rail source code. Even though it is an effective way to develop programming skills and understand the framework in-depth, sooner or later everybody finds mysterious places in a codebase that may cause frustration.

I bet that you have already seen if Rails.env.production? condition several times. Have you ever thought where it comes from? Today I learned that  ActiveSupport::StringInquirer1 class is a part of Ruby on Rails codebase and adds some magic powers to String objects 🙂

Wrapping a string in this class gives you a prettier way to test for equality.

In Rails codebase, we can see that the env method uses the class:

def env
  @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "development")
end

Surprisingly, the method does not return String object, but ActiveSupport::StringInquirer:

[22] pry(main)> Rails.env
=> "development"
[23] pry(main)> Rails.env.class
=> ActiveSupport::StringInquirer

So now it’s time to go even deeper and check the source code of ActiveSupport::StringInquirer2 class:

class StringInquirer < String
  private

  def respond_to_missing?(method_name, include_private = false)
    (method_name[-1] == "?") || super
  end

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

Let’s break its magic down using the Rails.env example:

  1. Rails.env returns "development", but its class isn’t String, but ActiveSupport::StringInquirer.
  2. ActiveSupport::StringInquirer inherits from String, so all its methods are available (Rails.env.titleize returns "Development" 🙂 ).
  3. development? method is not defined in String class nor ActiveSupport::StringInquirer, so method_missing one is executed as a fallback.
  4. method_missing checks if a method name ends with ?.
  5. In our case it ends with ?, so self == method_name[0..-2] comparison is made.
  6. "development" string is equal to "development?[0..-2]", so the method returns true.
Even more magic 🙂
String class defines inquiry method which wraps the current string in the ActiveSupport::StringInquirer class.

When to use ActiveSupport::StringInquirer?

First of all, you need to be brave enough to consciously use Rails magic 😉 Being serious, whenever you do strings comparison the class can be used instead. Please do some benchmarking first, though.

[28] pry(main)> name = "Johny"
=> "Johny"
[29] pry(main)> name.Johny?
NoMethodError: undefined method `Johny?' for "Johny":String
[30] pry(main)> name.inquiry.Johny?
=> true
[31] pry(main)> name.inquiry.class
=> ActiveSupport::StringInquirer
[32] pry(main)> ActiveSupport::StringInquirer.new("Johny").Johny?
=> true

Summary

Whenever you find a fragment of code you do not understand fully, it’s a good idea to go deeper and try to understand it fully. Thanks to such an approach I explored and understood new fragments of Ruby on Rails source code. As a result, Rails is a bit less magical to me 🧙.

Footnotes

  1. https://api.rubyonrails.org/classes/ActiveSupport/StringInquirer.html
  2. https://github.com/rails/rails/blob/master/activesupport/lib/active_support/string_inquirer.rb
 

Igor Springer

I build web apps. From time to time I put my thoughts on paper. I hope that some of them will be valuable for you. To teach is to learn twice.