Railsのcofig_forメソッドでアプリケーションのカスタム値を設定してみる

こんにちは!kossyです!





今回はcofig_forメソッドでカスタム値を設定する方法について、ブログに残してみたいと思います。





環境
Ruby 3.0.3
Rails 6.1.4




使い方

# config/environments/development.rb

Rails.application.configure do
  config.custom_value = config_for(:custom_value)
end

上記のように設定を行なった場合、config/custom_value.yml を見に行って、その定義値を Rails.application.config.custom_value で参照することができます。

# config/custom_value.yml

default: &default
  status: custom

development:
  <<: *default
test:
  <<: *default
production:
  <<: *default

ではコンソールで試してみます。

# In Console

$ Rails.application.config.custom_value.status
=> "custom"

問題なくymlの値を呼び出すことができました。


OSSでの使われ方を見てみる

Railsを用いているOSSではどのように使われている見てみました。

github.com

# frozen_string_literal: true

require 'health_cards'

Rails.application.configure do
  config.smart = config_for('well-known')
  config.metadata = config_for('metadata')
  config.operation = config_for('operation')

  config.hc_key_path = ENV['KEY_PATH']
  FileUtils.mkdir_p(File.dirname(ENV['KEY_PATH']))
  kp = HealthCards::PrivateKey.load_from_or_create_from_file(config.hc_key_path)

  config.hc_key = kp
  config.issuer = HealthCards::Issuer.new(url: ENV['HOST'], key: config.hc_key)

  config.auth_code = ENV['AUTH_CODE']
  config.client_id = ENV['CLIENT_ID']
end

どうやらシンボルでも文字列でもconfig/配下のymlファイルを探しに行くようですね。

上記のアプリケーションでは、APIのエンドポイントや外部ライブラリのバージョンや返り値のフォーマット?をymlファイルに切り出して管理しているようでした。


config_for メソッドのソースコードを見てみる

まずはソースコードの定義位置を確認してみます。

# In Console

$ Rails.application.method(:config_for).source_location
=> [".../lib/rails/application.rb", 218]

こちらでした。

github.com

def config_for(name, env: Rails.env)
  yaml = name.is_a?(Pathname) ? name : Pathname.new("#{paths["config"].existent.first}/#{name}.yml")

  if yaml.exist?
    require "erb"
    all_configs    = ActiveSupport::ConfigurationFile.parse(yaml).deep_symbolize_keys
    config, shared = all_configs[env.to_sym], all_configs[:shared]

    if shared
      config = {} if config.nil? && shared.is_a?(Hash)
      if config.is_a?(Hash) && shared.is_a?(Hash)
        config = shared.deep_merge(config)
      elsif config.nil?
        config = shared
      end
    end

    if config.is_a?(Hash)
      config = ActiveSupport::OrderedOptions.new.update(config)
    end

    config
  else
    raise "Could not load configuration. No such file - #{yaml}"
  end
end

一行ずつ見てみましょう。

yaml = name.is_a?(Pathname) ? name : Pathname.new("#{paths["config"].existent.first}/#{name}.yml")

引数のnameがPathnameクラスのインスタンスであれば、nameをそのまま返し、

なければnameと同名のymlファイルをconfigディレクトリ配下から見つけてPathnameインスタンスにして返却するようになっていました。

なので、symbol or string or Pathname でもymlファイルを見つけられるみたいですね。

# 以下3つはどの書き方でもymlファイルを読み込むことができる

config_for('custom_value')
config_for(:custom_value)
config_for(Pathname.new('config/custom_value.yml'))

もしymlファイルが見つからなければ例外が上がりますね。

if yaml.exist?
  # 省略
else
  raise "Could not load configuration. No such file - #{yaml}"
end

$ Rails.application.config_for(Pathname.new('config/custom_value.yml'))
=> RuntimeError (Could not load configuration. No such file - config/custom_value.yml)

ymlファイルが存在する場合のコードを読んでみます。

require "erb"
all_configs    = ActiveSupport::ConfigurationFile.parse(yaml).deep_symbolize_keys
config, shared = all_configs[env.to_sym], all_configs[:shared]

こちらはコンソールで試してみますか。

$ all_configs = ActiveSupport::ConfigurationFile.parse(yaml).deep_symbolize_keys
=> {:default=>
  {:status=>"custom_value"},
 :development=>
  {:status=>"custom_value"},
 :test=>
  {:status=>"custom_value"},
 :production=>
  {:status=>"custom_value"},
}

$ config, shared = all_configs[env.to_sym], all_configs[:shared]
=> [{:status=>"custom_value"}, nil]

ymlで定義した値をシンボル化して、Rails.envと合致するシンボルを持つ値をconfigとして変数に格納し、

sharedというシンボルの値があればそちらも変数にしていました。(私の環境ではsharedは設定していないので nil が返っています)

もしsharedが存在していれば、という処理も記載されていましたが、今回のコードリーディングのスコープからは除外します。

次は ActiveSupport::OrderedOptions 周りを読んでみます。

if config.is_a?(Hash)
  config = ActiveSupport::OrderedOptions.new.update(config)
end

ドキュメントを見てみます。

api.rubyonrails.org

OrderedOptions inherits from Hash and provides dynamic accessor methods.

With a Hash, key-value pairs are typically managed like this:

h = {}
h[:boy] = 'John'
h[:girl] = 'Mary'
h[:boy]  # => 'John'
h[:girl] # => 'Mary'
h[:dog]  # => nil

Using OrderedOptions, the above code can be written as:

h = ActiveSupport::OrderedOptions.new
h.boy = 'John'
h.girl = 'Mary'
h.boy  # => 'John'
h.girl # => 'Mary'
h.dog  # => nil

出典: https://api.rubyonrails.org/classes/ActiveSupport/OrderedOptions.html

使い方は理解できました。便利ですね。

updateメソッドはどうやら定義されていないようなので、method_missingメソッドで捕捉されているものと思われます。

config = ActiveSupport::OrderedOptions.new.update(config)

の実行結果は以下です。

$ config = ActiveSupport::OrderedOptions.new.update(config)
=> { :status=>"custom_value"}

結局config_forメソッドの返り値は configが返っていました。

まとめ

settingslogicやfigaro等でアプリケーションの設定値を管理するのがメジャーかと思っていたんですが、config_forでも全然良さそうですね。