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
Written on August 2, 2025