ActiveSupport::HashWithIndifferentAccessとはなんぞや

こんにちは!kossyです!




さて、今回はActiveSupport::HashWithIndifferentAccessというクラスを発見してしまったので、
何をしているものなのかを検証してみようかと思います。



環境

Ruby 2.6.6
Rails 6.0.3.6
Docker for Mac



params.to_unsafe_hでActiveSupport::HashWithIndifferentAccessが返る

controllerの中のparams変数は、

$ params.class
=> ActionController::Parameters

ということでActionController::Parametersクラスのインスタンスなのですが、to_unsafe_hというメソッドが実装されており、実行すると、

$ params.to_unsafe_h.class
=> ActiveSupport::HashWithIndifferentAccess

掲題のActiveSupport::HashWithIndifferentAccessクラスのインスタンスが返ります。

まずはto_unsafe_hメソッドの定義位置を調べてみます。

$ params.method(:to_unsafe_h).source_location
=> ["/usr/local/bundle/gems/actionpack-6.0.3.6/lib/action_controller/metal/strong_parameters.rb", 336]

strong_parameters.rbの336行目に定義されているらしいので、Railsソースコードを見に行ってみます。

github.com

    # Returns an unsafe, unfiltered
    # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of the
    # parameters.
    #
    #   params = ActionController::Parameters.new({
    #     name: "Senjougahara Hitagi",
    #     oddity: "Heavy stone crab"
    #   })
    #   params.to_unsafe_h
    #   # => {"name"=>"Senjougahara Hitagi", "oddity" => "Heavy stone crab"}
    def to_unsafe_h
      convert_parameters_to_hashes(@parameters, :to_unsafe_h)
    end

安全でない、フィルターされていないものを返します、とのことですね。

convert_parameters_to_hashesも見に行きます。

github.com

      def convert_parameters_to_hashes(value, using)
        case value
        when Array
          value.map { |v| convert_parameters_to_hashes(v, using) }
        when Hash
          value.transform_values do |v|
            convert_parameters_to_hashes(v, using)
          end.with_indifferent_access
        when Parameters
          value.send(using)
        else
          value
        end
      end

end.with_indifferent_accessの部分が実行されると、ActiveSupport::HashWithIndifferentAccessクラスのインスタンスが返りそうですね。

  def with_indifferent_access
    ActiveSupport::HashWithIndifferentAccess.new(self)
  end

ActiveSupport::HashWithIndifferentAccessのinitialize処理を見に行きます。

github.com

    def initialize(constructor = nil)
      if constructor.respond_to?(:to_hash)
        super()
        update(constructor)

        hash = constructor.is_a?(Hash) ? constructor : constructor.to_hash
        self.default = hash.default if hash.default
        self.default_proc = hash.default_proc if hash.default_proc
      elsif constructor.nil?
        super()
      else
        super(constructor)
      end
    end

constructorがto_hashメソッドを実行できるなら、superクラスのinitializeを呼び出しています。
updateは以下のような定義です。

    def update(*other_hashes, &block)
      if other_hashes.size == 1
        update_with_single_argument(other_hashes.first, block)
      else
        other_hashes.each do |other_hash|
          update_with_single_argument(other_hash, block)
        end
      end
      self
    end

update_with_single_argumentを見る。

      def update_with_single_argument(other_hash, block)
        if other_hash.is_a? HashWithIndifferentAccess
          regular_update(other_hash, &block)
        else
          other_hash.to_hash.each_pair do |key, value|
            if block && key?(key)
              value = block.call(convert_key(key), self[key], value)
            end
            regular_writer(convert_key(key), convert_value(value))
          end
        end
      end

regular_updateを見る。と思ったら、updateのエイリアスメソッドでした。

処理の肝はconvert_keyですね。

      if Symbol.method_defined?(:name)
        def convert_key(key)
          key.kind_of?(Symbol) ? key.name : key
        end
      else
        def convert_key(key)
          key.kind_of?(Symbol) ? key.to_s : key
        end
      end

nameというメソッドがSymbolクラスに生えていれば、
引数のkeyがSymbolクラスのインスタンスならkey.nameを返し、Symbolクラスでないならkeyをそのまま返すconvert_keyメソッドを定義し、

nameというメソッドがSymbolクラスに生えていなければ、
引数のkeyがSymbolクラスのインスタンスならkey.to_sを返し、Symbolクラスでないならkeyをそのまま返すconvert_keyメソッドを定義してました。

まとめ

unsafeという名前だったのは、特に値をフィルタリングするような処理を入れていないからですね。

通常、controllerでは値をフィルタリングするためにpermitメソッドを用いると思うのですが、to_unsafe_hをするとpermitされてないparamsも返り値として得られます。

paramsはActionController::Parametersなので、普通のHashとして処理したい場合にto_unsafe_hメソッドを用いるとよさそうです。




勉強になりました。



大いに参考にさせていただいたサイト

ActionController::ParametersをHashにする - Qiita