How Rails.env works with EnvironmentInquirer & StringInquirer (Example) | GoRails

How Rails.env works with EnvironmentInquirer & StringInquirer (Example) | GoRails
をみていました。
自然すぎて、気にもしなかった、Rails.env.development? のコード解説です。中身はエレガントな実装になってました。
12分ほどの動画なのでさらっと見れます。

rails consoleで確認

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

となりますが、これは一体どうやってるのか。

ActiveSupport::Stringinquirer

まずは、ActiveSupport::Stringinquirer。
こいつは、String を継承していて、? 付きのメソッド呼び出しがあれば、selfと?を削除した文字列との比較を行うようになってます。

# https://github.com/rails/rails/blob/v7.1.1/activesupport/lib/active_support/string_inquirer.rb
  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

要はこういうこと。

>> fruit = ActiveSupport::StringInquirer.new("apple")
=> "apple"
>> fruit.apple?
=> true
>> fruit.apple
/Users/rochefort/.asdf/installs/ruby/3.2.0/lib/ruby/gems/3.2.0/gems/activesupport-7.0.4.2/lib/active_support/string_inquirer.rb:29:in `method_missing': undefined method `apple' for "apple":ActiveSupport::StringInquirer (NoMethodError)

>>fruit.orange?
=> false

Rails.env

続いて、Rails.env を見てみると、環境名を渡して、ActiveSupport::EnvironmentInquirer を newしています。

# https://github.com/rails/rails/blob/v7.1.1/railties/lib/rails.rb
module Rails
  class << self
    def env
      @_env ||= ActiveSupport::EnvironmentInquirer.new(ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "development")
    end

ActiveSupport::EnvironmentInquirer

ここでは、initializeで環境ごとのインスタンス変数(@development, @test, @production )を生成し、 class_eval で 環境ごとのメソッド(development?, test?, production?)を作成しています。

#https://github.com/rails/rails/blob/v7.1.1/activesupport/lib/active_support/environment_inquirer.rb
module ActiveSupport
  class EnvironmentInquirer < StringInquirer # :nodoc:
    DEFAULT_ENVIRONMENTS = %w[ development test production ]

    def initialize(env)
      raise(ArgumentError, "'local' is a reserved environment name") if env == "local"

      super(env)

      DEFAULT_ENVIRONMENTS.each do |default|
        instance_variable_set :"@#{default}", env == default
      end

      @local = in? LOCAL_ENVIRONMENTS
    end

    DEFAULT_ENVIRONMENTS.each do |env|
      class_eval <<~RUBY, __FILE__, __LINE__ + 1
        def #{env}?
          @#{env}
        end
      RUBY
    end

こういう仕組みになっていたわけですね。

Rails.envはこのクラスのオブジェクトです。

>> Rails.env.class
=> ActiveSupport::EnvironmentInquirer

String

最初に説明した、 ActiveSupport::StringInquirer をnewする inquiry を用意しています。

# https://github.com/rails/rails/blob/v7.1.1/activesupport/lib/active_support/core_ext/string/inquiry.rb
class String
  def inquiry
    ActiveSupport::StringInquirer.new(self)
  end