String Inquirer

- - posted in active_support

The Point

Sometimes doing a String Comparison just feels too inelegant. Take, for example, this snippet, often seen in code when people are first starting out:

1
if Rails.env == "production"

Now take this alternative, which accomplishes the same thing, but looks 10 times better:

1
if Rails.env.production?

So Rails.env isn’t really magic, it’s just a ActiveSupport::StringInquirer. This class makes all sorts of inquiries like this possible, such as Rails.env.banana_brains? (This will probably return false.)

The Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
module ActiveSupport
  # Wrapping a string in this class gives you a prettier way to test
  # for equality. The value returned by <tt>Rails.env</tt> is wrapped
  # in a StringInquirer object so instead of calling this:
  #
  #   Rails.env == 'production'
  #
  # you can call this:
  #
  #   Rails.env.production?
  class StringInquirer < String
    private

      def respond_to_missing?(method_name, include_private = false)
        method_name[-1] == '?'
      end

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

The Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
require 'abstract_unit'

class StringInquirerTest < ActiveSupport::TestCase
  def setup
    @string_inquirer = ActiveSupport::StringInquirer.new('production')
  end

  def test_match
    assert @string_inquirer.production?
  end

  def test_miss
    assert_not @string_inquirer.development?
  end

  def test_missing_question_mark
    assert_raise(NoMethodError) { @string_inquirer.production }
  end

  def test_respond_to
    assert_respond_to @string_inquirer, :development?
  end
end

The Breakdown

1
2
3
  def respond_to_missing?(method_name, include_private = false)
    method_name[-1] == '?'
  end

Ruby objects have a mechanism for handling methods that aren’t actually defined. If a method is called that isn’t defined, respond_to_missing? gets called to determine whether method_missing should be called.

In this case, our custom method_missing will only be run if the last character of the method name is a question mark.

1
2
3
4
5
6
7
  def method_missing(method_name, *arguments)
    if method_name[-1] == '?'
      self == method_name[0..-2]
    else
      super
    end
  end

method_missing, as discussed above, is called when a method is called that isn’t defined. It provides the name of the method, and any arguments that were passed in.

If the last character of the method name is a question mark, we do the simple example from above (Rails.env == "production"), checking to see if the method name (minus the question mark, of course) matches the string. If the method name doesn’t end with a question mark, we pass up the method missing chain to the class’s ancestors.

Closing Thoughts

While I wouldn’t use StringInquirer on a daily basis, I could see using it for preferences or anything else that had a few likely options. DO you have a use case for it? Leave it in the comments!

Comments