YAMLに正規表現を書く方法(Rails・!ruby/regexpタグ)

8分で読めるテック

YAMLファイルに正規表現を書くには、 !ruby/regexp タグを使います。これを知るまで少し詰まったので、使い方と動作確認の手順をまとめます。

結論

regexp: !ruby/regexp /^[a-zA-Z\s]+$/

!ruby/regexp に続けて正規表現を書くだけです。Railsで読み込むとRubyのRegexpオブジェクトとして扱えます。

なぜYAMLに正規表現を書きたかったか

複数の国・企業向けに同じRailsアプリを展開していたことがあります。コードは共通のまま、環境ごとに読み込むYAMLファイルを切り替えることで細かな設定を調整する方式でした。文字列やフラグの値はすぐYAMLに書けましたが、URLバリデーション用の正規表現を環境ごとに変えたい場面で書き方が分からなくなりました。

YAMLは文字列・整数・真偽値・日付などを自動判別してくれますが、正規表現は対象外です。/^$/と書いてもただの文字列として読み込まれてしまいます。

# これはただの文字列になる
pattern: /^[a-zA-Z]+$/
Settings.pattern.class
# => String

バリデーションで使おうとして =~ にかけると、StringString の比較になるので期待どおりには動きません。

!ruby/regexpタグの仕組み

YAMLはデータのシリアライズ形式として、型情報を「タグ」で表現できます。タグは ! で始まり、デシリアライズ時にどのRubyクラスとして復元するかを指定します。

YAML仕様では !!str!!int!!bool などのコアタグが定義されていますが、Ruby固有のクラスには !ruby/ プレフィックスを持つタグが割り当てられています。

- !str 123              # 文字列
- !ruby/sym foo         # Ruby の Symbol
- !ruby/regexp /^[a-z]+$/  # Ruby の Regexp
- !ruby/object:MyClass  # 任意のRubyオブジェクト

正規表現の場合は!ruby/regexpに続けて/パターン/を書きます。フラグも使えます。

regexp_with_flag: !ruby/regexp /^[a-z]+$/i

i(大文字小文字無視)、m(複数行)、x(コメント許可)など、Rubyが対応するフラグをそのまま使えます。

Railsでの読み込み例

YAML.load_fileで直接読む場合

path = # YAMLファイルへのパス
config = YAML.load_file(path).symbolize_keys

config gemのSettings経由で読む場合

自分が使っていたプロジェクトでは、config gemを使ってSettingsオブジェクトに設定を集約していました。

path = # YAMLファイルへのパス
Settings.add_source!(path)
Settings.reload!

YAMLファイルは次のように書きます。

reg:
  sample_regexp: !ruby/regexp /^[a-zA-Z\s]+$/

動作確認

型指定あり・なしを並べて確認しました。

regexp: !ruby/regexp /^$/
string: /^$/

Railsコンソールで値とクラスを確認するとこうなります。

[1] pry(main)> Settings.regexp
=> /^$/
[2] pry(main)> Settings.regexp.class
=> Regexp
[3] pry(main)> Settings.string
=> "/^$/"
[4] pry(main)> Settings.string.class
=> String

!ruby/regexpをつけた方はRegexp、つけない方はStringとして読み込まれているのが分かります。正規表現マッチで使うにはRegexpでないといけないので、タグの指定が必要です。

実際にバリデーションで使うとこうなります。

# Regexp オブジェクトとして使える
pattern = Settings.reg.sample_regexp
"hello world" =~ pattern  # => 0(マッチ)
"hello123"    =~ pattern  # => nil(マッチしない)

# String のままだと意図しない動作になる
pattern_str = Settings.string
"hello" =~ pattern_str  # String#=~ は右辺が Regexp であることを期待する

YAML.load と YAML.safe_load の違いに注意

!ruby/regexp を含むYAMLを読み込む際に YAML.safe_load を使うとエラーになります。

content = "regexp: !ruby/regexp /^[a-z]+$/"

# これはエラーになる
YAML.safe_load(content)
# => Psych::DisallowedClass: Tried to load unspecified class: Regexp

# こちらは動く
YAML.load(content)
# => {"regexp"=>/^[a-z]+$/}

YAML.safe_load はデフォルトで StringIntegerFloatTrueClassFalseClassNilClassArrayHash のみを許可しており、任意のRubyオブジェクトの復元を防ぐセキュリティ上の制限があります。

ユーザーが入力したYAMLや信頼できないソースのYAMLを読む場合は safe_load が推奨されます。一方、自分たちで管理するコード内の設定ファイルを読む場合は、permitted_classes オプションで許可クラスを追加するか load を使うことになります。

# Ruby 3.1以降は permitted_classes で明示的に許可する書き方が推奨
YAML.safe_load(content, permitted_classes: [Regexp])

Ruby 3.1 / Psych 4.0 以降では YAML.load 自体もデフォルトで safe_load 相当の挙動になっています(任意クラスのデシリアライズを禁止)。permitted_classes キーワード引数はこのバージョンから安定して使える形式です。それ以前(Psych 3.x)では引数名が allowed_classes または位置引数で渡す形式のため、使用しているRubyのバージョンに合わせて公式ドキュメントを確認してください。

環境ごとの設定ファイルで使うパターン

実際に複数環境で使った構成です。config gemの config/settings.yml(共通設定)と config/settings/production.yml(本番設定)で正規表現を環境ごとに上書きするパターンです。

# config/settings.yml(開発・デフォルト)
validation:
  url_pattern: !ruby/regexp /^https?:\/\/.+/

# config/settings/production.yml(本番は厳しめのルール)
validation:
  url_pattern: !ruby/regexp /^https:\/\/[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]+)+.*/

Railsコードでは Settings.validation.url_pattern だけ呼べばよく、環境切り替えは設定ファイル側で完結します。

参考

質問・リクエストを送る

記事についての質問や、取り上げてほしいテーマがあればお気軽にどうぞ。いただいた質問はブログ記事として回答し、Q&Aページで公開することがあります。

このサイトについて

井上 周(Amane Inoue)の個人ブログです。技術・読書・ドラマ・旅・大学生活のことを書いています。