Lessons from WriteBook - Lesson 3 - Inquiry
After reviewing the Writebook repository, I stumbled upon a curious method: “inquiry”.
# app/models/leafable.rb
def leafable_name
@leafable_name ||= ActiveModel::Name.new(self).singular.inquiry # what is this?
end
In a quest to better understand it, I set about attempting to re-implement it myself. It’s the best way to learn. On second thoughts - we can see the inquiry method being used, for example, here:
class Rails
def self.environment
"production"
end
end
Rails.environment
# => "production"
# => just a plain string, right?
# if so, can we do this:
Rails.environment.production?
# of course not. production? is not a method on a string object
# but somehow, in the rails repo, we can mysteriously do this without problem.
But wouldn’t it be neat, if we could actually do that? Inquiry is what makes such a thing possible:
vehicle = ActiveSupport::StringInquirer.new('car')
vehicle.car? # => true
vehicle.bike? # => false
But how?
Shall we try to re-implement this ourselves? If you want to look at the rails implementation, check it out here.
# string_inquiry_experiment.rb
# irb
# load "./string_inquiry_experiment.rb"
class String
def inquiry
puts "we've added a new method on string"
end
end
"random_string".respond_to?(:inquiry)
"random_string".inquiry
#=> we've added a new method on string
#=> nil
"random_string".random_string?
# => undefined method `random_string?' for "random_string":String (NoMethodError)
# this won't work.
We’ll have to add in that functionality:
# string_inquiry_experiment.rb
# run from the terminal, and don't forget to reload!
# irb
# load "./string_inquiry_experiment.rb"
class StringInquiry
def initialize(string_input)
@string_input = string_input
end
end
class String
def inquiry
StringInquiry.new(self)
end
end
"test".inquiry
#=> #<StringInquiry:0x00007f4c96aced80 @string_input="test">
# now we're getting somewhere!
# but we still cannot do "test".inquiry.test?
# because tehre is no such method.
We will have to override method_missing
and typically when we do so, we will also have to override respond_to_missing?
. Let’s do both:
# string_inquiry_experiment.rb
# irb
# load "./string_inquiry_experiment.rb"
class StringInquiry
def initialize(string_input)
@string_input = string_input
end
def method_missing(method_name, *args, &block)
# method name is a symbol.
# we need to check if "string_input".to_sym is the same as method_name minus a question mark on the end
# hence:
return true if method_name[0..-2] == @string_input
super(method_name, args, block)
end
end
class String
def inquiry
StringInquiry.new(self)
end
end
"test".inquiry.test?
=> true
# it works
"test".inquiry.production?
#=> }`method_missing': undefined method `production?' for #<StringInquiry:0x00007f4c96e5dd70 @string_input="test"> (NoMethodError)
### hmmm. We want production? to return false. So let us not pass on anything to super in method_missing:
If you want a challenge - make it work to match the rails specs.
After much stuffing around, we finally resolved it to match the source code noted in the active support repository. Yes, we did take a peak - but the root of the problem was: a lack of proper tests and specs. If you’re not clear about what you want - then you will never get there:
The final answer:
module ActiveSupport
# = \String Inquirer
#
# 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?
#
# == Instantiating a new \StringInquirer
#
# vehicle = ActiveSupport::StringInquirer.new('car')
# vehicle.car? # => true
# vehicle.bike? # => false
class StringInquirer < String
private
def respond_to_missing?(method_name, include_private = false)
method_name.end_with?("?") || super # automatically forwards all arguments
end
def method_missing(method_name, ...)
if method_name.end_with?("?")
self == method_name[0..-2]
else
super
end
end
end
end