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

active_recordをrails以外で使う

ググるとActive Recordを単独で使う方法が出てきますが、手元のRails7系のActive Recordだと動かなかったので、調べながらやってみました。

結論

以下で動きました。

# Rakefile
require "bundler/gem_tasks"
require "bundler/setup"

require "active_record"
require "erb"

include ActiveRecord::Tasks # rubocop:disable Style/MixinUsage

root_dir = File.dirname(__FILE__)
database_config_path = File.join(root_dir, "config/database.yml")
database_config = YAML.safe_load(ERB.new(File.read(database_config_path)).result, aliases: true)

DatabaseTasks.database_configuration = database_config
DatabaseTasks.db_dir = "db"
DatabaseTasks.env = "development"
DatabaseTasks.migrations_paths = "db/migrate"
DatabaseTasks.root = root_dir

task :environment do
  ActiveRecord::Base.configurations = database_config
  ActiveRecord::Base.establish_connection :development
end

load "active_record/railties/databases.rake"

補足

ActiveRecord::Tasks::DatabaseTasks

ActiveRecord::Tasks::DatabaseTasks
migrateに関するロジックがカプセル化してまとめられているとのこと。

Example usage of DatabaseTasks outside Rails could look as such:

include ActiveRecord::Tasks
DatabaseTasks.database_configuration = YAML.load_file('my_database_config.yml')
DatabaseTasks.db_dir = 'db'
# other settings...

DatabaseTasks.create_current('production')

other settingsのところは、リンク先にそれぞれ記載されています。

active_record/railties/databases.rake

rails/databases.rake at v7.0.4.3 · rails/rails
これをloadすることでrails同様にActive Recordが利用できるようになります。便利。

$ bundle exec rake -T db
rake db:create              # Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all data...
rake db:drop                # Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases ...
rake db:encryption:init     # Generate a set of keys for configuring Active Record encryption in a given environment
rake db:environment:set     # Set the environment value for the database
rake db:fixtures:load       # Loads fixtures into the current environment's database
rake db:migrate             # Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)
rake db:migrate:down        # Runs the "down" for a given migration VERSION
rake db:migrate:redo        # Rolls back the database one migration and re-migrates up (options: STEP=x, VERSION=x)
rake db:migrate:status      # Display status of migrations
rake db:migrate:up          # Runs the "up" for a given migration VERSION
rake db:prepare             # Runs setup if database does not exist, or runs migrations if it does
rake db:reset               # Drops and recreates all databases from their schema for the current environment and loads the seeds
rake db:rollback            # Rolls the schema back to the previous version (specify steps w/ STEP=n)
rake db:schema:cache:clear  # Clears a db/schema_cache.yml file
rake db:schema:cache:dump   # Creates a db/schema_cache.yml file
rake db:schema:dump         # Creates a database schema file (either db/schema.rb or db/structure.sql, depending on `ENV['SCHEMA_FORMAT']` or `config.activ...
rake db:schema:load         # Loads a database schema file (either db/schema.rb or db/structure.sql, depending on `ENV['SCHEMA_FORMAT']` or `config.active_...
rake db:seed                # Loads the seed data from db/seeds.rb
rake db:seed:replant        # Truncates tables of each database for current environment and loads the seeds
rake db:setup               # Creates all databases, loads all schemas, and initializes with the seed data (use db:reset to also drop all databases first)
rake db:version             # Retrieves the current schema version number

haml-lintでrubocopのlintをviewに対しても実行する

なんかlint色々整理したくなってきたので、続いてはhamlです。
sds/haml-lint を入れればokです。
おすすめポイントは、rubocopも見てくれます(もちろんindentなどviewに適用できないやつ以外)。
個人的には 昔記事に書いたslim-lint同様、お仕事でも使いたいやつです。

Installtaion

# Gemfile
gem 'haml_lint', require: false

bundle install すれば良いです。

実行

ディレクトリの指定なしでも動作しますが、対象は絞っておくと良いでしょう。
あとは、必要に応じて、.haml-lint.yml をメンテナンスしていけば良いです。

bundle exec haml-lint app/views/
# .haml-lint.yml 例
linters:
  LineLength:
    enabled: false
  ViewLength:
    enabled: false

こんな感じ。

ついでにvscode

haml-lint はあまり使われてないのか、ダウンロード数は少なめですが、一応こちらで動作しました。(thanks!!)
ただ、動作が遅いので継続して利用するかは分かりません。
Haml Lint - Visual Studio Marketplace

bundlerを利用していれば、以下の設定を入れておく必要があります。

"hamlLint.useBundler": true

See Also

slim-lintが良い
lefthookでgit hooksのタイミングでlintを行う