Refinementを使って、安全にActiveSupportにモンキーパッチを当てて、Hash#to_jsonの結果を書き換える
環境
やりたかったこと
Rails上で
{today: Date.today}.to_json
とすると
{"today":"2019-02-20"}
となるが、ハッシュの要素が増えたときでも、strftime('%Y年%m月%d日')
みたいなのをできる限り書かないで(DRYに保ったままで)
{"today":"2019年02月20日","yesterday":"2019年02月19日"}
としたい。が、副作用が怖いので、単純なオーバーライドはしたくない。Refinementで済ませたいが、調べても方法が分からなかったので、自己解決。
まずは単純にオーバーライドして方針を確かめる
ActiveSupportのメソッドを読み進めていくと、active_support/core_ext/object/json.rb
で定義されているHash#as_json
メソッド内で
def as_json(options = nil) subset = if options : : else self end Hash[subset.map { |k, v| [k.to_s, options ? v.as_json(options.dup) : v.as_json] }] end
のように、valueに対してas_jsonメソッドを呼んでいる。valueにDateオブジェクトが入った時に、active_support/core_ext/object/json.rb
でDateオブジェクトに定義されている以下のメソッドが実行される。
class Date def as_json(options = nil) if ActiveSupport::JSON::Encoding.use_standard_json_time_format strftime("%Y-%m-%d") else strftime("%Y/%m/%d") end end end
strftimeの引数がハードコーディングされている。悲しい。 ひとまず、オープンクラスを使って、
class Date def as_json(options = nil) strftime("%Y年%m月%d日") end end
としてやれば、やりたいことはとりあえず達成される。 しかし、別のクラスでは別の形式を使いたいときに困ることが予想される上、やはりActiveSupportのモンキーパッチは危険な気がするため、できる限り避けたいですよね。
思考停止でRefinementを使ってみる(これは罠で失敗しました)
module M refine Date do def as_json(options = nil) strftime("%Y年%m月%d日") end end end
として、使いたいメソッド内でusing Mとしても、うまく行かない。
Refinementはusingと同じレキシカルスコープにしか効力を発さないので、直接Date#as_json
を呼び出しているところでusingを呼ばない限りはrefineできない。
あくまでも、今回のような場合はHash#to_json
をrefineすることしかできない。(勉強になりました)
Refinementを複数使ってみる
Date#as_json
を直接呼び出しているHash#as_json
をrefineする。Hash#as_json
を直接呼び出しているActiveSupport::JSON::Encoding::JSONGemEncoder
をrefineする……と繰り返していけばいいのではないかと考えた。これについてはコードを見ていただいた方が早いと思います。
module M1 refine Date do def as_json(options = nil) : : end end end module M2 refine Hash do using M1 def as_json(options = nil) : : end end end module M3 refine ActiveSupport::JSON::Encoding::JSONGemEncoder do using M2 def encode(value) stringify jsonify value.as_json(options.dup) end end end module M4 refine ActiveSupport::JSON.singleton_class do using M3 def encode(value, options = nil) ActiveSupport::JSON::Encoding.json_encoder.new(options).encode(value) end end end module M5 refine Hash do using M4 def to_json(options = nil) : : end end end
ActiveSupport::JSONのクラスメソッドをrefineする際には、ActiveSupport::JSONの特異クラスをrefineすることに気をつければ、面倒ではありますが、それほど難しいことはしていません。(ソースコードを読むときは、pryのshow-methodを使うととても便利です。今回初めて知りました)
結論
5時間くらいかかってしまったが、これでうまくいった。が、保守性は下がっている気もするので、マージするかは要検討中です。