LoginSignup
1
1

config gem の has_key? の落とし穴

Last updated at Posted at 2022-03-05

はじめに

config という gem は,プロジェクト全体で用いる設定値のたぐいを管理するもの。

あらかじめ値を YAML 形式で

settings.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 ファイルを再掲する:

settings.yaml
before:
  morning: 3
  evening: 4
after:
  morning: 4
  evening: 3

そして,Gemfile は

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 化して受け取ってくれるべきだろう。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1