Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

RubyのURI.joinはなぜ直感に反する動きをするのか?

Honeybadgerでは、ついに一つの節目を迎えました。販売ページがメインのRailsアプリケーションから分離されたのです。これは何年も前から私のウィッシュリストに入っていたものの、優先度はそれほど高くありませんでした。

この移行作業の一環として、特定のリダイレクトリンクを作成するためにURI.joinを使う場面がありました。ところがすぐに問題にぶつかります。URI.joinが期待どおりに動作しなかったのです。

私は、複数のパス断片を受け取って、それらを次のように連結してくれると考えていました。

# こうなることを期待していた。しかし現実は違った。
URI.join("https://www.honeybadger.io", "plans", "change")
=> "https://www.honeybadger.io/plans/change"

しかし実際のjoinメソッドの挙動は、はるかに奇妙なものでした。パス断片の一つ「plans」が消え去り、最後の「change」だけが使われたのです。

# 実際に起きたこと。
URI.join("https://www.honeybadger.io", "plans", "change")
=> "https://www.honeybadger.io/change"

一体なぜこんな動作になるのでしょうか?

誤解の正体

実は私は、URI.joinArray#joinの特殊版のようなものだと考え、URLの断片を組み合わせて完全なURLを作ってくれると期待していたのです。

しかしそれはURI.joinの役目ではありません。大きな誤算でした。

joinメソッドのソースコードを見てみると、単純にすべての引数を順番に処理し、それぞれに対してmergeを呼び出しているだけだとわかります。

# File uri/rfc2396_parser.rb, line 236
def join(*uris)
  uris[0] = convert_to_uri(uris[0])
  uris.inject :merge
end

mergeメソッドは2つのことを行います。

  1. "pages"のような文字列を相対URIオブジェクトに変換する。
  2. その相対URIをベースURIに対して解決しようとする。この処理は、RFC2396のセクション5.2で規定されている方法に厳密に従って行われる。

なるほど、それなら先ほどの予想外の挙動はどう説明できるのでしょうか?

URI.join("https://www.honeybadger.io", "plans", "change")
=> "https://www.honeybadger.io/change"

順を追って見ていきましょう。上記のコードは、実質的に以下と同じです。

URI.parse("https://www.honeybadger.io/plans").merge("change")

このコードは、絶対URI「https://www.honeybadger.io/plans」に対して、相対URI「change」を解決しようとしています。

この解決処理は、RFC2396のセクション5.2.6に従います。同セクションには次のように記されています。

a) ベースURIのパスコンポーネントのうち、最後のセグメント以外をすべてバッファへコピーする。言い換えれば、最後(最も右側)のスラッシュ文字以降の文字があれば、それらは除外される。

b) 参照(リファレンス)のパスコンポーネントをバッファ文字列に追加する。

実際に手順に従ってみましょう。

  1. 絶対URLの最後のセグメント以外をすべてコピーする。これにより "https://www.honeybadger.io/" が得られる。
  2. 相対パスを追加すると、結果は "https://www.honeybadger.io/change" となる。

これで世界の秩序が取り戻されました!

まとめ

URI.joinは確かに複数のパス断片からURLを構築できますが、それは本来の設計目的ではありません。このメソッドはもう少し複雑なことを意図して作られています。すなわち、RFCで定められた標準に従って、URIを再帰的にマージすることです。

ちなみに私自身の目的——新しい販売ページへのリダイレクト用URLの構築——については、結局Array#joinを使うことにしました。:)

2016年8月12日追記: この記事を公開した後、「この用途にはFile.joinを使えばいい」という提案のツイートをいくつかいただきました。確かにFile.joinを使えばスラッシュの重複(例:/my//path)を避けられますが、Windowsのようにパス区切り文字がフォワードスラッシュではないOSでは動作しなくなるという欠点があります。

  1. なぜクラスを作るのか?抽象化の重要性と良いクラス設計の基本

    前回の記事「なぜnilを使うのか」に続き、普段当たり前と思って使っているものについても、同じ問いを立ててみるのは良いアイデアではないでしょうか。 例えば… 巨大で醜いコードの塊を1つ作るのではなく、なぜ私たちはクラスを使うのでしょうか? 基本的なレベルで言えば、コードとデータを論理的な単位に整理するためにクラスを使います。しかし、それだけではありません。 クラスを使うと、抽象化(abstraction)を作り出せるのです。 抽象化とは何か? 実は、私たちは毎日、毎分、抽象化を利用しています。 今この記事を書いているキーボードは、次のようなもので構成されています。 ケーブル Cherry MX

  2. Windows 10が使いにくいと言われる15の理由とその対策

    Windows 10は世界的に有名なオペレーティングシステムであり、定期的なアップデートによって信頼性を維持している点が特徴です。搭載されているアプリやウィジェットは完璧ではありませんが、十分に実用的です。しかし、設定や機能にはまだ改善の余地があります。 Microsoftは全世界で約13億人のWindows 10ユーザーを抱えていますが、「Windows 10は使えない」と感じている人も少なくありません。その理由は、日常的に発生するさまざまな問題にあります。例えば、エクスプローラーの不具合、VMwareとの互換性問題、データ消失などです。また、Windows 10 Proはファイル階層の管