Rails' Hidden Gems: ActiveSupport StringInquirer
Tags: ruby
Published: 2021-05-25 15:09:19

Rails is a large framework, and it grows larger every year. This makes it easy for some helpful features to slip by unnoticed. In this series, we'll take a look at some lesser-known functionality built into Rails for specific tasks.

In the first article in this series, we're going to take a look at how calling `Rails.env.test?` actually works under-the-hood by using the little-known `StringInquirer` class from ActiveSupport. Going one step further, we will also walk through the `StringInquirer` source code to see how it works, which (spoiler alert) is a simple example of Ruby's metaprogramming via the special `method_missing` method.

## The Rails.env helper

You've probably already seen code that checks the Rails environment:

```ruby
if Rails.env.test?
  # return hard-coded value...
else
  # contact external API...
end
```

However, what does `test?` actually do, and where does this method come from?

If we check `Rails.env` in a console, it acts like a string:

```ruby
=> Rails.env
"development"
```

The string here is whatever the RAILS*ENV environment variable is set to; it's not \_just* a String, though. When reading the class in the console, we see:

```ruby
=> Rails.env.class
ActiveSupport::StringInquirer
```

`Rails.env` is actually a StringInquirer.

## StringInquirer

The code for [StringInquirer](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/string_inquirer.rb) is both brief and well documented. How it works, though, may not be obvious unless you are familiar with Ruby's metaprogramming capabilities. We'll walk through what's happening here.

```ruby
class StringInquirer < String
...
end
```

First, we see that StringInquirer is a subclass of String. This is why `Rails.env` acts like a String when we call it. By inheriting from String, we automatically get all the String-like functionality, so we can treat it like a String. `Rails.env.upcase` works, as does `ActiveRecordModel.find_by(string_column: Rails.env)`.

While `Rails.env` is a handy, built-in example of StringInquirer, we are also free to create our own:

```ruby
def type
  result = "old"
  result = "new" if @new

  ActiveSupport::StringInquirer.new(result)
end
```

Then, we get the question-mark methods on the returned value:

```ruby
=> @new = false
=> type.old?
true
=> type.new?
false
=> type.testvalue?
false

=> @new = true
=> type.new?
true
=> type.old?
false
```

### method_missing

`method_missing` is the real secret sauce here. In Ruby, whenever we call a method on an object, Ruby begins by looking at all the ancestors of the object (i.e., base classes or included modules) for the method. If one is not found, Ruby will call `method_missing`, passing in the name of the method it is looking for, along with any arguments.

By default, this method just raises an exception. We're all probably familiar with the `NoMethodError: undefined method 'test' for nil:NilClass` type of error message. We can implement our own `method_missing` that does not raise an exception, which is exactly what `StringInquirer` does:

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

For _any_ method name that ends in `?`, we check the value of `self` (i.e., the String) against the method name without the `?`. Put another way, if we call `StringInquirer.new("test").long_test_method_name?`, the returned value is `"test" == "long_test_method_name"`.

If the method name does _not_ end with a question mark, we fall back to the original `method_missing` (the one that will raise an exception).

### respond_to_missing?

There's one more method in the file: `respond_to_missing?`. We might say that this is a companion method to `method_missing`. Although `method_missing` gives us the functionality, we also need a way to tell Ruby that we accept these question-mark-ending methods.

```ruby
def respond_to_missing?(method_name, include_private = false)
  method_name.end_with?("?") || super
end
```

This comes into play if we call `respond_to?` on this object. Without this, if we called `StringInquirer.new("test").respond_to?(:test?)`, the result would be `false` because we have no explicit method called `test?`. This is obviously misleading because I would expect it to return true if I call `Rails.env.respond_to?(:test?)`.

`respond_to_missing?` is what allows us to say "yes, I can handle that method" to Ruby. If the method name does not end in a question mark, we fall back to the superclass method.

## Practical use cases

Now that we know _how_ StringInquirer works, let's take a look at instances where it could be useful:

### 1. Environment variables

Environment variables meet two criteria that make them great choices for StringInquirer. First, they often have a limited, known set of possible values (like an `enum`), and second, we often have conditional logic that depends on their value.

Let's say our app is hooked up to a payment API, and we have the credentials stored in environment variables. In our production system, this is obviously the real API, but in our staging or development version, we want to use their sandbox API:

```ruby
# ENV["PAYMENT_API_MODE"] = sandbox/production

class PaymentGateway
  def api_mode
    # We use ENV.fetch because we want to raise if the value is missing
    @api_mode ||= ENV.fetch("PAYMENT_API_MODE").inquiry
  end

  def api_url
    # Pro-tip: We *only* use production if MODE == 'production', and default
    # to sandbox if the value is anything else, this prevents us using production
    # values if the value is mistyped or incorrect
    if api_mode.production?
      PRODUCTION_URL
    else
      SANDBOX_URL
    end
  end
end
```

Note that in the above, we are using ActiveSupport's `String#inquiry` method, which handily converts a String into a StringInquirer for us.

### 2. API responses

Continuing our payment API example from above, the API will send us a response that includes some success/failure state. Again, this meets the two criteria that make it a candidate for StringInquirer: a limited set of possible values and conditional logic that will test these values.

```ruby
class PaymentGateway
  def create_charge
    response = JSON.parse(api_call(...))

    result = response["result"].inquiry

    if result.success?
      ...
    else
      ...
    end

    # result still acts like a string
    Rails.logger.info("Payment result was: #{result}")
  end
end
```

## Conclusion

StringInquirer is an interesting tool to have in your back pocket, but personally, I wouldn't reach for it too often. It has some uses, but most of the time, an explicit method on an object gives you the same result. An explicitly named method also has a couple of benefits; if the value ever needs to change, you only have to update a single place, _and_ it makes the codebase easier to search if a developer is trying to locate the method.

Although it focuses on StringInquirer, this article is intended to be more of a gentle introduction to some of Ruby's metaprogramming capabilities using `method_missing`. I would say that you probably don't want to use `method_missing` in your application. However, it is often used in frameworks, such as Rails or domain-specific-languages provided by gems, so it can be very helpful to know "how the sausage is made" when you encounter issues.