はじめに
config という gem は,プロジェクト全体で用いる設定値のたぐいを管理するもの。
あらかじめ値を YAML 形式で
before:
morning: 3
evening: 4
after:
morning: 4
evening: 3
のように用意しておけば
p Settings.before.evening # => 4
のようにしてメソッドチェーンで値を取り出すことができる。
Rails の定数管理の定番 gem らしいが,べつに Rails 専用というわけではなく,フレームワークを使わないようなプログラムでも便利に使える。
※本記事は config の(2022-03-05 時点の)最新版であるバージョン 4.0.0 に基づいている。
ちょっとハマった
昔つくった Rails アプリがいくつもある。定数管理には settingslogic を使ったものと easy_settings を使ったものとがあった。
ところがこの二つの gem はそれぞれ 10 年前,7 年前に開発が止まっていて,安心して使い続けられない気がした。
そこで,手始めに easy_settings を使った Rails アプリ一つを config に変えてみようと思った。
easy_settings と config は使い方が意外と似ていて,ちょっとの修正で config に移行することができた(ように見えた)。
ところがテストの一部が通らない。
その原因とは?というのがこの記事のテーマ。
落とし穴
本題に入る前に,config の使い方をもう少し説明したい。
まず,冒頭に掲げた YAML ファイルを再掲する:
before:
morning: 3
evening: 4
after:
morning: 4
evening: 3
そして,Gemfile は
source "https://rubygems.org"
gem "config"
としておく。
Rails のことは忘れてシンプルに小さな Ruby プログラムで config を使ってみよう。
require "bundler"
Bundler.require
Config.load_and_set_settings "settings.yaml"
p Settings.before.morning # => 3
こんなふうに使える。
さて,Settings.before
について,morning
の値を取り出すか,evening
の値を取り出すかが動的に決まる場合はどう書けばよいか。
もちろん send
メソッドを使って
time = :evening
p Settings.before.send(time) # => 4
と書くこともできるわけだが,config で作られるオブジェクトはハッシュ的なインターフェースを持っているので,[ ]
を使って
time = :evening
p Settings.before[time] # => 4
と書くこともできる。
[ ]
に与える引数は,シンボルでも文字列でも構わない。ココ重要。
だから,
p Settings.before[:evening] # => 4
p Settings.before["evening"] # => 4
ということになる。
このあたりの仕様は easy_settings でも変わらない。たしか settingslogic でも同じだったと思う。
問題はここからだ。
Settings
とか Settings.before
とかはハッシュ的なインターフェースを持つオブジェクトなのだが,ハッシュと同様,「〇〇というキーを持っているか」は has_key?
(もしくはエイリアスの関係にある key?
)メソッドで知ることができる。
つまり,
p Settings.has_key?(:before) # => true
p Settings.before.key?(:evening) # => true
p Settings.after.key?(:noon) # => false
という具合。
この点も easy_settings と共通している。
ようやく本題に入る。
easy_settings では,has_key?
の引数はシンボルでも文字列でもよかった。
ところが,config では シンボルでなければならない んである。
こんなふうになる:
p Settings.has_key?(:before) # => true
p Settings.has_key?("before") # => false
げえぇ〜
引数はシンボルでなければ正しくキーとして認識してくれないのに,文字列を与えても TypeError は出ない!
これは恐ろしい。分かりづらいバグの原因になる。
今回は運良くテストコードによって気づくことができたが,テストで引っかかってくれていなかったら何時間も潰すところだった。
件のコードは has_key?
にリテラルではなく変数を与えていたため,もし config のこの仕様を最初から知っていたとしても問題の所在に気づきにくかったかもしれない。
みなさまご注意を。
(2023-07-01 追記)今日もまたこの問題でバグってしまった。これどう考えても config gem の仕様がおかしい。String と Symbol には一対一の対応があり,has_key?
はキーの有無を問うているのだから,String は Symbol 化して受け取ってくれるべきだろう。