普段仕事で使っている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}
レシーバに対して to_json
すると、JSON文字列を返すという処理です
ソースコードを読んでみる
1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def to_json
を探してみます
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
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"]}
hashの各キー、バリューをjsonifyし、再度ハッシュを作り直すという処理になります
Arrayクラスの場合を見てみます
value.map { |v| jsonify(v) }
配列の各要素に対して jsonify
しています
上記以外の場合は
jsonify value.as_json
引数を as_json
しているので、上記クラスのどれかになるまで処理が繰り返されます
ここまでが to_json
の処理になります
json.rb ではさきほど見てきた処理以外にもオープンクラスして as_json
メソッドを定義している箇所があります
こちらについては別記事にまとめようと思います
読んでみて
json化する処理はよく使ったりするので、調べてみて内部で何をしているのか把握することは大切だと感じました