[Ruby] ERBを埋め込んだYAMLファイルを読み込む(Railsのdatabase.ymlみたいに!)
ERBを埋め込んだYAMLファイルを読み込むためのモジュールを作成します。
設定ファイルとしてYAMLファイルを用意した場合に便利です。
この記事の要点
-
以下の感じで実装できます。パッと見でよくわからない場合は、実装・実行例のところを見てください
require "yaml" require "erb" module ERBEmbeddedYAML module_function def load_file(yaml_path) erb_str = File.read(yaml_path) yaml = ERB.new(erb_str).result YAML.load(yaml) end end
-
Railsのrails/railties/lib/rails/application/configuration.rbの
load_database_yaml
辺りが参考になります
使い道
YAMLで設定ファイルを書く場合に...
- 本番環境のDBの接続情報など、秘匿すべき情報を環境変数としてセットする
- 設定ファイルは共有バージョン管理リポジトリ(GitHubなど)に管理したりするので、秘匿情報をそのまま直書きすると危険。そのため、環境変数を使ってプログラム実行時に値を渡すようにする
- 「今日から30日前」など、プログラム実行時に動的に決まる値をパラメータとしてセットする
などなど。
Ruby on Railsの例
例えばRuby on Railsでは、config/database.ymlにて
- 開発・テスト環境のDB接続情報は直書き
- 本番環境のDB接続情報は、環境変数から取得
という風になっています。
# 本題と関係ないので以下例では省略していますが、
# 開発・テスト環境DBのpassword, urlは、Rails側がよしなにしてくれます。
development:
...
database: sample_rails_app_development
...
test:
...
database: sample_rails_app_test
...
production:
...
database: sample_rails_app_production
username: sample_rails_app
password: <%= ENV['SAMPLE_RAILS_APP_DATABASE_PASSWORD'] %>
url: <%= ENV['SAMPLE_RAILS_APP_DATABASE_URL'] %>
Rubyファイルでよくない?
確かにrubyファイルでも同様のことはできますが、
- 「パラメータの設定ファイルである」用途が明確で、利用者が戸惑いにくい
- 利用者が余計なことをしにくい(「いじるべきはパラメータの値部分」と認識しやすいので)
という「いい意味で制限できる」点で、ERB埋め込みのYAMLの方がいいかなと個人的に思っています。
実装・実行サンプル
単純な実装サンプルとして、ユーザに挨拶をし、指定した期間の間に日曜日が何回あるかを教えてくれるプログラムを作成します(いいネタが思いつきませんでした)。
下記のような構成とします。
.
├── config
│ ├── dates.yml # <- いつからいつまで、を設定する
│ └── username.yml # <- usernameを設定する(開発・本番環境それぞれ設定)
├── lib
│ ├── config_loader.rb # <- 各config/*.yml専用の読み込みメソッド群を定義したモジュール
│ └── erb_embedded_yaml.rb # <- ERBを埋め込んだ任意のYAMLファイルを読み込むモジュール
└── main.rb
ERBを埋め込んだYAMLファイルを読み込むモジュール
この記事の中で、一番意味のある部分です。
ERBが埋め込まれた文字列としてYAMLファイルを読み込み (File.read(yaml_file)
)、ERBを実行して最終的に得られたYAML文字列 (ERB.new(erb_str).result
)を、YAML.load
でHashなどYAMLの中身に応じたオブジェクトに変換します。
require "yaml"
require "erb"
module ERBEmbeddedYAML
module_function
def load_file(yaml_file)
# rails/railties/lib/rails/application/configuration.rbを参考にした
erb_str = File.read(yaml_file)
yaml = ERB.new(erb_str).result
YAML.load(yaml)
end
end
ちなみに、読み込んだYAMLファイルが空っぽの場合は、YAML文字列が空文字""
になりますが、この空文字を引数として渡したYAML.load("")
の結果は、空文字""
ではなくfalse
になります。
(Rubyリファレンスマニュアル Psych.load
;YAMLオブジェクトは実際はPsychオブジェクトなので、リンク先がPsych.load
になっています。)
これは、a || b
(a
がnil
もしくはfalse
の時だけb
, それ以外はa
を返す)の書き方を利用して、以下のようにデフォルト値を簡単に設定できるからだと思います。
loaded = YAML.load(yaml) || {} # YAML.load(yaml)がfalseの場合は、{}を代入する
今回の例では、Hashを返すYAMLのみを前提としているわけではないため使っていませんが、前述のRailsのコード中では使われているので、一度確認してみてください。
ERBを埋め込んだYAMLファイル
config/ディレクトリ配下に置いています。
dates.yml
いつから (start_at), いつまで (end_at) を設定します。
プログラム実行時に動的に変わる値Date.today
を使う例です。
-
ConfigLoader.dates
で読み込む- Dateクラスのオブジェクトか、
Date.strptime
で解釈できる文字列を設定できる
- Dateクラスのオブジェクトか、
# --------------------------------------------------
# * 特定の日時を指定したい場合は、
# end_at: 2030-01-30
# のように、YYYY-mm-ddの書式で指定すること。
#
# * start_atはend_atより前の日を指すようにすること。
# --------------------------------------------------
start_at: <%= Date.today + 1 %>
# end_at: <%= Date.today + 30 %>
end_at: 2030-01-30 # <- 日付指定で書き換えている
Dateオブジェクトが書かれているだけだとわざわざ設定ファイルに切り出す意義がわかりづらいので、「特定の日時でend_atを書き換えた」パターンの内容にしています。
また、面倒くさいので「start_atをend_atより前の日にする」のはユーザに任せています(エラー処理を書いていない)。
username.yml
挨拶するユーザ名 (username) を設定します。
環境変数の値を使う例です。
-
ConfigLoader.username
で読み込む- 環境変数APP_ENVに指定する実行環境によって、読み込まれる設定が変わる(ように読み込みモジュールを書く)ので、開発環境と実行環境両方のusername設定を記載する
- 開発環境 (development) の場合は、developer
- 本番環境 (production) の場合は、環境変数APP_USERNAMEに設定した文字列
- 環境変数APP_ENVに指定する実行環境によって、読み込まれる設定が変わる(ように読み込みモジュールを書く)ので、開発環境と実行環境両方のusername設定を記載する
development:
username: developer
production:
username: <%= ENV['APP_USERNAME'] %>
(プログラム的に、設定ファイルにする意味は全然ないですが、サンプルということで無理やりです。)
config/配下の各設定YAMLファイルを読み込むモジュール
コードが長いですが、長さのわりに大したことはしていないので、まずは以下だけ認識して、次のmain.rbと実行結果を見た方がよいです。
- config/配下の各設定YAMLファイル(dates.yml, username.yml)に対し、
ConfigLoader
モジュール内に、それぞれ専用の読み込みメソッドを定義している-
ConfigLoader.dates
でconfig/dates.ymlを読み込み、{"start_at" => #<Date: 2021-02-18>, "end_at" => #<Date: 2030-01-30>}
のようなHashを返す -
ConfigLoader.username
でconfig/username.ymlを読み込み、環境変数APP_ENVの値によってセクションを選択して、{"username" => "developer"}
のようなHashを返す
-
lib/config_loader.rbの補足
-
config_switched_by_env
は、環境変数APP_ENVで指定された実行環境に合わせたconfigを抜き出します-
ConfigLoader.username
で使われています
-
-
daten
は、config/dates.ymlのstart_at, end_atをDateクラスに変換します- 名前は適当です
- 設定YAMLファイルというよりはconfig/dates.yml特有の事情なので、本当は
ConfigDates
クラスのようなものを定義して、その中でデータの性質として処理した方が見通しが良いと思います
-
ConfigLoader
モジュール内にconfig/*.ymlを読み込むメソッドを一括で定義するので、YAMLファイルのパスをここにハードコードしてもいいかな、という気持ちです。
require "date"
require_relative "erb_embedded_yaml.rb"
module ConfigLoader
def self.config_switched_by_env(loaded_config)
# 環境変数APP_ENVで指定された実行環境に合わせたconfigを抜き出す
# 環境変数APP_ENVの値を変数app_envにセット;指定がない場合は"development"
app_env = ENV['APP_ENV'] || "development"
if app_env.downcase == "production"
loaded_config["production"]
else # 無効な値の場合も含め、デフォルトではdevelopmentのセクションを返す
loaded_config["development"]
end
end
def self.daten(obj)
# objがStringならばDateに変換、そうでないならば(元からDateと仮定して)そのまま
if obj.class == String
Date.strptime(obj)
else
obj
end
end
module_function
def dates
config_path = "config/dates.yml"
config = ERBEmbeddedYAML.load_file(config_path)
# start_at, end_atをDateクラスに揃えておく
config["start_at"] = daten(config["start_at"])
config["end_at"] = daten(config["end_at"])
config
# cf.) -> {"start_at" => #<Date: 2021-02-18>, "end_at" => #<Date: 2030-01-30>}
end
def username
config_path = "config/username.yml"
loaded_config = ERBEmbeddedYAML.load_file(config_path)
# 実行環境APP_ENVにしたがって、パラメータを抜き出す
config = config_switched_by_env(loaded_config)
# APP_ENV=productionの場合は、APP_USERNAMEでusernameを指定しなければならない
# ...が、その指定がないときはnilになっているので、"nobody"にする
config["username"] = config["username"] || "nobody"
config # cf.) -> {"username" => "developer"}
end
end
メインプログラムmain.rb
YAMLファイルに設定した値は、lib/config_loader.rbのConfigLoader.dates
, ConfigLoader.username
経由で取得しています。
require_relative "lib/config_loader.rb"
require "date"
def main
dates = ConfigLoader.dates
# cf.) -> {"start_at" => #<Date: 2021-02-18>, "end_at" => #<Date: 2030-01-30>}
start_at = dates["start_at"] # -> Date
end_at = dates["end_at"] # -> Date
username = ConfigLoader.username["username"] # -> String
# start_atからend_atの間に存在する日曜日を集める
sundays = (start_at..end_at).select { |date| date.sunday? } # -> Array of Date
# 出力
puts "こんにちは、#{username}さん"
puts "#{start_at}から#{end_at}の間には、日曜日が#{sundays.size}回あります!"
puts "直近だと、"
sundays[0...3].each do |date|
puts " #{date}"
end
puts "ですね!"
end
# 実行
main
本筋とはあまり関係ないですが、
-
start_at..end_at
は、DateオブジェクトからなるRangeオブジェクトです(..
なので、終端end_at
を含む範囲) -
出力のところの
sundays[]
の引数0...3
も、IntegerオブジェクトからなるRangeオブジェクトです(...
なので、終端3
を含まない範囲)RangeやRange#selectの部分がいまいちピンとこない方向け補足
Rubyリファレンスマニュアル Rangeクラスを見て、コード例やその改造版をirbで実行してみるとよくわかると思います。
start_at..end_at
やRange#select
(Enumetableから継承しているメソッドなので本当はEnumerable#select
)の部分は、例えば以下をirbなどで実行したりいじったりしてみてください。(rubyファイルに保存して実行した方が見やすいかも。)require "date" start_at = Date.new(2021, 4, 1) end_at = Date.new(2021, 4, 14) date_range = start_at..end_at date_range.each { |date| puts "[#{date}] date.sunday: #{date.sunday?}" } sundays = date_range.select { |date| date.sunday? } puts "sundays: #{sundays}"
-
この
main
メソッドを定義するやり方は、伊藤淳一さんの「Rubyスクリプトにもmainメソッドを定義するといいかも、という話」という記事を参考にしています。
(今回は内部でのメソッド切り出しもしていないですが、あとで分けようとなる場合に面倒くさくなるので、初めからmain
メソッドを定義しています。)
実行例
環境変数でusernameをコントロールするようにしたので、幾つかの状況で実行する例を載せます。これ以外にも、APP_ENV=3で実行など、いじって実験してみてください。
(あまり有意義なプログラムではないので、特筆事項は特にないです。数にすると日曜日が意外と少なく見えることくらいかな...)
環境変数をセットしないで実行
- config/username.ymlのdevelopmentセクションを使用
=> usernameはdevelopment
$ ruby main.rb
こんにちは、developerさん
2021-02-18から2030-01-30の間には、日曜日が467回あります!
直近だと、
2021-02-21
2021-02-28
2021-03-07
ですね!
環境変数APP_ENVをproduction, APP_USERNAMEをIchiroにして実行
- config/username.ymlのproductionセクションを使用
=> usernameはAPP_USERNAMEから読み込み、Ichiroに
$ APP_ENV=production APP_USERNAME=Ichiro ruby main.rb
こんにちは、Ichiroさん
2021-02-18から2030-01-30の間には、日曜日が467回あります!
直近だと、
2021-02-21
2021-02-28
2021-03-07
ですね!
環境変数APP_ENVをproduction, APP_USERNAMEは指定なしで実行
- config/username.ymlのproductionセクションを使用
=> usernameはAPP_USERNAMEから読み込むが、指定がないのでConfigLoader.username
内でnobodyに
$ APP_ENV=production ruby main.rb
こんにちは、nobodyさん
2021-02-18から2030-01-30の間には、日曜日が467回あります!
直近だと、
2021-02-21
2021-02-28
2021-03-07
ですね!
おわりに
Railsに感謝
YAMLにERBを埋め込むやり方はとても便利なので、教えてくれたRailsにはとても感謝です。
この記事のモチベーションの半分くらいは、Railsのどこに読み込みメソッドが定義されているかをメモしておくことにあります笑
もっといい設定ファイルのやり方があれば、是非教えてください!
私の場合、普通のRubyプロジェクトでも、
- config/ディレクトリを作成してsmtp.yml, database.ymlなど接続情報を実行環境別に用意
- 開発・テスト環境の接続情報は(ローカルの場合は)直書き
- 本番環境の接続情報は、環境変数で受け取る
- config/*.ymlを(実行環境に合わせて)読み込むモジュールメソッドを、ひとつの
ConfigLoader
のような名前のモジュール内にまとめて、YAMLファイル別に作成- データの性質が複雑な時は、Structもしくはクラスを作成
- YAMLのファイルパスは、専用読み込みモジュールメソッドかクラス内にハードコード
という、今回のサンプルプロジェクトのような構成をよく取ります。
ただ、Railsの形を借りてきた自己流なので、特に設定ファイルの読み込み方に関して「こっちのほうがいいよ」「メジャーなのはこのやり方だよ」「このやり方はこの点でまずいよ」というのがありましたら、ご指摘いただけると嬉しいです。
Discussion