発端
Ruby で YAML ファイルを読み込んで利用するコードを書いていたのだが,ある日とつぜん,YAML がエラーを出すようになった。
そのプロジェクトでは何千行もある YAML ファイルの編集が進行中であった。その YAML データを Ruby スクリプトで読み込む際に以下のようなエラーが発生するようになったのだ。
Psych::BadAlias: Unknown alias: foobar
(foobar
の部分は実際のものとは変えてある)
このプロジェクトで初めての経験だった。
調査
「Psych」というのが YAML のバックエンドであることは知っていた。
Ruby の yaml ライブラリーは psych ライブラリーを内部で利用していて,YAML モジュールは,実際の仕事を Psych モジュールに振っているらしい。
だから,YAML データに不備があったとき YAML モジュールではなく Psych モジュールで例外が発生する,ということは驚くに値しない。
ともかく,「Unknown alias: foobar」なので「foobar」というエイリアスは知らん,と言っているようだ。
なるほど,日々 YAML ファイルを編集している過程で何かやらかしたに違いない。
ところが,いくら調べても問題が見当たらない。最小再現コード1を書いて「YAML ファイルは間違っていない」という確信を得た。
以下が最小(に近い)再現コード:
require "yaml"
p YAML.load <<~EOT
a: &foo 1
b: *foo
EOT
原因
原因は Psych がバージョン 4.0 で仕様が変わったことにあった。セキュリティー上の理由から,デフォルトではエイリアスが使えないようになったのだ。
前節のコードがエラー
Unknown alias: foo (Psych::BadAlias)
を出すのは,システムに psych 4.0.0 以上がインストールされているとき。
(いま手許では 4.0.2 で実験した)
Psych のバージョンを意図的に下げて
gem "psych", "<4"
require "yaml"
p YAML.load <<~EOT
a: &foo 1
b: *foo
EOT
とすればエラーは出ない。
それにしても腑に落ちないのはエラーメッセージだ。エイリアスを認めていないのだから,
Unknown alias: foo (Psych::BadAlias)
(不明なエイリアス)
というエラーメッセージはおかしい。Psych::BadAlias という例外クラスも変。
どう見たってエイリアスの記述がおかしいと思ってしまう。このせいでけっこうな時間を無駄に費やしてしまった。
対策
Psych 4.0 以上でも,エイリアスを使う方法はある。オプション aliases: true
を与えればいいのだ:
require "yaml"
p YAML.load <<~EOT, aliases: true
a: &foo 1
b: *foo
EOT
これで「古い Psych を使い続ける」という筋の悪い回避策を取らなくて済む。
課題
自分で書いたコードはいいのだが,利用しているライブラリーでこの問題が起こった場合は厄介だ。
例えば ISBN コードのお世話をする lisbn という gem がある(次節参照)。この gem は require "lisbn"
しただけで落ちる。内部で,エイリアスを含む YAML データを読み込んでいる のだ。
2019 年で開発が止まっており,すぐに修正されるとは思えない。
簡単な対処法としては Gemfile 等で psych のバージョンを抑えるしかないと思うが,もし他のライブラリーで 4.0 以上の psych を要求するようなら同時には使えないことになる。
追記(2022-03-10) 直った
lisbn のこの問題は 2022 年 1 月 26 日にリリースされたバージョン 0.3.2 で修正された。
https://github.com/ragalie/lisbn/commit/70fcd68e8fac90a3c1fc9afa6329654484652a3f
余談
本記事のテーマとは全く関係ないが,前節で紹介した lisbn の使い道を書く。長い余談である。
ISBN の処理といった場合,まず浮かぶのがチェックディジットの検査だろう。これは Ruby なら 1 行で書ける簡単な処理なので,ライブラリーを使わなくても自分で実装すればいいかもしれない(でもテストコードはしっかり書こうな)。
そこで,ここではチェックディジット検査以外の ISBN コード処理を考えることにしよう。
ある日あなたは『プロを目指す人のためのRuby入門[改訂2版]』の ISBN が知りたくなった。
書名で検索すると,南米の大河の名をつけたサイトが上位に出てきたのでそこをクリック2。
「登録情報」のところに
ISBN-13 : 978-4297124373
という行を見つける。
えっ,ちょっ,ナニコレ。ハイフンが 1 個しか入ってねえじゃん3。
なぜこうなっているかは容易に想像がつく。
ISBN は,接頭記号,グループ記号,出版者記号,書名記号,チェックディジットの五つの数字列をハイフンで繋いだ構造になっているが,接頭記号とチェックディジットを除けば桁数は固定されていない(桁数の合計は 13 で固定)。
接頭記号は 978 か 979 しかありえないが,グループ記号は 1〜5 桁がありうる。
先頭が 1, 2, 3, 4, 5 であるグループ記号は 1 桁と決まっているので,上記の場合,グループ記号が 4(日本)であると分かるのだが,番号空間がどのように分割されているのかは International ISBN Agency のデータベースを見て初めて分かることである。
ISBN コードのどこにハイフンが入るべきかは,このデータに基づいて決定しなければならない。そのためのプログラムは,そう複雑なものではないが,自分で実装するのは少々面倒ではある4。番号空間の分割と割り当ては更新されていくので,データベースの更新に追随もしなければならない。
そこで重宝していたのが lisbn であった:
require "lisbn"
puts Lisbn.new("978-4297124373").parts.join("-")
# => 978-4-297-12437-3
正しいハイフンの入った ISBN がいとも簡単に得られる。
lisbn は,International ISBN Agency が XML ファイルで提供しているデータ(範囲データ)を YAML 形式で保持し,require
時にそれを読み込んでいるのであった。その際に(psych 4.0 以上では)エラーが出るというワケ。
lisbn の最新版は 2019 年 12 月 7 日のバージョン 0.3.1。International ISBN Agency のデータもその時点のものなので,ここ 2 年のうちに新しく割り当てられたグループ記号・出版社記号の範囲に入っている ISBN コードには,そのままでは対応できない5。
なお,978-4(日本)に関しては番号空間の分割はだいぶ前に終わっている。日本が 978-4 を使い切って新たにグループ記号をもらうまでは,(日本に関しては)現行の lisbn で問題ないはずだ。
psych 問題と範囲データが古い問題は場合によってはなかなかつらい。いつか lisbn の開発が再開するかもしれないが,結局私はこの処理を自分で書くことになった。
追記(2022-03-10) Rails では
エイリアスを含んだ YAML ファイルは Rails でも使われており,同様の問題があった。
これは Rails 6.1.4(2021 年 6 月 24 日リリース)で解消されている。
参考:https://github.com/rails/rails/blob/v6.1.4/railties/CHANGELOG.md
古い Rails を使う必要がある場合は Gemfile に
gem "psych", "~> 3.3"
と書くなどしてバージョン 3 系の psych を指定することになるが,Rails 6.1.4 以降であればその必要はなく,むしろ古い psych を使い続ける弊を避けるべきだろう。