♟️

RailsでSettingslogicから最小限の変更でconfig_forに移行する

2023/11/01に公開

背景

ハコベル株式会社システム開発部 一般貨物運送手配システムG の坂東です。普段はサーバーサイドエンジニアとして、ハコベル配車管理の開発を行っています。

https://www.hacobell.com/dispatch

ハコベル配車管理のバックエンド処理は、Ruby on Railsで構築されています。このRailsアプリケーションにおける諸々の設定管理には、従来はSettingslogicというgemを使用していました。

https://github.com/settingslogic/settingslogic

Settingslogicではアプリケーションの設定を簡単に取得し、利用することができます。

具体的には以下のようなyamlファイルを用意し、

# config/application.yml
defaults: &defaults
  hoge: 1
  foo:
     bar: "sample"

次のようなSettingsクラスを用意して、設定ファイルを読み込みます。

# app/models/settings.rb
class Settings < Settingslogic
  source Rails.root.join('config', 'application.yml')
  namespace ::Rails.env
end

すると以下のようなメソッドチェーンで、ネストした設定値にアクセスすることができるようになります。

Settings.hoge #=> 1

Settings.foo.bar #=> "sample"

しかしRubyのバージョンアップに伴い、この方法ではエラーが出るようになりました。Ruby 3.1からはPsychというyamlを読み込むgemに破壊的変更があり、Settingslogicが正常に動作しないのです。

詳しくは、こちらの記事が参考になります。
Rails 6.1のままRuby 3.2にアップデートし、YJITの恩恵を受ける

なお、この記事では以下のバージョンを仮定します。

  • Ruby: 3.2.2
  • Rails: 6.1.7

課題

Settingslogicにパッチを当てて運用を続けることも考えましたが、最新コミットが執筆時点では9年前で使い続けるのに不安があったため、今回は別の方法を考えることにしました。

チームの状況を踏まえると、アプリケーション影響やQAの工数を最小限に抑えたかったので、従来の使用感を変えずに最小限の変更で移行できることを重視して対応しました。

選択肢としては以下の2つを考えました。

  • Rails標準のconfig_forを使う
  • その他のgemを使う

Settingslogicからその他のgemに移行するにしてもメンテナンス切れの問題はついて回るし、Rails標準の機能で実現できればベストと考えたため、前者の方法を採用しました。

しかし1つ問題がありました。素直にconfig_forを使用するとHash likeな使い方になってしまうため、従来のメソッドチェーンでの呼び方から書き換える必要があるのです。

例えば先ほどのyamlファイルをconfig_forで読み出すと、以下のように値を参照することになります。

config = Rails.application.config_for(Rails.root.join('config/application.yml'))

config.hoge #=> 1

config.foo[:bar] #=> "sample"

解決策

この問題を解決するため、Settingsクラスの実装を工夫しました。

以下が実装例です。

class Settings
  def self.load_config
    config ||= convert_to_ordered_options(Rails.application.config_for(Rails.root.join('config/application.yml')))
    config.each_key do |key|
      define_singleton_method(key) do
        config[key]
      end
    end
  end

  def self.convert_to_ordered_options(hash)
    ordered_options = ActiveSupport::OrderedOptions.new

    hash.each do |key, value|
      ordered_options[key] = if value.is_a?(Hash)
                               convert_to_ordered_options(value)
                             else
                               value
                             end
    end

    ordered_options
  end

  private_class_method :convert_to_ordered_options
  private_class_method :load_config

  load_config
end

この実装では、Settings クラスに load_config メソッドを導入し、設定の読み込みとメソッドの生成を行っています。

ActiveSupport::OrderedOptionsを使用すると、ハッシュの要素をメソッドで取得できるようになるのがポイントです。

convert_to_ordered_options メソッドでは、Rails.application.config_forでハッシュとして読み込まれたyamlファイルの内容を、再帰的に OrderedOptions に変換します。

そしてconfigのkeyに対してdefine_singleton_methodを適用することで、Settingsクラスに対してconfigのkeyに対応したメソッドを生成できます。

このようにして、ネストした設定値をSettingsクラスのメソッドチェーンで呼び出すことが可能になります。

Settings.hoge #=> 1

Settings.foo.bar #=> "sample"

よって当初の目的通り、Settingslogicに近い使用感のまま、Settingsクラスを移行することができました!

まとめ

この記事では、Rails標準のconfig_forを使いつつ、メソッドチェーンで設定値を読み出すための実装の工夫についてご紹介しました。

もし既存のrailsアプリケーションでSettingslogicを使っていて、gemの移行を考えている方はぜひ参考にしてみてください。

参考文献

Rails 6.1のままRuby 3.2にアップデートし、YJITの恩恵を受ける

Settingslogicの置き換えを狙ったSettingsCabinetを発明した

Hacobell Developers Blog

Discussion