🌐

雰囲気でgemを使ったことを反省して学びなおす 〜Faraday編〜

2024/11/16に公開

はじめに

現在携わっているプロジェクトで、Faradayを利用してコードを書く機会がありましたが、READMEや関連部分のソースコードをチラ見したくらいで、十分に理解して利用しているとは言えない状態でした。

きちんと理解した上で使いたい!という気持ちが芽生えたので、Faradayはそもそも何のか、その仕組みや使い方、内部の実装等を理解するために、公式ドキュメントの確認、挙動の確認やコードリーディング等を行いました。

gemのソースコードを詳細に読んでみるのは初めてで、自分の実力不足を痛感しまくったわけですが、学んだ内容をこの記事にまとめてみました。

Faradayを使ったことがないという方、使ったことはあるけどソースコードはチラ見したくらい...という方等の参考にになれば幸いです!

「自分なりにこう理解している」という内容を記載していますが、Web系エンジニアに転職してまだ1年目ということもあり、記載されている内容に誤りが含まれている可能性があります。

「この部分おかしいんじゃ?」「これってどういうこと?」等のご指摘やご質問がございましたら、コメント頂けますと幸いです🙏

Faradayとは

https://github.com/lostisland/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で利用されているミドルウェアの例です

  1. Rack::Runtime
    リクエストがどれくらいの時間で処理されたかを計測し、レスポンスヘッダーに追加する。
  2. ActionDispatch::Cookies
    クッキーを管理する。ユーザーごとに情報を保存し、次のリクエスト時に同じクッキー情報を使って再認証などができる。
  3. ActionDispatch::Session::CookieStore
    ユーザーのセッション情報をクッキーに保存する。セッション情報は、ユーザーがログインしているかどうかなどの状態を保持するために使われる。
  4. 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&param=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&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"=>"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&param=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: 接続オブジェクトをカスタマイズするためのブロック

処理の流れ

  1. オプションのマージ:

    options = Utils.deep_merge(default_connection_options, options)
    

    ここでは、デフォルトの接続オプションと指定されたオプションをマージしています。Utils.deep_mergeメソッドを使って、デフォルトの設定にユーザーが指定した設定を上書きします。

  2. 新しい接続オブジェクトの作成:

    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というクラスを継承していることを示しています。

また、OptionsStructのサブクラスになっています。

# 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については、今回のコードリーディングで初めて知りました。難しい...😇
https://docs.ruby-lang.org/ja/latest/class/Struct.html

Struct.newにブロックを指定した場合は、定義したStructをコンテキストにブロックを評価します。

OptionsStructのサブクラスとなっているため、Options.newのブロック内で書かれているoptionsmemoizedは、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については、今回初めて知りました。激ムズ...😇
https://docs.ruby-lang.org/ja/latest/method/Module/i/class_eval.html

class_evalを使うと、与えられた文字列内のRubyコードを、そのクラスのコンテキストで評価・実行します。

<<-RUBY ... RUBYは、ヒアドキュメントと呼ばれるもので、複数行の文字列を定義する方法です。

__FILE____LINE__は、擬似変数と呼ばれるもので、内容はそれぞれ下記の通りです。

  • __FILE__:現在実行中のソースファイルのファイル名を表す特殊な定数
  • __LINE__:現在実行中のソースコードの行番号を表す特殊な定数

以上を踏まえ、__FILE__, __LINE__ + 1class_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、初めて知りました😇
https://docs.ruby-lang.org/ja/latest/method/BasicObject/i/instance_eval.html

確認してきたことをまとめると、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メソッドは、sourcehashを再帰的にマージした新しいハッシュを返します。
deep_mergeメソッドの内部で、deep_merge!メソッドを呼び出し、条件次第では、deep_merge!メソッドの中で、deep_mergeが呼ばれます。

...ムズすぎる😂😂😂

再帰的処理の部分がかなり難しかったので、具体的な値が代入された場合の実行過程を考えてみました。

deep_mergeメソッドの具体的な実行過程と結果

sourcehashが下記の通りだとします。

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!メソッドの引数に渡されるtargethashは下記の通りです。

target = {
  request: { timeout: 30, open_timeout: 10 }
}

hash = {
  request: { timeout: 60, headers: { "User-Agent" => "Faraday" } }
}

deep_merge!(target, hash)を実行すると、hash.eachによって、hashの各keyvalueが順に処理されます。

  key::request
  value:{ timeout: 60, headers: { "User-Agent" => "Faraday" } }

次に、下記の条件式を確認していきます。

if value.is_a?(Hash) && (target[key].is_a?(Hash) || target[key].is_a?(Options))

valuetarget[: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の各keyvalue、処理は下記の通りです。

  • 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.newoptions引数が渡された場合、default_connection_optionsoptionsで渡された値を反映したハッシュでConnectionOptionsインスタンスが作成されます。

続いて、下記の部分を見ていきます。

if url.is_a?(Hash) || url.is_a?(ConnectionOptions)
  options = Utils.deep_merge(options, url)
  url     = options.url
end

urlがハッシュまたはConnectionOptionsクラスのインスタンスである場合に、optionsを更新し、urlを設定するためのものです。これにより、urloptionsの両方が設定され、後続の処理で使用できるようになります。

続いて、下記の部分を見ていきます。

@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 クラスのインスタンス変数を初期化しています。

  1. @parallel_manager = nil
    @parallel_manager は並列リクエストを管理するための変数です。初期値として nil を設定しています。

  2. @headers = Utils::Headers.new
    @headers はHTTPリクエストのヘッダーを格納するための変数です。Utils::Headers クラスの新しいインスタンスを作成して初期化しています。

  3. @params = Utils::ParamsHash.new
    @params はURIクエリパラメータを格納するための変数です。Utils::ParamsHash クラスの新しいインスタンスを作成して初期化しています。

  4. @options = options.request
    @options はリクエストオプションを格納するための変数です。引数として渡された ConnectionOptions オブジェクトの request プロパティを使用して初期化しています。

  5. @ssl = options.ssl
    @ssl はSSLオプションを格納するための変数です。引数として渡された ConnectionOptions オブジェクトの ssl プロパティを使用して初期化しています。

  6. @default_parallel_manager = options.parallel_manager
    @default_parallel_manager はデフォルトの並列マネージャを格納するための変数です。引数として渡された ConnectionOptions オブジェクトの parallel_manager プロパティを使用して初期化しています。

  7. @manual_proxy = nil
    @manual_proxy はプロキシ設定が手動で行われたかどうかを示すフラグです。初期値として nil を設定しています。

続いて下記部分を見ていきます。

@builder = options.builder || begin
  # Builderがデフォルトのミドルウェアを仮定しないように、空のブロックを渡す
  options.new_builder(block_given? ? proc { |b| } : nil)
end

このコードは、Faraday::Connectionのミドルウェアスタックを適切に初期化するためのものです。options.builderが指定されていない場合、デフォルトのミドルウェアが設定されないようにしています。

  1. @builder = options.builder || begin

    • @builderは、Faraday::Connectionのミドルウェアスタックを管理するオブジェクトです。
    • options.builderが存在する場合、それを@builderに設定します。
    • 存在しない場合、beginブロック内のコードを実行して@builderを設定します。
  2. # pass an empty block to Builder so it doesn't assume default middleware
    Builderがデフォルトのミドルウェアを仮定しないように、空のブロックを渡すことを説明しています。

  3. 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が設定されているため、上記のコードでは、RackBuilderinitializeメソッドが実行されます。

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

このメソッドは、ミドルウェアスタックを構築するために使用されます。

  1. raise_if_locked

    • この行は、ミドルウェアスタックがロックされているかどうかを確認します。ロックされている場合、変更は許可されません。
  2. block_given? ? yield(self) : request(:url_encoded)

    • ここでは、ブロックが渡されているかどうかをチェックします。
    • ブロックが渡されている場合、そのブロックを実行し、selfを引数として渡します。
    • ブロックが渡されていない場合、デフォルトのリクエストミドルウェア :url_encoded を使用します。
  3. adapter(Faraday.default_adapter, **Faraday.default_adapter_options) unless @adapter

    • この行は、アダプターが設定されていない場合 (@adapternil)、デフォルトのアダプターとそのオプションを設定します。

request(:url_encoded)の部分では、下記のrequestメソッドが実行されます。

ruby2_keywords def request(key, *args, &block)
  use_symbol(Faraday::Request, key, *args, &block)
end

requestメソッドは、指定されたキーに基づいてミドルウェアをスタックに追加するためのメソッドです。このメソッドは、use_symbolメソッドを呼び出して、Faraday::Requestモジュールからミドルウェアを見つけて追加します。

  1. ruby2_keywords
    これは、Ruby 2.7以降でキーワード引数の互換性を保つためのキーワードです。

  2. def request(key, *args, &block)

    • requestという名前のメソッドを定義しています。
    • keyは、リクエストミドルウェアの種類を指定するための引数です。
    • *argsは、可変長引数で、任意の数の引数を受け取ることができます。
    • &blockは、ブロックを引数として受け取ります。
  3. 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メソッドに渡して追加します。

  1. def use_symbol(mod, key, *args, &block):
    modはモジュール、keyはシンボル、*argsは可変長引数、&blockはブロックを受け取ります。

  2. 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

このメソッドでは、指定されたキーに対応するミドルウェアクラスを検索します。

  1. load_middleware(key)メソッドを呼び出して、キーに対応するミドルウェアをロードしようとします。今回、keyには:url_encodeが入っています。
  2. もしload_middleware(key)nilを返した場合(つまり、キーに対応するミドルウェアが見つからなかった場合)、Faraday::Error例外を発生させます。この例外メッセージには、キーが登録されていないことが含まれます。

load_middleware

このメソッドは、指定されたキーに対応するミドルウェアを登録済みのミドルウェアから取得し、必要に応じて登録を更新します。

  1. value = registered_middleware[key]

    • 指定されたキーに対応するミドルウェアを取得します。
    • 今回、valueにはregistered_middleware[:url_encoded]で取得されたFaraday::Request::UrlEncodedが代入されます。
  2. case value
    取得したミドルウェアの型に応じて処理を分岐します。

  3. 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::Requestregister_middlewareメソッドは定義されていませんが、Faraday::Requestに下記のように記載されており、Faraday::Request::UrlEncodedFaraday::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
  1. def register_middleware(**mappings)
    **mappingsは、複数のキーバリューペアを受け取るハッシュ引数を意味します。
  2. 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クラス、初めて知りました。難しすぎる😂😂😂
https://docs.ruby-lang.org/ja/latest/class/Monitor.html#I_MON_SYNCHRONIZE

自分なりに、大まかに下記のように理解しました...

  • Monitorクラス
    • スレッド間の排他制御を行うための機能を提供してくれる
    • 複数のスレッドが同時に同じデータやリソースを操作するのを防いでくれる
  • synchronizeメソッド
    • Monitorクラスが提供するメソッドで、ブロック内の処理を一度に一つのスレッドだけが実行できるようにする
    • これにより、複数のスレッドが同時にデータを変更することを防ぐ

やっと、下記コードが何をしているか(何となく)理解することができました。
ただ、まだ疑問に残っていることがあります。この行はどのタイミングで実行されるのでしょうか?🤔

# faraday/request/url_encoded.rb

Faraday::Request.register_middleware(url_encoded: Faraday::Request::UrlEncoded)

copilotに聞いてみた回答は下記の通りでした

本当にそうなんでしょうか?🤔 確かめる方法がすぐに思いつかなかったので、お分かりの方いらっしゃいましたら、コメント等で教えていただけますと幸いです🙇‍♂️

ここまで確認してきてやっっっと、Faraday::RackBuilderbuildメソッドの2行目のrequest(:url_encoded)Faraday::Request.lookup_middleware(:url_encode)が実行され、Faraday::Request::UrlEncodedが返ってくることがわかりました。

続いて、Faraday::RackBuilderbuildメソッドの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
  1. ruby2_keywords def adapter(klass = NO_ARGUMENT, *args, &block)

    • klassはアダプターのクラスを指定する引数で、デフォルト値はNO_ARGUMENTです。*argsは可変長引数、&blockはブロックを受け取ります。
    • 今回、klass:net_httpとなっています。
  2. return @adapter if klass == NO_ARGUMENT || klass.nil?

    • klassNO_ARGUMENTまたはnilの場合、既に設定されているアダプター(@adapter)を返します。
    • 今回、klass:net_httpとなっているので、この行は実行されません。
  3. 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となります。
  4. @adapter = self.class::Handler.new(klass, *args, &block)

    • klassを使って新しいHandlerオブジェクトを作成し、@adapterに設定します。
    • 今回は、Faraday::RackBuilder::Handler.new(Faraday::Adapter::NetHttp)が実行されます。

Faraday::RackBuilder::Handler.new

Faraday::RackBuilder::Handlerinitializeは下記の通り定義されています。

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)を実行すると、以下のような挙動になります。

  1. クラス名を文字列に変換して保存

    • @name = klass.to_sの行で、渡されたクラスの名前を文字列に変換し、インスタンス変数@nameに保存します。
    • ここでは、@nameには"Faraday::Adapter::NetHttp"が格納されます。
  2. レジストリにクラスを登録

    • REGISTRY.set(klass) if klass.respond_to?(:name)の行で、渡されたクラスがnameメソッドを持っている場合、そのクラスをレジストリに登録します。
    • Faraday::Adapter::NetHttpnameメソッドを持っているため、レジストリに登録されます。

やっと、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

このメソッドは、プロキシ設定を初期化するために使用されます。

  1. プロキシ設定の確認

    • options.proxy が設定されているかどうかを確認します。
    • 設定されている場合、@manual_proxytrue に設定します。
  2. プロキシオプションの設定

    • options.proxy が設定されている場合、その値を ProxyOptions オブジェクトに変換して @proxy に設定します。
    • 設定されていない場合、proxy_from_env(url)が実行されます。

今回は、options.proxyがnilで、@manual_proxyfalse@proxynilだとして、これ以上深追いしないでおきます(限界が近い)。

続いて、下記の行を確認します。

# 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__
  • メタプログラミングによる動的なメソッド定義
  • 分岐の条件部分で=による代入の結果を値として使う場合は条件全体をかっこ( )で囲む
  • メソッドの再帰的呼び出し

今回新たに知った知識について、まだ十分に理解できていない状態なので、各トピックごとに今後学んでいきたいです。メタプログラミングについては、この本が気になる..
https://www.oreilly.co.jp/books/9784873117430/

以上、この記事の内容が、自分と同じレベル感の方の参考になれば嬉しいです!

最後までご覧いただきありがとうございました🙇‍♂️

合同会社春秋テックブログ

Discussion