GoRails: StringInquirer Script

[Intro]
Have you ever wondered why we can write Rails.env.production? instead of having to check with equals like this Rails.env == "production"

Well, that is because of a little innocent class called StringInquirer in ActiveSupport module of Rails. We're about to see exactly why the predicate method producetion? works. And in the process see an example of using method_missing and couple other metaprogramming concepts in Ruby.

A quick review, method_missing is a method on the Object class. It is called at the end of the ancestor chain. all method calls on an Object will end up in method_missing assuming the method is not found anywhere else in the chain. [todo: finish explaining method_missing]

So we can override method_missing in our class to 'catch' a number of calls to methods that don't actually exist on our object. Sometimes called Ghost Methods. Why is this useful?

Let's go back to StringInquirer.

[cmd+tab to browser open tab - StringInquire source code]

Here's what its definition looks like. It's pretty short. But here is method_missing.

class StringInquirer < String
    private
      def respond_to_missing?(method_name, include_private = false)
        method_name.end_with?("?") || super
      end

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

[cmd+tab terminal. type in the following code]

>> fruit = ActiveSupport::StringInquirer.new("apple")
=> "apple"
>> fruit.apple?
=> true
>> fruit.orange?
=> false
>> fruit.sldjlsd?
=> false

You get the idea. That's all it does. But how does it reply correctly to arbitrary method names?

Let's go back to the source code.

[cmd+tab to browser open tab - StringInquire source code]

  • It checks whether the name of the method ends with a ? i.e if it's a predicate method like production?. If it does, we want to 'catch' it.
  • If so, it gets the method_name without the ? and does a comparison with self using double equals and returns true or false. Remember that value of self during a method call is the receiver. In this case, self is an ActiveSupport::StringInquirer which is a subclass of String. So we can compare it to a string.
>> fruit.class
=> ActiveSupport::StringInquirer
>> fruit
=> "apple"
>> fruit == "apple"
=> true
  • If the method_name does not end with a ?, it passes the call off to super which will throw 'NoMethodError' error for other methods. We're only trying to catch methods ending in ? not all methods.
>> fruit.grow
/Users/bhumi/.rbenv/versions/3.0.3/lib/ruby/gems/3.0.0/gems/activesupport-7.0.0/lib/active_support/string_inquirer.rb:29:in `method_missing': undefined method `grow' for "apple":ActiveSupport::StringInquirer (NoMethodError)

I don't know what you're talking about. So far so good.

  • And there is also this respond_to_missing, we'll come back to that one.

Now let's jump into how StringInquirer is used in Rails.env

[Cmd+tab to browser. cntr tab to rails.env source code]

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

    # Sets the \Rails environment.
    #
    #   Rails.env = "staging" # => "staging"
    def env=(environment)
      @_env = ActiveSupport::EnvironmentInquirer.new(environment)
    end

It wraps the env name string in a class called EnvironmentInquirer. I bet that's a subclass of our StringInquirer.

[cntr + tab over to that tab]

Yup it is. Though it's not using method_missing. It does an optimization (read the comment). Because we know we're not going to have arbritrary string values here. It'll be handful to values like development, production, test, etc.

This code has more metaprogramming examples, dynamically adding instance varibles and opening the class at runtime.
[todo: explain the code with instance_variable_set and class_eval]

Before we wrap up, we said we'd come back to respond_to_missing. Why is that needed? [todo: add explanation and code for that]

[Outro]
Finally, rails.env

>> Rails.env
=> "development"
>> Rails.env.development?
=> true
>> Rails.env.test?
=> false

This is just one example of how ruby metaprogramming concepts are applied ruby gems and libraries like Rails's ActiveSupport.

Hope you enjoyed looking into some ActiveSupport code with me. That's all I got. Bye [wave and hang up]


For StringInquirer:
The sourcecode https://github.com/rails/rails/blob/832fb1de704899a230c83e7c966efac03a012137/activesupport/lib/active_support/string_inquirer.rb#L21

Using it in Rails.env https://github.com/rails/rails/blob/832fb1de704899a230c83e7c966efac03a012137/railties/lib/rails.rb#L72

It uses a subclass of StringInquirer called EnvironmentInquirer that does not rely on method_missing. uses instance_variable_set, class_eval
https://github.com/rails/rails/blob/main/activesupport/lib/active_support/environment_inquirer.rb#L7