Railsのソースコード読んでみる | Active Support to_json編

f:id:sktktk1230:20190921180106p:plain

普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました

読めるようにするまで

以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

2.11 JSON support

Active Supportが提供するto_jsonメソッドの実装は、通常json gemがRubyオブジェクトに対して提供しているto_jsonよりも優れています。その理由は、HashやOrderedHash、Process::Statusなどのクラスでは、正しいJSON表現を提供するために特別な処理が必要になるためです。
引用:RAILS GUIDES:Active Support コア拡張機能:JSON

JSON を見てみます

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
Railsの日本語ドキュメントを見てみると

Returns a JSON string representing the hash. Without any options, the returned JSON string will include all the hash keys. For example:

  { :name => "Konata Izumi", 'age' => 16, 1 => 2 }.to_json
  # => {"name": "Konata Izumi", "1": 2, "age": 16}  

引用:API dock:to_json

レシーバに対して to_jsonすると、JSON文字列を返すという処理です

thinkit.co.jp

ソースコードを読んでみる

1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def to_json を探してみます

f:id:sktktk1230:20180201183238p:plain

2. 該当箇所が2個ほどあったので、それぞれみてみます

1. activesupport > lib > active_support > core_ext > object > json.rb
1. module ActiveSupport::ToJsonWithActiveSupportEncoder
省略

module ActiveSupport
  module ToJsonWithActiveSupportEncoder # :nodoc:
    def to_json(options = nil)
      if options.is_a?(::JSON::State)
        # Called from JSON.{generate,dump}, forward it to JSON gem's to_json
        super(options)
      else
        # to_json is being invoked directly, use ActiveSupport's encoder
        ActiveSupport::JSON.encode(self, options)
      end
    end
  end
end

メソッド to_json のみ実装されたモジュールです さらに読み進めてみます

省略

[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable].reverse_each do |klass|
  klass.prepend(ActiveSupport::ToJsonWithActiveSupportEncoder)
end

reverse_each の挙動を調べてみます

reverse_eachメソッドは、配列の要素の数だけブロックを繰り返し実行します。繰り返しごとにブロック引数には各要素が末尾から逆順に入ります。戻り値はレシーバ自身です。
animals = ["dog", "cat", "mouse"]
animals.reverse_each {|anim| puts anim }
mouse  
cat  
dog

引用:Rubyリファレンス:reverse_each

Enumerableから順番に klass へ各要素が入ります

次にブロックの中

klass.prepend(ActiveSupport::ToJsonWithActiveSupportEncoder)

を見てみます

prependがわからないので、調べてみると

指定したモジュールを self の継承チェインの先頭に「追加する」ことで self の定数、メソッド、モジュール変数を「上書き」します。
継承チェイン上で、self のモジュール/クラスよりも「手前」に 追加されるため、結果として self で定義されたメソッドは override されます。
modules で指定したモジュールは後ろから順に処理されるため、 modules の先頭が最も優先されます。
また、継承によってこの「上書き」を処理するため、prependの引数として 渡したモジュールのインスタンスメソッドでsuperを呼ぶことで self のモジュール/クラスのメソッドを呼び出すことができます。
実際の処理は modules の各要素の prepend_features を後ろから順に呼びだすだけです。 Module#prepend_features が継承チェインの改変を実行し、結果として上のような 処理が実現されます。そのため、prepend_features を override することで prepend の処理を追加/変更できます。
引用:Ruby2.5.0:prepend

配列のクラス [Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable] にモジュールを追加して to_json をオーバーライドしてます
再度、ActiveSupport::ToJsonWithActiveSupportEncoderを見てみると

def to_json(options = nil)
  if options.is_a?(::JSON::State)
    # Called from JSON.{generate,dump}, forward it to JSON gem's to_json
    super(options)
  else
    # to_json is being invoked directly, use ActiveSupport's encoder
    ActiveSupport::JSON.encode(self, options)
  end
end

optionに ::JSON::State を指定した場合には、super(options) により継承元の to_json を呼んでいます
それ以外は ActiveSupport::JSON.encode(self, options) となります

encodeの処理を見てみます

2. activesupport > lib > active_support > json > encoding.rb
module JSON
  # Dumps objects in JSON (JavaScript Object Notation).
  # See http://www.json.org for more info.
  #
  #   ActiveSupport::JSON.encode({ team: 'rails', players: '36' })
  #   # => "{\"team\":\"rails\",\"players\":\"36\"}"
  def self.encode(value, options = nil)
    Encoding.json_encoder.new(options).encode(value)
  end

引数valueに入るのは to_json した際のレシーバです

次に Encoding.json_encoderまでみてみます

module ActiveSupport

省略

  module JSON

省略

    module Encoding #:nodoc:

省略

    self.json_encoder = JSONGemEncoder
    self.time_precision = 3
    end

module Encoding内でjson_encoderにJSONGemEncoderクラスをセットしています

JSONGemEncoderクラスのinitializerでは引数のオプションをインスタンス変数に入れています

module Encoding #:nodoc:
  class JSONGemEncoder #:nodoc:
    attr_reader :options

    def initialize(options = nil)
      @options = options || {}
    end

ここまでみると Encoding.json_encoder.new(options) の戻り値は JSONGemEncoderクラスのインスタンスです

省略

class JSONGemEncoder #:nodoc:

省略

# Encode the given object into a JSON string
def encode(value)
  stringify jsonify value.as_json(options.dup)
end

上記は括弧が省略されていますが、 stringify(jsonify(value.as_json(options.dup))) と同義です
value.as_jsonでvalueは to_json した際のレシーバですので、ハッシュなどになります

jsonifyをみてみます

# Convert an object into a "JSON-ready" representation composed of
# primitives like Hash, Array, String, Numeric,
# and +true+/+false+/+nil+.
# Recursively calls #as_json to the object to recursively build a
# fully JSON-ready object.
#
# This allows developers to implement #as_json without having to
# worry about what base types of objects they are allowed to return
# or having to remember to call #as_json recursively.
#
# Note: the +options+ hash passed to +object.to_json+ is only passed
# to +object.as_json+, not any of this method's recursive +#as_json+
# calls.
def jsonify(value)
  case value
  when String
    EscapedString.new(value)
  when Numeric, NilClass, TrueClass, FalseClass
    value.as_json
  when Hash
    Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }]
  when Array
    value.map { |v| jsonify(v) }
  else
    jsonify value.as_json
  end
end

Stringクラスの場合には

# This class wraps all the strings we see and does the extra escaping
class EscapedString < String #:nodoc:
  def to_json(*)
    if Encoding.escape_html_entities_in_json
      super.gsub ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS
    else
      super.gsub ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS
    end
  end

  def to_s
    self
  end
end

上記EscapedStringクラスに値を入れ直して、newしています

Numeric, NilClass, TrueClass, FalseClassの場合には、as_jsonするだけになります

Hashクラスの場合もみてみます

Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }]

まずvalue.map { |k, v| [jsonify(k), jsonify(v)] } ではvalue(ハッシュ)のkey,valueをそれぞれjsonifyし、2次元配列を作っています
※ 2次元配列はこのようなデータです [['hoge', 'huga'],['piyo', 'poyo']]

メソッドの内部で再度同じメソッドを呼ぶことが不思議に思えるかもしれませんが、再帰といいます

※ 再帰とはどのようなものかはこちらの記事がわかりやすいかと思います
再帰呼び出し

mapはブロックの戻り値を配列化します

ブロックの内部ではハッシュのキー、バリューを jsonifyし配列を作成しています

Hash[] の処理を調べてみます

新しいハッシュを生成します。 引数は必ず偶数個指定しなければなりません。奇数番目がキー、偶数番目が値になります。

このメソッドでは生成するハッシュにデフォルト値を指定することはできません。 Hash.newを使うか、Hash#default=で後から指定してください。

[PARAM] key_and_value:

生成するハッシュのキーと値の組です。必ず偶数個(0を含む)指定しなければいけません。

[EXCEPTION] ArgumentError:

奇数個の引数を与えたときに発生します。

以下は配列からハッシュを生成する方法の例です。


省略


4) キーや値が配列の場合

alist = [[1,["a"]], [2,["b"]], [3,["c"]], [[4,5], ["a", "b"]]]
hash = Hash[alist] # => {1=>["a"], 2=>["b"], 3=>["c"], [4, 5]=>["a", "b"]}

引用:Ruby2.5.0:Hash

hashの各キー、バリューをjsonifyし、再度ハッシュを作り直すという処理になります

Arrayクラスの場合を見てみます
value.map { |v| jsonify(v) } 配列の各要素に対して jsonify しています

上記以外の場合は

jsonify value.as_json

引数を as_json しているので、上記クラスのどれかになるまで処理が繰り返されます

ここまでが to_json の処理になります

json.rb ではさきほど見てきた処理以外にもオープンクラスして as_json メソッドを定義している箇所があります
こちらについては別記事にまとめようと思います

読んでみて

json化する処理はよく使ったりするので、調べてみて内部で何をしているのか把握することは大切だと感じました