[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 likeproduction?
. 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 ofself
during a method call is the receiver. In this case,self
is anActiveSupport::StringInquirer
which is a subclass ofString
. 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