Rubyで、Hashのkeyに対して再帰的に変換するクラスを、いい感じに実装してみた
はじめに
JSとRailsのデータの受け渡しの際に、Rubyは基本的に変数をsnake_caseで書くのに対し、JSではcamel_caseで書くことが多いと思います。
何らかの形で変換するか、どちらかのコードで混在してしまうことを妥協しないといけません。
今回は、Ruby側ですべて明示的に変換することにしました。
かなり普遍的に使えそうなコードが良い感じに書けたので、共有します。
もしよろしければ参考にしていただければ幸いです。
環境
ActiveSupportが導入されており、ruby2.6系ならば動くと思います。
実装
# frozen_string_literal: true # NOTE: keyが衝突する場合の動作は未定義なので注意 # NOTE: keyはStringに、HashのサブクラスはHashに、それぞれ暗黙の型変換が起こる class HashKeyConverter class << self def to_snake_case_recursively(object) key_change_recursively(object, ->(key) { key.to_s.underscore }) end def to_camel_case_recursively(object, first_letter = :lower) key_change_recursively(object, ->(key) { key.to_s.camelize(first_letter) }) end private def key_change_recursively(object, lambda_exp) if object.is_a?(Hash) object.to_h { |k, v| [lambda_exp.call(k), key_change_recursively(v, lambda_exp)] } elsif object.is_a?(Array) object.map { |e| key_change_recursively(e, lambda_exp) } else object end end end end
解説
見たままではあるのですが、軽く解説します。
- key_change_recursivelyというメソッドで、lambdaオブジェクトを再帰的にkeyに対して適用していきます
- lambdaオブジェクト中のto_sはsymbolなどが与えられたときのための暗黙の型変換です(symbolは変換しないなどの場合は、lambdaオブジェクトの定義を変更します)
- atcive_supportを使い慣れている人が使いやすいように、camelizeする際の引数で、lowerとupperを選べるようにしています
テストコード
動作がわかりにくい方のために、rspecで書いたテストも貼っておきます。(長いので折りたたんでいます)
テストコード
# frozen_string_literal: true RSpec.describe HashKeyConverter do describe '#to_snake_case_recursively' do context 'HashでもArrayでもない場合' do res = HashKeyConverter.to_snake_case_recursively('hoge') it { expect(res).to eq 'hoge' } end context 'Hashの場合' do res = HashKeyConverter.to_snake_case_recursively( 'hoge' => 'foo', 'hoge_fuga' => 'foo_bar', 'fugaHoge' => 'fooBar', 'PiyoPiyo' => 'FooBar' ) it do expect(res).to eq( 'hoge' => 'foo', 'hoge_fuga' => 'foo_bar', 'fuga_hoge' => 'fooBar', 'piyo_piyo' => 'FooBar' ) end end context 'Hashのkeyが文字列以外の場合' do res = HashKeyConverter.to_snake_case_recursively( 1 => true, true => 1, nil => false, hogeFuga: 'foo_bar' ) it do expect(res).to eq( '1' => true, 'true' => 1, '' => false, 'hoge_fuga' => 'foo_bar' ) end end context 'ネストされたハッシュの場合' do res = HashKeyConverter.to_snake_case_recursively( 'hogeFuga' => { 'hogeFuga' => { 'hogeFuga' => { 'hogeFuga' => 'foo_bar' } } } ) it do expect(res).to eq( 'hoge_fuga' => { 'hoge_fuga' => { 'hoge_fuga' => { 'hoge_fuga' => 'foo_bar' } } } ) end end context 'HashWithIndifferentAccessの場合' do res = HashKeyConverter.to_snake_case_recursively( { 'hogeFuga' => 1 }.with_indifferent_access ) it { expect(res).to eq('hoge_fuga' => 1) } end context 'Arrayの場合' do res = HashKeyConverter.to_snake_case_recursively( [ { 'hogeFuga' => 'foo_bar' }, [1, true, nil], { 'hogeFuga' => [1, [[]], { 'hogeFuga' => [] }.with_indifferent_access] } ] ) it do expect(res).to eq( [ { 'hoge_fuga' => 'foo_bar' }, [1, true, nil], { 'hoge_fuga' => [1, [[]], { 'hoge_fuga' => [] }] } ] ) end end end describe '#to_camel_case_recursively' do context 'HashでもArrayでもない場合' do res = HashKeyConverter.to_camel_case_recursively('hoge') it { expect(res).to eq 'hoge' } end context 'Hashの場合' do res = HashKeyConverter.to_camel_case_recursively( 'hoge' => 'foo', 'hoge_fuga' => 'foo_bar', 'fugaHoge' => 'fooBar', 'PiyoPiyo' => 'FooBar' ) it do expect(res).to eq( 'hoge' => 'foo', 'hogeFuga' => 'foo_bar', 'fugaHoge' => 'fooBar', 'piyoPiyo' => 'FooBar' ) end end context 'Hashのkeyが文字列以外の場合' do res = HashKeyConverter.to_camel_case_recursively( 1 => true, true => 1, nil => false, hoge_fuga: 'foo_bar' ) it do expect(res).to eq( '1' => true, 'true' => 1, '' => false, 'hogeFuga' => 'foo_bar' ) end end context 'ネストされたハッシュの場合' do res = HashKeyConverter.to_camel_case_recursively( { 'hoge_fuga' => { 'hoge_fuga' => { 'hoge_fuga' => { 'hoge_fuga' => 'foo_bar' } } } }, :upper ) it do expect(res).to eq( 'HogeFuga' => { 'HogeFuga' => { 'HogeFuga' => { 'HogeFuga' => 'foo_bar' } } } ) end end context 'HashWithIndifferentAccessの場合' do res = HashKeyConverter.to_camel_case_recursively( { 'hoge_fuga' => 1 }.with_indifferent_access ) it { expect(res).to eq('hogeFuga' => 1) } end context 'Arrayの場合' do res = HashKeyConverter.to_camel_case_recursively( [ { 'hoge_fuga' => 'foo_bar' }, [1, true, nil], { 'hoge_fuga' => [1, [[]], { 'hoge_fuga' => [] }.with_indifferent_access] } ], :upper ) it do expect(res).to eq( [ { 'HogeFuga' => 'foo_bar' }, [1, true, nil], { 'HogeFuga' => [1, [[]], { 'HogeFuga' => [] }] } ] ) end end end end