【Rails】sandbox_by_defaultのPRを読みながらRailsの設定の仕組みを少し読んだ

はじめに

先日、Railsガイドを読んでいたら、sandbox_by_defaultという設定がRails7.1から追加されたことを知りました。 本番環境での操作ミスを低減できるので良いなと感じ、早速業務で使っているアプリでもこの設定を有効にしました。

この設定ってどういうコードで実現しているんだろうというのを疑問に思ったので、sandbox_by_defaultのPRを見ながらざっくりRailsのコードリーディングしてみました。

github.com

(まとめ)PRと関連のRailsコードを読んでわかったこと

  • Railsでconfigを管理しているのはRails::Application::Configurationクラス
    • config.xxxで書くような設定値はこのクラスのインスタンスのアクセサメソッド(attr_accessor)で管理されている
  • コンソールでのオプション管理を行うのはRails::Command::ConsoleCommandクラス
    • class_optionrailsコマンドのオプションを設定可能
    • そのうち、オプションの値を管理しているのはThor::Optionsクラス
  • 機能としてはただ単に設定を追加しただけに見えるが、PRを見ると、明示的にオプションをfalseで指定した時にはオプションを優先するといった考慮がされているのがわかった

コードリーディング内容

1. /rails/application/configuration.rb への変更

Rails::Application::Configurationクラスのattr_accessorsandbox_by_defaultが追加されている。

Rails::Application::Configurationクラスとは?
=> config/application.rbの中にconfig.xxxの形で設定を書くが、このconfigRails::Application::Configurationクラスのインスタンス

initializeの中で以下を設定し、デフォルトはfalseとして設定する。

@sandbox_by_default = false

config.sandbox_by_default = trueとすることで値を設定できるのは、 Rails::Application::Configurationクラスのインスタンスのアクセサメソッドが定義されているので、falseをtrueに書き換えているということ。

2. /rails/commands/console/console_command.rb への変更

class_optionへの変更

class_optionとは?の説明はRailsガイドにあった。

railsguides.jp

default: falseというのを見て気づいたが、--sandbox-s)のオプションには引数でbooleanが渡せる。

rails c -s false
#=> sandboxモードで起動しない

rails c -s true
#=> sandboxモードで起動する
疑問: 何も渡さない場合はtrueになるのはなんで?どこで制御している?

(1) railsコンソールを起動すると、class_optionを定義しているRails::Command::ConsoleCommandクラスのinitializeメソッドが呼ばれる

  • ruby/3.1.0/gems/railties-7.1.3.3/lib/rails/commands/console/console_command.rb
  • local_optionsという引数に ["-s", "false"]["-s"]というかたちで値が渡されている
  • その中でsuperでRails::Command::Baseinitializeが呼ばれる
    • ruby/3.1.0/gems/thor-1.3.0/lib/thor/base.rb
module Command
  class ConsoleCommand < Base
    # 略
    class_option :sandbox, aliases: "-s", type: :boolean, default: nil,
      desc: "Rollback database modifications on exit."

    def initialize(args = [], local_options = {}, config = {})
      # 略
      super(args, local_options, config) # local_options => ["-s", "false"]や["-s"]
    end
  end
end

(2) Rails::Command::Baseinitializeの中のopts.parse(array_options)でオプションのパースを行っている

  • array_optionsには ["-s", "false"]["-s"]の値が入っている
  • optsThor::Optionsインスタンス
module Base
  # 略
  def initialize(args = [], local_options = {}, config = {})
    # 略
    if local_options.is_a?(Array)
      array_options = local_options
      hash_options = {}
    else
      # 略
    end

    opts = Thor::Options.new(parse_options, hash_options, stop_on_unknown, disable_required_check, relations)

    self.options = opts.parse(array_options)
    # 略
  end
end

(3) opts.parseの中のparse_peek(switch, options)の中で実際にパースしている

  • ruby/3.1.0/gems/thor-1.3.0/lib/thor/parser/options.rb
  • parseの中でparse_peekを呼ぶ
  • parse_peekの中でparse_booleanが呼ばれて、オプションのキー(--sandbox)に対する値が渡されているかどうかを見て、値が渡されている場合は値をパースする
class Thor
  class Options < Arguments
    def parse(args)
      # 略
      result = parse_peek(switch, option)
      assign_result!(option, result)
      # 略
      assigns
    end

    private

    def parse_peek(switch, option)
      # 略
      send(:"parse_#{option.type}", switch) # option.typeはbooleanなのでparse_booleanメソッドが呼ばれる
    end

    def parse_boolean(switch)
      if current_is_value?
        if ["true", "TRUE", "t", "T", true].include?(peek)
          shift
          true
        elsif ["false", "FALSE", "f", "F", false].include?(peek) # 'false'という文字をfalseにする
          shift
          false
        else
          @switches.key?(switch) || !no_or_skip?(switch)
        end
      else # 値がない場合は下記の処理をする
        @switches.key?(switch) || !no_or_skip?(switch) # --sandboxがkeyに含まれているのでtrueを返す
      end
    end
  end
end

sandbox?メソッドへの変更

class_optionの変更にあるように、デフォルト値がnilになっている。 つまり、--sandboxのオプションを使用せずrailsコンソール起動した場合、Rails7.0までは、options[:sandbox]falseだったが、Rails7.1からはnilを返すことになる。

この修正によって、以下の2点がわかる。

  • rails c --sandbox falseのように「オプションを明示的にfalseで指定した時」とrails cのように「オプションを指定しないとき」の区別がつくようになる。
  • オプションを明示的で指定したときは、configの設定に関わらずオプションの方を優先するようになる。(rails c --sandbox falseだとsandboxモードは無効にする。)
# config.sandbox_by_default = true の設定を書いている時

$ rails c
#=> sandboxモードで起動

$ rails c --sandbox false
#=> sandboxモードは無効

ちなみに、この設定は本番環境で間違ってDB更新をすることを避けるために作られているため、developmenttest環境では無効となっている。 この制御をするために使われているRails.env.local?もRails7.1から入ったメソッド。

techracho.bpsinc.jp