雰囲気でgemを使ったことを反省して学びなおす 〜Faraday編〜
はじめに
現在携わっているプロジェクトで、Faradayを利用してコードを書く機会がありましたが、READMEや関連部分のソースコードをチラ見したくらいで、十分に理解して利用しているとは言えない状態でした。
きちんと理解した上で使いたい!という気持ちが芽生えたので、Faradayはそもそも何のか、その仕組みや使い方、内部の実装等を理解するために、公式ドキュメントの確認、挙動の確認やコードリーディング等を行いました。
gemのソースコードを詳細に読んでみるのは初めてで、自分の実力不足を痛感しまくったわけですが、学んだ内容をこの記事にまとめてみました。
Faradayを使ったことがないという方、使ったことはあるけどソースコードはチラ見したくらい...という方等の参考にになれば幸いです!
「自分なりにこう理解している」という内容を記載していますが、Web系エンジニアに転職してまだ1年目ということもあり、記載されている内容に誤りが含まれている可能性があります。
「この部分おかしいんじゃ?」「これってどういうこと?」等のご指摘やご質問がございましたら、コメント頂けますと幸いです🙏
Faradayとは
GitHubのREADMEには下記のように書かれています。
Faraday is an HTTP client library abstraction layer that provides a common interface over many adapters (such as Net::HTTP) and embraces the concept of Rack middleware when processing the request/response cycle. Take a look at Awesome Faraday for a list of available adapters and middleware.
翻訳すると👇
FaradayはHTTPクライアントライブラリの抽象化レイヤで、多くのアダプタ(Net::HTTPなど)に共通のインターフェイスを提供し、リクエスト/レスポンスサイクルの処理にRackミドルウェアの概念を取り入れています。利用可能なアダプタとミドルウェアの一覧は Awesome Faraday をご覧ください。
正直、上記の説明が難しいなと感じましたし、よくわかりませんでした😂
よく理解できない原因は、説明の中で使われている用語がよく分かっていないからだと感じましたので、疑問に感じた箇所を調べてみました📝
アダプタ is 何?
FaradayはHTTPリクエストを自分では行わず、Faradayアダプタに依存します。
アダプタとは、Faradayが実際にHTTPリクエストを送る際に使う特定のライブラリ(Net::HTTP、HTTPClient、Excon、Typhoeusなど)を指します。
HTTPクライアントライブラリの抽象化レイヤ?
アダプタによってリクエストの処理方法が変わりますが、Faradayはそれぞれのアダプタに対して統一的なインターフェースを提供しており、利用者はアダプタが何であるかを意識せずに同じコードでリクエストが可能です。
「異なるライブラリを統一的に扱えるようにしてくれる仕組み」があるので、「抽象化レイヤ」と表現されているのだと理解しました。
ミドルウェアって何だっけ...
ミドルウェアは、アプリケーション(Rails等)とサーバー(NGINX、Apache等)間の橋渡し役です。
サーバーにリクエストが届いてから最終的にアプリケーションに届くまで、または、アプリケーションから返されるレスポンスがユーザーに戻るまでに、必要な処理(例:ログを記録する、認証情報の追加、エラーハンドリング等)を追加できる「フィルター」のようなもの、と自分なりに理解しています。
以下はRailsで利用されているミドルウェアの例です
- Rack::Runtime
リクエストがどれくらいの時間で処理されたかを計測し、レスポンスヘッダーに追加する。 - ActionDispatch::Cookies
クッキーを管理する。ユーザーごとに情報を保存し、次のリクエスト時に同じクッキー情報を使って再認証などができる。 - ActionDispatch::Session::CookieStore
ユーザーのセッション情報をクッキーに保存する。セッション情報は、ユーザーがログインしているかどうかなどの状態を保持するために使われる。 - Rack::MethodOverride
HTMLフォームでPOSTリクエストしか送れない問題を解決する。このミドルウェアを通じて、リクエストに特定のパラメータを追加することで、PUTやDELETEなど他のHTTPメソッドに対応できるようにしている。
因みに、Railsアプリでは、以下のコマンドを実行することで、使用されているミドルウェアを確認することができます。
bin/rails middleware
# 実行結果の例
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActionDispatch::ServerTiming
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Sprockets::Rails::QuietAssets
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use WebConsole::Middleware
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use ActionDispatch::PermissionsPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run Myapp::Application.routes
調べた内容をまとめると...
以下のとおりだと自分なりに理解しました。
- Faradayは、RubyでHTTP通信を行うためのクライアントライブラリ
- 複数のアダプタに対して共通のインターフェイスを提供しており、利用者はアダプタの違いを気にせずコードが書ける
- ミドルウェア機構を備えており、リクエストやレスポンスに対する追加の処理を行うことができる
Faradayのミドルウェア
ここからは、Faradayのミドルウェアについて確認していきます。
Faradayは、Rackにインスパイアされたミドルウェア・スタックを使用してリクエストを行っています。以下は、Faradayのミドルウェアが提供できる機能の例です。
- 認証
- レスポンスをディスクやメモリにキャッシュ
- クッキー
- リダイレクト
- JSONのエンコード/デコード
- ロギング
これらの機能を使うには、Faraday::ConnectionをFaraday.newで作成し、適切なミドルウェアをブロックに追加します。コードの例は以下の通りです。
require 'faraday'
connection = Faraday.new do |f|
f.request :json # リクエストボディをJSONとしてエンコードする
f.response :logger # リクエストとレスポンスのログ
f.response :json # レスポンス・ボディをJSONとしてデコードする
f.adapter :net_http # アダプタはNet::HTTPを使用する
end
response = connection.get("http://httpbingo.org/get")
仕組み
Faraday::ConnectionはFaraday::RackBuilderを使って、HTTPリクエストを行うためのRack風のミドルウェアスタックを組み立てます。
それぞれのミドルウェアが実行され、Envオブジェクト(FaradayがHTTPリクエストやレスポンスを処理する際に使う情報の集まり)を次のミドルウェアに渡します。最後のミドルウェアが実行されると、Faraday はエンドユーザに Faraday::Response を返します。
Faraday Websiteより引用
例えば、次のようなコードがある場合、
Faraday.new(...) do |connection|
connection.request :authorization
connection.response :json
connection.response :parse_dates
end
ミドルウェアスタックは下記のようになります。
authorization do
# 認証リクエストフック
json do
# JSONリクエストフック
parse_dates do
# 日付解析リクエストフック
response = adapter.perform(request)
# 日付解析レスポンスフック
end
# JSONレスポンスフック
end
# 認証レスポンスフック
end
動かしてみる
ここからは、Rails newで作成したアプリにFaradayをインストールして、実際の挙動から学んだことをまとめていきます。
環境
- Ruby 3.3.5
- Rails 7.2.2
- Faraday 2.12.0
インストール
Gemfileに下記を記載
gem faraday
下記を実行
bundle
リクエスト
Faraday.getを使って簡単なGETリクエストができます。
HTTPに関するあらゆることができるモックサーバーで、無料で使うことができるhttpbingo.orgサービスを利用して、GETリクエストを試してみます。
myapp(dev)> response = Faraday.get('https://httpbingo.org')
railsコンソールでの実行結果は下記の通りです。
myapp(dev)> response.status
# => 200
myapp(dev)> response.headers
# =>
{
"access-control-allow-credentials": "true",
"access-control-allow-origin": "*",
"content-security-policy": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' camo.githubusercontent.com",
"content-type": "text/html; charset=utf-8",
"date": "Sat, 02 Nov 2024 12:12:40 GMT",
"transfer-encoding": "chunked",
"content-encoding": "gzip",
"server": "Fly/**** (2024-10-30)",
"via": "1.1 fly.io",
"fly-request-id": "****"
}
myapp(dev)> response.body
# => "<!DOCTYPE html><html> ...
Faraday Connection
Faradayを利用する場合、特にサードパーティのサービスやAPIと連携する場合は、Faraday::Connectionを作成します。
コネクションのイニシャライザでは、下記を設定します。
- デフォルトのリクエストヘッダとクエリパラメータ
- プロキシやタイムアウトなどのネットワーク設定
- 共通URLベースパス
- Faraday アダプタとミドルウェア
Faraday::Connection を作成するには、まずFaraday.new を呼び出し、その後、Faraday::Connection上で各HTTPリクエストメソッド(get, post等)を呼び出してリクエストを実行します。
myapp(dev)>
connection = Faraday.new(
url: 'https://httpbingo.org',
params: {param: '1'},
headers: {'Content-Type' => 'application/json'}
)
myapp(dev)> connection
# =>
#<Faraday::Connection:0x0000****>
@builder=#<Faraday::RackBuilder:0x0000**** @adapter=Faraday::Adapter::NetHttp, @handlers=[Faraday::Request::UrlEncoded]>,
@default_parallel_manager=nil,
@headers={"Content-Type"=>"application/json", "User-Agent"=>"Faraday v2.12.0"},
@manual_proxy=false,
@options=
#<struct Faraday::RequestOptions
params_encoder=nil,
proxy=nil,
bind=nil,
timeout=nil,
open_timeout=nil,
read_timeout=nil,
write_timeout=nil,
boundary=nil,
oauth=nil,
context=nil,
on_data=nil>,
@parallel_manager=nil,
@params={"param"=>"1"},
@proxy=nil,
@ssl=
#<struct Faraday::SSLOptions
verify=nil,
verify_hostname=nil,
ca_file=nil,
ca_path=nil,
verify_mode=nil,
cert_store=nil,
client_cert=nil,
client_key=nil,
certificate=nil,
private_key=nil,
verify_depth=nil,
version=nil,
min_version=nil,
max_version=nil,
ciphers=nil>,
@url_prefix=#<URI::HTTPS https://httpbingo.org/>>
myapp(dev)>
response = connection.post('/post') do |req|
req.params['limit'] = 100
req.body = {query: 'chunky bacon'}.to_json
end
myapp(dev)> JSON.parse(response.body)
# =>
{"args"=>{"limit"=>["100"], "param"=>["1"]},
"headers"=>
{"Accept"=>["*/*"],
"Accept-Encoding"=>["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"],
"Content-Length"=>["24"],
"Content-Type"=>["application/json"],
"Host"=>["httpbingo.org"],
"User-Agent"=>["Faraday v2.12.0"],
"Via"=>["1.1 fly.io"],
"X-Forwarded-For"=>["****, ****"],
"X-Forwarded-Port"=>["443"],
"X-Forwarded-Proto"=>["https"],
"X-Forwarded-Ssl"=>["on"],
"X-Request-Start"=>["t=****"]},
"method"=>"POST",
"origin"=>"****",
"url"=>"https://httpbingo.org/post?limit=100¶m=1",
"data"=>"{\"query\":\"chunky bacon\"}",
"files"=>{},
"form"=>{},
"json"=>{"query"=>"chunky bacon"}}
GET, HEAD, DELETE, TRACE
Faradayは、通常リクエストボディを含まない以下のHTTPリクエストメソッドをサポートしています
- get(url, params = nil, headers = nil)
- head(url, params = nil, headers = nil)
- delete(url, params = nil, headers = nil)
- trace(url, params = nil, headers = nil)
リクエストを行う際に、URIクエリパラメータやHTTPヘッダを指定することができます。
以下はGETリクエストとレスポンスの例です。
myapp(dev)> response = connection.get('get', { boom: 'zap' }, { 'User-Agent' => 'myapp' })
# => GET https://httpbingo.org/get?boom=zap
myapp(dev)> response
# =>
#<Faraday::Response:0x0000****>
@env=
#<struct Faraday::Env
method=:get,
request_body=nil,
url=#<URI::HTTPS https://httpbingo.org/get?boom=zap¶m=1>,
request=
#<struct Faraday::RequestOptions
params_encoder=nil,
proxy=nil,
bind=nil,
timeout=nil,
open_timeout=nil,
read_timeout=nil,
write_timeout=nil,
boundary=nil,
oauth=nil,
context=nil,
on_data=nil>,
request_headers={"Content-Type"=>"application/json", "User-Agent"=>"myapp"},
ssl=
#<struct Faraday::SSLOptions
verify=true,
verify_hostname=nil,
ca_file=nil,
ca_path=nil,
verify_mode=nil,
cert_store=nil,
client_cert=nil,
client_key=nil,
certificate=nil,
private_key=nil,
verify_depth=nil,
version=nil,
min_version=nil,
max_version=nil,
ciphers=nil>,
parallel_manager=nil,
params=nil,
response=#<Faraday::Response:0x0000**** ...>,
response_headers=
{"access-control-allow-credentials"=>"true",
"access-control-allow-origin"=>"*",
"content-type"=>"application/json; charset=utf-8",
"date"=>"Sat, 02 Nov 2024 12:42:40 GMT",
"content-encoding"=>"gzip",
"transfer-encoding"=>"chunked",
"server"=>"Fly/**** (2024-10-30)",
"via"=>"1.1 fly.io",
"fly-request-id"=>"****"},
status=200,
reason_phrase="OK",
response_body=
# "{\n \"args\": {\n \"boom\": ...,
@on_complete_callbacks=[]>
myapp(dev)> JSON.parse(response.body)
# =>
{"args"=>{"boom"=>["zap"], "param"=>["1"]},
"headers"=>
{"Accept"=>["*/*"],
"Accept-Encoding"=>["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"],
"Content-Type"=>["application/json"],
"Host"=>["httpbingo.org"],
"User-Agent"=>["myapp"],
"Via"=>["1.1 fly.io"],
"X-Forwarded-For"=>["****, ****"],
"X-Forwarded-Port"=>["443"],
"X-Forwarded-Proto"=>["https"],
"X-Forwarded-Ssl"=>["on"],
"X-Request-Start"=>["t=****"]},
"method"=>"GET",
"origin"=>"****",
"url"=>"https://httpbingo.org/get?boom=zap¶m=1"}
POST, PUT, PATCH
Faradayはボディを持つHTTPリクエストメソッドもサポートしており、クエリパラメータの代わりに、リクエストボディを受け取ります。
- post(url, body = nil, headers = nil)
- put(url, body = nil, headers = nil)
- patch(url, body = nil, headers = nil)
以下は、POSTリクエストの例です。
# POST JSON content
myapp(dev)> response = connection.post('post', '{"boom": "zap"}', "Content-Type" => "application/json")
myapp(dev)> response
# =>
#<Faraday::Response:0x0000****>
@env=
#<struct Faraday::Env
method=:post,
request_body="{\"boom\": \"zap\"}",
url=#<URI::HTTPS https://httpbingo.org/post?param=1>,
request=
#<struct Faraday::RequestOptions
params_encoder=nil,
proxy=nil,
bind=nil,
timeout=nil,
open_timeout=nil,
read_timeout=nil,
write_timeout=nil,
boundary=nil,
oauth=nil,
context=nil,
on_data=nil>,
request_headers={"Content-Type"=>"application/json", "User-Agent"=>"Faraday v2.12.0"},
ssl=
#<struct Faraday::SSLOptions
verify=true,
verify_hostname=nil,
ca_file=nil,
ca_path=nil,
verify_mode=nil,
cert_store=nil,
client_cert=nil,
client_key=nil,
certificate=nil,
private_key=nil,
verify_depth=nil,
version=nil,
min_version=nil,
max_version=nil,
ciphers=nil>,
parallel_manager=nil,
params=nil,
response=#<Faraday::Response:0x0000**** ...>,
response_headers=
{"access-control-allow-credentials"=>"true",
"access-control-allow-origin"=>"*",
"content-type"=>"application/json; charset=utf-8",
"date"=>"Sat, 02 Nov 2024 12:58:54 GMT",
"content-encoding"=>"gzip",
"transfer-encoding"=>"chunked",
"server"=>"Fly/**** (2024-10-30)",
"via"=>"1.1 fly.io",
"fly-request-id"=>"****"},
status=200,
reason_phrase="OK",
myapp(dev)> JSON.parse(response.body)
# =>
{"args"=>{"param"=>["1"]},
"headers"=>
{"Accept"=>["*/*"],
"Accept-Encoding"=>["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"],
"Content-Length"=>["15"],
"Content-Type"=>["application/json"],
"Host"=>["httpbingo.org"],
"User-Agent"=>["Faraday v2.12.0"],
"Via"=>["1.1 fly.io"],
"X-Forwarded-For"=>["****, ****"],
"X-Forwarded-Port"=>["443"],
"X-Forwarded-Proto"=>["https"],
"X-Forwarded-Ssl"=>["on"],
"X-Request-Start"=>["t=****"]},
"method"=>"POST",
"origin"=>"****",
"url"=>"https://httpbingo.org/post?param=1",
"data"=>"{\"boom\": \"zap\"}",
"files"=>{},
"form"=>{},
"json"=>{"boom"=>"zap"}}
ミドルウェアを使う
ミドルウェアは、リクエストとレスポンスのサイクルにフックして、リクエストを変更することを可能にするクラスです。
例えば、ミドルウェアは以下のようなことを手助けしてくれます。
- 認証ヘッダの追加
- JSONレスポンスの解析
- リクエストとレスポンスのロギング
- 4xx および 5xx レスポンスでエラーを発生させる
例えば、以下のようなAPIを呼び出したいとします。
- Authorizationヘッダで認証トークンを要求する
- JSONリクエストボディを期待する
- JSONレスポンスを返す
- 4xxおよび5xxレスポンスに対して自動的にエラーを発生させ、すべてのリクエストとレスポンスをログに記録する
Faradayで接続に必要なミドルウェアを追加することで、上記のすべてを簡単に実現することができます。以下はコードと実行結果です。
コード
# lib/faraday.rb
def connection
@connection ||= Faraday.new(url: 'https://httpbingo.org') do |builder|
# 各リクエストで MyAuthStorage.get_auth_token を呼び出して認証トークンを取得し、
# Bearer スキームで Authorization ヘッダーに設定します。
builder.request :authorization, 'Bearer', -> { MyAuthStorage.get_auth_token }
# 各リクエストで Content-Type ヘッダーを application/json に設定します。
# また、リクエストボディが Hash の場合、自動的に JSON としてエンコードされます。
builder.request :json
# JSON レスポンスボディを解析します。
# レスポンスボディが有効な JSON でない場合、Faraday::ParsingError を発生させます。
builder.response :json
# 4xx および 5xx レスポンスでエラーを発生させます。
builder.response :raise_error
# リクエストとレスポンスをログに記録します。
# デフォルトでは、リクエストメソッドと URL、およびリクエスト/レスポンスヘッダーのみをログに記録します。
builder.response :logger
end
end
def request
begin
response = connection.post('post', { payload: 'this ruby hash will become JSON' })
puts response.status
puts JSON.pretty_generate(response.body)
rescue Faraday::Error => e
# You can handle errors here (4xx/5xx responses, timeouts, etc.)
puts e.response[:status]
puts e.response[:body]
end
end
class MyAuthStorage
def self.get_auth_token
rand(36 ** 8).to_s(36)
end
end
実行結果(正常にレスポンスが返る場合)
# bundle exec rails c
myapp(dev)> require 'faraday'
# => true
myapp(dev)> request
I, [2024-11-02T13:50:37.333816 #137] INFO -- request: POST https://httpbingo.org/post
I, [2024-11-02T13:50:37.333981 #137] INFO -- request: User-Agent: "Faraday v2.12.0"
Authorization: "Bearer ****"
Content-Type: "application/json"
I, [2024-11-02T13:50:38.966502 #137] INFO -- response: Status 200
I, [2024-11-02T13:50:38.966757 #137] INFO -- response: access-control-allow-credentials: "true"
access-control-allow-origin: "*"
content-type: "application/json; charset=utf-8"
date: "Sat, 02 Nov 2024 13:50:38 GMT"
content-encoding: "gzip"
transfer-encoding: "chunked"
server: "Fly/**** (2024-10-30)"
via: "1.1 fly.io"
fly-request-id: "****"
200
{
"args": {
},
"headers": {
"Accept": [
"*/*"
],
"Accept-Encoding": [
"gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
],
"Authorization": [
"Bearer ****"
],
"Content-Length": [
"45"
],
"Content-Type": [
"application/json"
],
"Host": [
"httpbingo.org"
],
"User-Agent": [
"Faraday v2.12.0"
],
"Via": [
"1.1 fly.io"
],
"X-Forwarded-For": [
"****, ****"
],
"X-Forwarded-Port": [
"443"
],
"X-Forwarded-Proto": [
"https"
],
"X-Forwarded-Ssl": [
"on"
],
"X-Request-Start": [
"t=****"
]
},
"method": "POST",
"origin": "****",
"url": "https://httpbingo.org/post",
"data": "{\"payload\":\"this ruby hash will become JSON\"}",
"files": {
},
"form": {
},
"json": {
"payload": "this ruby hash will become JSON"
}
}
実行結果(エラーが発生する場合)
# 存在しないURL(https://httpbingo.org/hoge)を設定してpostリクエストを送信する
myapp(dev)> request
I, [2024-11-02T13:58:23.546668 #143] INFO -- request: POST https://httpbingo.org/hoge
I, [2024-11-02T13:58:23.546793 #143] INFO -- request: User-Agent: "Faraday v2.12.0"
Authorization: "Bearer ****"
Content-Type: "application/json"
I, [2024-11-02T13:58:24.230168 #143] INFO -- response: Status 404
I, [2024-11-02T13:58:24.230408 #143] INFO -- response: access-control-allow-credentials: "true"
access-control-allow-origin: "*"
content-type: "text/plain; charset=utf-8"
x-content-type-options: "nosniff"
date: "Sat, 02 Nov 2024 13:58:24 GMT"
content-encoding: "gzip"
transfer-encoding: "chunked"
server: "Fly/**** (2024-10-30)"
via: "1.1 fly.io"
fly-request-id: "****"
404
404 page not found
コードリーディング
挙動が何となくわかってきたので、ここからは、内部実装がどうなっているか学ぶために、主要コンポーネントのコードリーディングをしていきます。
Faraday.new
# faraday.rb
module Faraday
# 省略
class << self
def new(url = nil, options = {}, &block)
options = Utils.deep_merge(default_connection_options, options)
Faraday::Connection.new(url, options, &block)
end
end
# 省略
end
役割
new
メソッドは、指定されたURLとオプションを使って新しいFaraday::Connection
オブジェクトを作成します。
引数
-
url
: 接続先の基本URL -
options
: 接続の設定を含むハッシュ -
&block
: 接続オブジェクトをカスタマイズするためのブロック
処理の流れ
-
オプションのマージ:
options = Utils.deep_merge(default_connection_options, options)
ここでは、デフォルトの接続オプションと指定されたオプションをマージしています。
Utils.deep_merge
メソッドを使って、デフォルトの設定にユーザーが指定した設定を上書きします。 -
新しい接続オブジェクトの作成:
Faraday::Connection.new(url, options, &block)
マージされたオプションとURLを使って、新しい
Faraday::Connection
オブジェクトを作成します。必要に応じて、ブロックを使って接続オブジェクトをさらにカスタマイズできます。
default_connection_options
の中身は、どうなっているのでしょうか?
# faraday.rb
def default_connection_options
@default_connection_options ||= ConnectionOptions.new
end
上記のようなメソッドで実装されていました。
ここからは、ConnectionOptions.new
について見ていきます。
Faraday::ConnectionOptions.new
# faraday/options/connection_options.rb
module Faraday
# @!parse
# # ConnectionOptions contains the configurable properties for a Faraday
# # connection object.
# class ConnectionOptions < Options; end
ConnectionOptions = Options.new(:request, :proxy, :ssl, :builder, :url,
:parallel_manager, :params, :headers,
:builder_class) do
options request: RequestOptions, ssl: SSLOptions
memoized(:request) { self.class.options_for(:request).new }
memoized(:ssl) { self.class.options_for(:ssl).new }
memoized(:builder_class) { RackBuilder }
def new_builder(block)
builder_class.new(&block)
end
end
end
ConnectionOptions
というクラスが、Options
というクラスを継承していることを示しています。
また、Options
はStruct
のサブクラスになっています。
# faraday/options.rb
module Faraday
# Subclasses Struct with some special helpers for converting from a Hash to
# a Struct.
class Options < Struct
# 省略
end
end
Struct
については、今回のコードリーディングで初めて知りました。難しい...😇
Struct.new
にブロックを指定した場合は、定義したStruct
をコンテキストにブロックを評価します。
Options
はStruct
のサブクラスとなっているため、Options.new
のブロック内で書かれているoptions
やmemoized
は、Options
クラスに定義されているメソッドのことだとわかりました。
options
options
メソッドは下記の通り定義されています。
# faraday/options.rb
def self.options(mapping)
attribute_options.update(mapping)
end
def self.attribute_options
@attribute_options ||= {}
end
以上を踏まえると、Options
クラスの下記コードは:request
と:ssl
の各オプションに対して、それぞれ専用のクラスを割り当てているということがわかりました。
# faraday/options/connection_options.rb
options request: RequestOptions, ssl: SSLOptions
memoized
続いて、Options
クラスのmemoized
メソッドは下記の通り定義されています。
# faraday/options.rb
def self.memoized(key, &block)
unless block
raise ArgumentError, '#memoized must be called with a block'
end
memoized_attributes[key.to_sym] = block
class_eval <<-RUBY, __FILE__, __LINE__ + 1
remove_method(key) if method_defined?(key, false)
def #{key}() self[:#{key}]; end
RUBY
end
def self.memoized_attributes
@memoized_attributes ||= {}
end
今回コードリーディングしている箇所ではブロックが使われているので、unless
部分は無視して良さそうです。
class_eval
については、今回初めて知りました。激ムズ...😇
class_eval
を使うと、与えられた文字列内のRubyコードを、そのクラスのコンテキストで評価・実行します。
<<-RUBY ... RUBY
は、ヒアドキュメントと呼ばれるもので、複数行の文字列を定義する方法です。
__FILE__
や__LINE__
は、擬似変数と呼ばれるもので、内容はそれぞれ下記の通りです。
-
__FILE__
:現在実行中のソースファイルのファイル名を表す特殊な定数 -
__LINE__
:現在実行中のソースコードの行番号を表す特殊な定数
以上を踏まえ、__FILE__, __LINE__ + 1
を class_eval
に渡すことで、評価されるコードのファイル名と開始行番号を指定していることがわかりました。
おそらくですが、エラーやデバッグ時に問題の特定をしやすくするためでしょうか...?
わかる方いれば教えていただきたいです🙏
続いて、ヒアドキュメントの中身を見ていきます。
remove_method(key) if method_defined?(key, false)
この行では、もしクラス内に既に同じ名前のメソッドが定義されていた場合、それを削除します。
これにより、メソッドを上書きする際の警告やエラーを防ぎます。
def #{key}() self[:#{key}]; end
この行では、key
という名前のメソッドを定義し、そのメソッドが呼ばれたときにself[:key]
の値を返すようにします。
[]メソッドについては、下記の通り定義されています。
def [](key)
key = key.to_sym
if (method = self.class.memoized_attributes[key])
super(key) || (self[key] = instance_eval(&method))
else
super
end
end
このメソッドは、Rubyの[]演算子をオーバーライドしています。
メモ化された属性かを確認し、既に値が存在すればそれを返します。
値が存在しなければ、ブロックを実行して値を生成・保存し、返すようになっています。
instance_eval、初めて知りました😇
確認してきたことをまとめると、memoized
メソッドが実現していることは以下のとおりであることがわかりました。
-
Options
クラスにおいて、keyに対応するメソッドが動的に定義される。def request() self[:request] # この行で[]メソッドが呼ばれる end def ssl() self[:ssl] # この行で[]メソッドが呼ばれる end def builder_class() self[:builder_class] # この行で[]メソッドが呼ばれる end
- 上記メソッドを通じて、下記のように属性にアクセスできる。
conn_options = Faraday::ConnectionOptions.new # conn_optionsの中身 #<struct Faraday::ConnectionOptions request=#<struct Faraday::RequestOptions params_encoder=nil, proxy=nil, bind=nil, timeout=nil, open_timeout=nil, read_timeout=nil, write_timeout=nil, boundary=nil, oauth=nil, context=nil, on_data=nil>, proxy=nil, ssl=#<struct Faraday::SSLOptions verify=nil, verify_hostname=nil, ca_file=nil, ca_path=nil, verify_mode=nil, cert_store=nil, client_cert=nil, client_key=nil, certificate=nil, private_key=nil, verify_depth=nil, version=nil, min_version=nil, max_version=nil, ciphers=nil>, builder=nil, url=nil, parallel_manager=nil, params=nil, headers=nil, builder_class=Faraday::RackBuilder> request_options = conn_options.request # request_optionsの中身 #<struct Faraday::RequestOptions params_encoder=nil, proxy=nil, bind=nil, timeout=nil, open_timeout=nil, read_timeout=nil, write_timeout=nil, boundary=nil, oauth=nil, context=nil, on_data=nil>
- 最初に属性にアクセスするときに値を計算し、その後は保存した値を返す。
# []メソッドの関連部分抜粋 # 最初に属性にアクセスする場合は、下記部分のinstance_evalが実行される。 # 保存された値がある場合は、下記部分のsuper(key)が呼ばれる。 super(key) || (self[key] = instance_eval(&method))
default_connection_options
の中身がどうやって生成されているのかやっとわかったところで、ここからは、Faraday.new
の中で書かれているUtils.deep_merge
について、見ていきます。
Utils#deep_merge
# faraday/utils.rb
module Faraday
# Utils contains various static helper methods.
module Utils
def deep_merge!(target, hash)
hash.each do |key, value|
target[key] = if value.is_a?(Hash) && (target[key].is_a?(Hash) || target[key].is_a?(Options))
deep_merge(target[key], value)
else
value
end
end
target
end
# Recursive hash merge
def deep_merge(source, hash)
deep_merge!(source.dup, hash)
end
end
end
deep_merge
メソッドは、source
とhash
を再帰的にマージした新しいハッシュを返します。
deep_merge
メソッドの内部で、deep_merge!
メソッドを呼び出し、条件次第では、deep_merge!
メソッドの中で、deep_merge
が呼ばれます。
...ムズすぎる😂😂😂
再帰的処理の部分がかなり難しかったので、具体的な値が代入された場合の実行過程を考えてみました。
deep_mergeメソッドの具体的な実行過程と結果
source
とhash
が下記の通りだとします。
source = {
request: { timeout: 30, open_timeout: 10 }
}
hash = {
request: { timeout: 60, headers: { "User-Agent" => "Faraday" } }
}
deep_merge(source, hash)
を実行すると、source.dup
によってsource
の複製が作成され、この複製はtarget
としてdeep_merge!
に渡されます。
deep_merge!
メソッドの引数に渡されるtarget
とhash
は下記の通りです。
target = {
request: { timeout: 30, open_timeout: 10 }
}
hash = {
request: { timeout: 60, headers: { "User-Agent" => "Faraday" } }
}
deep_merge!(target, hash)
を実行すると、hash.each
によって、hash
の各key
とvalue
が順に処理されます。
key::request
value:{ timeout: 60, headers: { "User-Agent" => "Faraday" } }
次に、下記の条件式を確認していきます。
if value.is_a?(Hash) && (target[key].is_a?(Hash) || target[key].is_a?(Options))
value
もtarget[:request]
もハッシュなので、再帰的にdeep_merge(target[:request], value)
を呼び出します。
source = target[:request] = { timeout: 30, open_timeout: 10 }
hash = value = { timeout: 60, headers: { "User-Agent" => "Faraday" } }
deep_merge
メソッドの中で、deep_merge!(source.dup, hash)
が呼び出されます。
source.dup → { timeout: 30, open_timeout: 10 }
deep_merge!({ timeout: 30, open_timeout: 10 }, { timeout: 60, headers: { "User-Agent" => "Faraday" } })
deep_merge!
メソッド内の下記処理はどうなるでしょうか?
hash.each do |key, value|
target[key] = if value.is_a?(Hash) && (target[key].is_a?(Hash) || target[key].is_a?(Options))
deep_merge(target[key], value)
else
value
end
end
hash.each
による、hash
の各key
、value
、処理は下記の通りです。
- 1巡目
- key: :timeout
- value: 60
- if条件: false(valueはハッシュではない)
- 処理:target[:timeout] = 60(上書き)
- 2巡目
- key: :headers
- value: { "User-Agent" => "Faraday" }
- if条件: false(valueはハッシュであり、target[:headers]は存在しない)
- 処理:target[:headers] = { "User-Agent" => "Faraday" }(新規追加)
マージ結果は下記の通りとなり、
{
timeout: 60,
open_timeout: 10,
headers: { "User-Agent" => "Faraday" }
}
target[:request]
にマージ結果が代入されます。
target[:request] = {
timeout: 60,
open_timeout: 10,
headers: { "User-Agent" => "Faraday" }
}
リターンされる最終的なマージ結果(deep_merge
の返り値)は下記の通りです。
{
request: {
timeout: 60,
open_timeout: 10,
headers: { "User-Agent" => "Faraday" }
}
}
具体的な値を想定して処理を追っていくことで、やっとなんとなく理解できた気がする...
ここまでの内容で、Faraday.new
の中身の1行目が何をしているのか、やっとわかりました。
def new(url = nil, options = {}, &block)
options = Utils.deep_merge(default_connection_options, options)
Faraday::Connection.new(url, options, &block)
end
が、まだ、Faraday::Connection.new
が残っていました😂
ここからは、Faraday::Connection.new
について見ていきます。
Faraday::Connection.new
# faraday/connection.rb
module Faraday
class Connection
METHODS = Set.new %i[get post put delete head patch options trace]
USER_AGENT = "Faraday v#{VERSION}".freeze
attr_reader :params
attr_reader :headers
attr_reader :url_prefix
attr_reader :builder
attr_reader :ssl
attr_reader :parallel_manager
attr_writer :default_parallel_manager
attr_reader :proxy
def initialize(url = nil, options = nil)
options = ConnectionOptions.from(options)
if url.is_a?(Hash) || url.is_a?(ConnectionOptions)
options = Utils.deep_merge(options, url)
url = options.url
end
@parallel_manager = nil
@headers = Utils::Headers.new
@params = Utils::ParamsHash.new
@options = options.request
@ssl = options.ssl
@default_parallel_manager = options.parallel_manager
@manual_proxy = nil
@builder = options.builder || begin
# pass an empty block to Builder so it doesn't assume default middleware
options.new_builder(block_given? ? proc { |b| } : nil)
end
self.url_prefix = url || 'http:/'
@params.update(options.params) if options.params
@headers.update(options.headers) if options.headers
initialize_proxy(url, options)
yield(self) if block_given?
@headers[:user_agent] ||= USER_AGENT
end
end
end
まず下記の行について見ていきます。
options = ConnectionOptions.from(options)
ConnectionOptions
クラスはOptions
クラスを継承しており、fromメソッドはOptions
クラスに定義されています。
# faraday/options.rb
module Faraday
# Subclasses Struct with some special helpers for converting from a Hash to
# a Struct.
class Options < Struct
# Public
def self.from(value)
value ? new.update(value) : new
end
# Public
def update(obj)
obj.each do |key, value|
sub_options = self.class.options_for(key)
if sub_options
new_value = sub_options.from(value) if value
elsif value.is_a?(Hash)
new_value = value.dup
else
new_value = value
end
send(:"#{key}=", new_value) unless new_value.nil?
end
self
end
Faraday.new
でoptions
引数が渡された場合、default_connection_options
にoptions
で渡された値を反映したハッシュでConnectionOptions
インスタンスが作成されます。
続いて、下記の部分を見ていきます。
if url.is_a?(Hash) || url.is_a?(ConnectionOptions)
options = Utils.deep_merge(options, url)
url = options.url
end
url
がハッシュまたはConnectionOptions
クラスのインスタンスである場合に、options
を更新し、url
を設定するためのものです。これにより、url
とoptions
の両方が設定され、後続の処理で使用できるようになります。
続いて、下記の部分を見ていきます。
@parallel_manager = nil
@headers = Utils::Headers.new
@params = Utils::ParamsHash.new
@options = options.request
@ssl = options.ssl
@default_parallel_manager = options.parallel_manager
@manual_proxy = nil
この部分では、Faraday::Connection
クラスのインスタンス変数を初期化しています。
-
@parallel_manager = nil
@parallel_manager
は並列リクエストを管理するための変数です。初期値としてnil
を設定しています。 -
@headers = Utils::Headers.new
@headers
はHTTPリクエストのヘッダーを格納するための変数です。Utils::Headers
クラスの新しいインスタンスを作成して初期化しています。 -
@params = Utils::ParamsHash.new
@params
はURIクエリパラメータを格納するための変数です。Utils::ParamsHash
クラスの新しいインスタンスを作成して初期化しています。 -
@options = options.request
@options
はリクエストオプションを格納するための変数です。引数として渡されたConnectionOptions
オブジェクトのrequest
プロパティを使用して初期化しています。 -
@ssl = options.ssl
@ssl
はSSLオプションを格納するための変数です。引数として渡されたConnectionOptions
オブジェクトのssl
プロパティを使用して初期化しています。 -
@default_parallel_manager = options.parallel_manager
@default_parallel_manager
はデフォルトの並列マネージャを格納するための変数です。引数として渡されたConnectionOptions
オブジェクトのparallel_manager
プロパティを使用して初期化しています。 -
@manual_proxy = nil
@manual_proxy
はプロキシ設定が手動で行われたかどうかを示すフラグです。初期値としてnil
を設定しています。
続いて下記部分を見ていきます。
@builder = options.builder || begin
# Builderがデフォルトのミドルウェアを仮定しないように、空のブロックを渡す
options.new_builder(block_given? ? proc { |b| } : nil)
end
このコードは、Faraday::Connection
のミドルウェアスタックを適切に初期化するためのものです。options.builder
が指定されていない場合、デフォルトのミドルウェアが設定されないようにしています。
-
@builder = options.builder || begin
-
@builder
は、Faraday::Connection
のミドルウェアスタックを管理するオブジェクトです。 -
options.builder
が存在する場合、それを@builder
に設定します。 - 存在しない場合、
begin
ブロック内のコードを実行して@builder
を設定します。
-
-
# pass an empty block to Builder so it doesn't assume default middleware
Builder
がデフォルトのミドルウェアを仮定しないように、空のブロックを渡すことを説明しています。 -
options.new_builder(block_given? ? proc { |b| } : nil)
:-
options.new_builder
メソッドを呼び出して新しいBuilder
オブジェクトを作成します。 -
block_given?
は、ブロックが渡されているかどうかをチェックします。 - ブロックが渡されている場合、空のプロック(無名関数)を渡します。
- ブロックが渡されていない場合、
nil
を渡します。
-
options.new_builder
メソッドを呼び出されると、ConnectionOptions
クラスのnew_builder
メソッドが実行されます。
# faraday/options/connection_options.rb
def new_builder(block)
builder_class.new(&block)
end
Faraday
クラスのdefault_connection_options
生成時に、ConnectionOptions
クラスのbuilder_class
にはRackBuilder
が設定されているため、上記のコードでは、RackBuilder
のinitialize
メソッドが実行されます。
Faraday::RackBuilder.new
# faraday/rack_builder.rb
module Faraday
class RackBuilder
NO_ARGUMENT = Object.new
def initialize(&block)
@adapter = nil
@handlers = []
build(&block)
end
build(1〜2行目)
buildメソッドは下記のように定義されています。
def build
raise_if_locked
block_given? ? yield(self) : request(:url_encoded)
adapter(Faraday.default_adapter, **Faraday.default_adapter_options) unless @adapter
end
このメソッドは、ミドルウェアスタックを構築するために使用されます。
-
raise_if_locked
- この行は、ミドルウェアスタックがロックされているかどうかを確認します。ロックされている場合、変更は許可されません。
-
block_given? ? yield(self) : request(:url_encoded)
- ここでは、ブロックが渡されているかどうかをチェックします。
- ブロックが渡されている場合、そのブロックを実行し、
self
を引数として渡します。 - ブロックが渡されていない場合、デフォルトのリクエストミドルウェア
:url_encoded
を使用します。
-
adapter(Faraday.default_adapter, **Faraday.default_adapter_options) unless @adapter
- この行は、アダプターが設定されていない場合 (
@adapter
がnil
)、デフォルトのアダプターとそのオプションを設定します。
- この行は、アダプターが設定されていない場合 (
request(:url_encoded)
の部分では、下記のrequest
メソッドが実行されます。
ruby2_keywords def request(key, *args, &block)
use_symbol(Faraday::Request, key, *args, &block)
end
request
メソッドは、指定されたキーに基づいてミドルウェアをスタックに追加するためのメソッドです。このメソッドは、use_symbol
メソッドを呼び出して、Faraday::Request
モジュールからミドルウェアを見つけて追加します。
-
ruby2_keywords
これは、Ruby 2.7以降でキーワード引数の互換性を保つためのキーワードです。 -
def request(key, *args, &block)
-
request
という名前のメソッドを定義しています。 -
key
は、リクエストミドルウェアの種類を指定するための引数です。 -
*args
は、可変長引数で、任意の数の引数を受け取ることができます。 -
&block
は、ブロックを引数として受け取ります。
-
-
use_symbol(Faraday::Request, key, *args, &block)
-
use_symbol
メソッドを呼び出し、Faraday::Request
モジュールとkey
、*args
、&block
を渡します。 -
use_symbol
メソッドは、指定されたキーに基づいてミドルウェアを見つけてスタックに追加します。
-
use_symbol
メソッドは下記の通り定義されています。
ruby2_keywords def use_symbol(mod, key, *args, &block)
use(mod.lookup_middleware(key), *args, &block)
end
use_symbol
は、シンボルを使ってミドルウェアを追加するためのメソッドです。このメソッドは、指定されたモジュール(mod
)からミドルウェアを見つけ出し、それをuse
メソッドに渡して追加します。
-
def use_symbol(mod, key, *args, &block)
:
mod
はモジュール、key
はシンボル、*args
は可変長引数、&block
はブロックを受け取ります。 -
use(mod.lookup_middleware(key), *args, &block)
:-
mod.lookup_middleware(key)
は、モジュールmod
からkey
に対応するミドルウェアを見つけ出します。 - 見つけたミドルウェアと引数、ブロックを
use
メソッドに渡して追加します。
-
lookup_middleware
メソッドはFaraday::MiddlewareRegistry
モジュールに定義されています。
Faraday::MiddlewareRegistry
# faraday/middleware_registry.rb
require 'monitor'
module Faraday
module MiddlewareRegistry
def registered_middleware
@registered_middleware ||= {}
end
def register_middleware(**mappings)
middleware_mutex do
registered_middleware.update(mappings)
end
end
def lookup_middleware(key)
load_middleware(key) ||
raise(Faraday::Error, "#{key.inspect} is not registered on #{self}")
end
private
def middleware_mutex(&block)
@middleware_mutex ||= Monitor.new
@middleware_mutex.synchronize(&block)
end
def load_middleware(key)
value = registered_middleware[key]
case value
when Module
value
when Symbol, String
middleware_mutex do
@registered_middleware[key] = const_get(value)
end
when Proc
middleware_mutex do
@registered_middleware[key] = value.call
end
end
end
end
end
lookup_middleware
このメソッドでは、指定されたキーに対応するミドルウェアクラスを検索します。
-
load_middleware(key)
メソッドを呼び出して、キーに対応するミドルウェアをロードしようとします。今回、key
には:url_encode
が入っています。 - もし
load_middleware(key)
がnil
を返した場合(つまり、キーに対応するミドルウェアが見つからなかった場合)、Faraday::Error
例外を発生させます。この例外メッセージには、キーが登録されていないことが含まれます。
load_middleware
このメソッドは、指定されたキーに対応するミドルウェアを登録済みのミドルウェアから取得し、必要に応じて登録を更新します。
-
value = registered_middleware[key]
- 指定されたキーに対応するミドルウェアを取得します。
- 今回、
value
にはregistered_middleware[:url_encoded]
で取得されたFaraday::Request::UrlEncoded
が代入されます。
-
case value
取得したミドルウェアの型に応じて処理を分岐します。 -
when Module
:- ミドルウェアがモジュールの場合、そのまま返します。
- 今回、
value
に格納されているFaraday::Request::UrlEncoded
はモジュールですので、そのまま、value
を返します。
ところで、なぜregistered_middleware[:url_encoded]
でFaraday::Request::UrlEncoded
を取得できるのでしょう?
registered_middleware
# faraday/middleware_registry.rb
def registered_middleware
@registered_middleware ||= {}
end
registered_middleware
は上記の通りとなっており、登録されたミドルウェア(@registered_middleware
)がなければ、空のハッシュを返すようになっています。
これまで見てきたコードの中で、ミドルウェアの登録をしているような処理は見当たりませんでしたが、関連しそうなファイルを探していると...faraday/request/url_encoded.rb
の最終行に下記のような記載が!!!
# faraday/request/url_encoded.rb
Faraday::Request.register_middleware(url_encoded: Faraday::Request::UrlEncoded)
Faraday::Request
にregister_middleware
メソッドは定義されていませんが、Faraday::Request
に下記のように記載されており、Faraday::Request::UrlEncoded
もFaraday::MiddlewareRegistry
モジュールのメソッドをクラスメソッドとして使用できるようになっています。
# faraday/request.rb
module Faraday
Request = Struct.new(:http_method, :path, :params, :headers, :body, :options) do
extend MiddlewareRegistry
# 省略
end
require 'faraday/request/url_encoded'
register_middleware
メソッドはFaraday::MiddlewareRegistry
に下記の通り定義されています。
# faraday/middleware_registry.rb
def register_middleware(**mappings)
middleware_mutex do
registered_middleware.update(mappings)
end
end
-
def register_middleware(**mappings)
**mappings
は、複数のキーバリューペアを受け取るハッシュ引数を意味します。 -
registered_middleware.update(mappings)
registered_middleware
メソッドを呼び出し、mappings
で渡されたミドルウェアを登録します。update
メソッドは、既存のハッシュに新しいキーと値を追加または更新します。
途中に出てきたmiddleware_mutex
は何をやっているのでしょうか?🤔
middleware_mutex
メソッドは下記のように定義されています。
# faraday/middleware_registry.rb
def middleware_mutex(&block)
@middleware_mutex ||= Monitor.new
@middleware_mutex.synchronize(&block)
end
Monitorクラス、初めて知りました。難しすぎる😂😂😂
自分なりに、大まかに下記のように理解しました...
-
Monitor
クラス- スレッド間の排他制御を行うための機能を提供してくれる
- 複数のスレッドが同時に同じデータやリソースを操作するのを防いでくれる
-
synchronize
メソッド- Monitorクラスが提供するメソッドで、ブロック内の処理を一度に一つのスレッドだけが実行できるようにする
- これにより、複数のスレッドが同時にデータを変更することを防ぐ
やっと、下記コードが何をしているか(何となく)理解することができました。
ただ、まだ疑問に残っていることがあります。この行はどのタイミングで実行されるのでしょうか?🤔
# faraday/request/url_encoded.rb
Faraday::Request.register_middleware(url_encoded: Faraday::Request::UrlEncoded)
copilotに聞いてみた回答は下記の通りでした
本当にそうなんでしょうか?🤔 確かめる方法がすぐに思いつかなかったので、お分かりの方いらっしゃいましたら、コメント等で教えていただけますと幸いです🙇♂️
ここまで確認してきてやっっっと、Faraday::RackBuilder
のbuild
メソッドの2行目のrequest(:url_encoded)
でFaraday::Request.lookup_middleware(:url_encode)
が実行され、Faraday::Request::UrlEncoded
が返ってくることがわかりました。
続いて、Faraday::RackBuilder
のbuild
メソッドの3行目を見ていきます。
Faraday::RackBuilder.newの続き
build(3行目)
# faraday/rack_builder.rb
def build
raise_if_locked
block_given? ? yield(self) : request(:url_encoded)
adapter(Faraday.default_adapter, **Faraday.default_adapter_options) unless @adapter
end
Faraday.default_adapter
は:net_http
、**Faraday.default_adapter_options
は何も設定していなければ空のハッシュとなります。
adapter
adapter
メソッドは下記の通り定義されています。
# faraday/rack_builder.rb
ruby2_keywords def adapter(klass = NO_ARGUMENT, *args, &block)
return @adapter if klass == NO_ARGUMENT || klass.nil?
klass = Faraday::Adapter.lookup_middleware(klass) if klass.is_a?(Symbol)
@adapter = self.class::Handler.new(klass, *args, &block)
end
-
ruby2_keywords def adapter(klass = NO_ARGUMENT, *args, &block)
-
klass
はアダプターのクラスを指定する引数で、デフォルト値はNO_ARGUMENT
です。*args
は可変長引数、&block
はブロックを受け取ります。 - 今回、
klass
は:net_http
となっています。
-
-
return @adapter if klass == NO_ARGUMENT || klass.nil?
-
klass
がNO_ARGUMENT
またはnil
の場合、既に設定されているアダプター(@adapter
)を返します。 - 今回、
klass
は:net_http
となっているので、この行は実行されません。
-
-
klass = Faraday::Adapter.lookup_middleware(klass) if klass.is_a?(Symbol)
-
klass
がシンボルの場合、対応するミドルウェアクラスをFaraday::Adapter.lookup_middleware
メソッドで検索し、klass
に代入します。 - 今回は、
klass
は:net_http
というシンボルなので、Faraday::Adapter.lookup_middleware(:net_http)
が実行され、結果はFaraday::Adapter::NetHttp
となります。
-
-
@adapter = self.class::Handler.new(klass, *args, &block)
-
klass
を使って新しいHandler
オブジェクトを作成し、@adapter
に設定します。 - 今回は、
Faraday::RackBuilder::Handler.new(Faraday::Adapter::NetHttp)
が実行されます。
-
Faraday::RackBuilder::Handler.new
Faraday::RackBuilder::Handler
のinitialize
は下記の通り定義されています。
ruby2_keywords def initialize(klass, *args, &block)
@name = klass.to_s
REGISTRY.set(klass) if klass.respond_to?(:name)
@args = args
@block = block
end
Faraday::RackBuilder::Handler.new(Faraday::Adapter::NetHttp)
を実行すると、以下のような挙動になります。
-
クラス名を文字列に変換して保存
-
@name = klass.to_s
の行で、渡されたクラスの名前を文字列に変換し、インスタンス変数@name
に保存します。 - ここでは、
@name
には"Faraday::Adapter::NetHttp"
が格納されます。
-
-
レジストリにクラスを登録
-
REGISTRY.set(klass) if klass.respond_to?(:name)
の行で、渡されたクラスがname
メソッドを持っている場合、そのクラスをレジストリに登録します。 -
Faraday::Adapter::NetHttp
はname
メソッドを持っているため、レジストリに登録されます。
-
やっと、Faraday:RackBuilder.new
のコードリーディングが終わりました。
ここから、Faraday:Connection.new
の続きを見ていきます。
Faraday:Connection.newの続き
# faraday/connection.rb
self.url_prefix = url || 'http:/'
上記のコードは、url
が与えられていればその値を、与えられていなければ 'http:/'
を url_prefix
に設定します。
# faraday/connection.rb
@params.update(options.params) if options.params
@headers.update(options.headers) if options.headers
上記のコードは、ConnectionOptions
オブジェクトから渡されたパラメータとヘッダーを、@paramsと@headersに追加または上書きしています。
続いて、下記のコードを見ていきます。
# faraday/connection.rb
initialize_proxy(url, options)
呼ばれているのはこのメソッドです。
# faraday/connection.rb
def initialize_proxy(url, options)
@manual_proxy = !!options.proxy
@proxy =
if options.proxy
ProxyOptions.from(options.proxy)
else
proxy_from_env(url)
end
end
このメソッドは、プロキシ設定を初期化するために使用されます。
-
プロキシ設定の確認
-
options.proxy
が設定されているかどうかを確認します。 - 設定されている場合、
@manual_proxy
をtrue
に設定します。
-
-
プロキシオプションの設定
-
options.proxy
が設定されている場合、その値をProxyOptions
オブジェクトに変換して@proxy
に設定します。 - 設定されていない場合、
proxy_from_env(url)
が実行されます。
-
今回は、options.proxy
がnilで、@manual_proxy
はfalse
、@proxy
はnil
だとして、これ以上深追いしないでおきます(限界が近い)。
続いて、下記の行を確認します。
# faraday/connection.rb
yield(self) if block_given?
Faraday.new
の中で、Faraday::Connection.new
はブロック付きで呼ばれていないので、今回この行は無視してよさそうです。
ついに最後の行です!
# faraday/connection.rb
USER_AGENT = "Faraday v#{VERSION}".freeze
@headers[:user_agent] ||= USER_AGENT
VERSION
は下記のように定義されています。
# faraday/version.rb
module Faraday
VERSION = '2.12.0'
end
よって、@headers[:user_agent]
に代入される値は、"Faraday v2.12.0"
となります。
これで、やっっっっっっとFaraday::Connection.new
の内部実装の確認が完了し、Faraday.new
のコードリーディングを終えることができました🙌
Faraday.new
の中身の3行読むのがこんなに大変だとは...😂
まとめ
この記事では、Faradayの概要、仕組み、ミドルウェア利用したリクエスト処理、ソースコードのコードリーディングについて、自分なりに学んだことをまとめてみました。
gemのコードリーディングを行うのは初めて、かなり苦戦しましたが、今まで知らなかった知識に沢山触れることができ、とても勉強になったので、これからも続けていけたらなと思います。
コードリーディングで新たに知った(学んだ)ことの例
- instance method Module#class_eval
- instance method BasicObject#instance_eval
- instance method Module#define_method
- class Struct
- class Monitor
- 擬似変数(
__FILE__
、__LINE__
) - メタプログラミングによる動的なメソッド定義
- 分岐の条件部分で=による代入の結果を値として使う場合は条件全体をかっこ( )で囲む
- メソッドの再帰的呼び出し
今回新たに知った知識について、まだ十分に理解できていない状態なので、各トピックごとに今後学んでいきたいです。メタプログラミングについては、この本が気になる..
以上、この記事の内容が、自分と同じレベル感の方の参考になれば嬉しいです!
最後までご覧いただきありがとうございました🙇♂️
Discussion