Ruby 1.8系のHashは順列を保持しないため、Hashを含むデータをJSONで出力しようとすると、実行するたびに並び順の違うJSONが出力されてしまいます。しかしそれだと、テストがやりづらかったり、本来同一内容のファイルが違う内容になってしまったり、いろいろ面倒です。
諸事情ありRuby 1.9系にバージョンを上げたくなかったので、Ruby 1.8系のままsort by keyなJSONをdump出来るようにしてみました。
サンプルコード
手軽にデータを準備...ということで、YAMLをJSONに変換するコードで解説。
yaml2json.rb
#!/usr/bin/env ruby -w
require 'rubygems'
require 'json/pure'
require 'yaml'
module JSON::Pure::Generator::GeneratorMethods::Hash
remove_method :json_transform
def json_transform(state)
delim = ','
delim << state.object_nl
result = '{'
result << state.object_nl
depth = state.depth += 1
first = true
indent = !state.object_nl.empty?
keys.sort.each { |k|
result << delim unless first
result << state.indent * depth if indent
result << k.to_s.to_json(state)
result << state.space_before
result << ':'
result << state.space
result << self[k].to_json(state)
first = false
}
depth = state.depth -= 1
result << state.object_nl
result << state.indent * depth if indent if indent
result << '}'
result
end
end
puts JSON.dump(YAML.load(ARGF.read))
ポイント
jsonモジュールはデフォルト動作はCで書かれた拡張版で動作しますが、json/pureを明示的に指定することで、Rubyで書かれたpure版を使う事が出来ます。Rubyであれば既存のモジュールのメソッドを上書きすることが出来るので、Hashをまさにeachしているこいつのコードを書き換えてsortするようにしてしまいます。先にremove_methodしているのは、これをしないで再定義すると"warning: method redefined; discarding old json_transform"と警告が出てしまうので、お行儀を良くするために。
これでJSON中に含まれる全てのHashはキーでソートされるので、実行毎に出力結果が変わることが無くなります。