my_api_client gemのソースコードを読んでみた
はじめに
こんにちは、kmkntです。
ソースコードを読んでみたシリーズの第2回になります。
前回はこちらになります。
今回は題材としてmy_api_clientというgemを取り上げようと思います。弊社CTOが公開しているOSSで社内でも活用しています。
my_api_clientとは
一言でいうと、APIクライアントを作成するためのライブラリになります。
READMEの冒頭に下記のように書かれているとおり、
MyApiClient は API リクエストクラスを作成するための汎用的な機能を提供します。Sawyer や Faraday をベースにエラーハンドリングの機能を強化した構造になっています。
エラーハンドリングの機能が強化されていることが特徴で、実際に使っていてとても便利に感じています。
エラーハンドリング以外にもRSpecを書くためのスタブ機能など便利な機能があるのですが、今回はエラーハンドリングの機能に焦点を絞り、機能の紹介をして実装の詳細を見ていきたいと思います。
エラーハンドリングをどのように行うのかは、サンプルのソースコードを見てもらうのが早いかと思います。
class ExampleApiClient < MyApiClient::Base
endpoint 'https://example.com'
error_handling status_code: 400..499,
raise: MyApiClient::ClientError
error_handling status_code: 500..599, raise: MyApiClient::ServerError do |_params, logger|
logger.warn 'Server error occurred.'
end
error_handling json: { '$.errors.code': 10..19 },
raise: MyApiClient::ClientError,
with: :my_error_handling
# Omission
private
# @param params [MyApiClient::Params::Params] HTTP reqest and response params
# @param logger [MyApiClient::Request::Logger] Logger for a request processing
def my_error_handling(params, logger)
logger.warn "Response Body: #{params.response.body.inspect}"
end
end
error_handling
メソッドを使って、どのようにエラーハンドリングするかを定義していることがわかります。以降では、こちらのメソッドの実装を追っていきます。
ソースコードを読む
MyApiClient::Base
APIクライアントを実装するとき、MyApiClient::Base
を継承するので、まずはそちらから見ていきます。
エラーハンドリングに関して、ここで見るべきポイントがいくつかあります。
-
MyApiClient::ErrorHandling
の中にerror_handling
の実装があります。 -
MyApiClient::Exceptions
の中にretry_on
やdiscard_on
の実装があります。 -
MyApiClient::DefaultErrorHandlers
にデフォルトのエラーハンドリングが定義されています。
また、空配列を初期値としてclass_attributeで宣言されたerror_handlers
にエラーハンドリングを持たせることになります。
MyApiClient::DefaultErrorHandlers
見たとおりですが、ステータスコード400番台はClientError
およびそのサブクラス、500番台はServerError
およびそのサブクラスがraiseされることがわかります。また、NetworkError
の場合はリトライが実行されることがわかります。
MyApiClient::ErrorHandling
ここからerror_handling
の詳細を見ていきます。
RetryOptionProcessor
RetryOptionProcessor
のcall
メソッドにoptionsを渡して処理しているので、そちらを見てみます。
以下のようにerror_handling
にはretryオプションを定義できる仕様になっています。
error_handling json: { '$.errors.code': 20 },
raise: MyApiClient::ApiLimitError,
retry: { wait: 30.seconds, attempts: 3 }
error_handling json: { '$.errors.code': 20 },
raise: MyApiClient::ApiLimitError,
retry: true
error_handling_options.delete(:retry)
でretryの値となるHashもしくはtrueを取り出していることがわかります。verify_error_handling_options
で検証後にHash(trueの場合は空のHashに変換して)を返しています。
MyApiClient::ErrorHandling
側でRetryOptionProcessor
で処理されたオプションをretry_on
に渡しています。最終的にはretry_on
として別で定義した場合と同じメソッドが実行されることがわかります。
MyApiClient::Base
を継承したクラスは複数のerror_handling
を定義できます。それらをerror_handlers
にlambdaとして持たせています。error_handlers
に直接入れず、dup
で複製していますが、これはerror_handlers
がclass_attributeなので、MyApiClient::Base
を継承した他のクラスのerror_handlers
を汚染しないようにするためと思われます。
Generator
lambdaの中でGenerator
のcall
を実行しています。前振りが長くなったのですが、今回の記事の本題はここからになります。
少し脱線しますが、Generator
も先ほどのRetryOptionProcessor
もServiceAbstract
を継承しています。クラスメソッドであるcall
の中でインスタンス化してインスタンスメソッドのcall
を呼んでいます。
ですので、まずはinitialize
メソッドから見ていきます。
options[:raise]
の指定がなければ、MyApiClient::Error
が呼び出されることがわかります。
次のverify_and_set_arguments
メソッドでoptionsとして渡されたキーがARGUMENTS
に含まれるか検証した上で、instance_variable_set
で渡されたキーと値で動的に_
で始まるインスタンス変数を定義しています。attr_reader
でアクセサも定義されていますね。
call
メソッドを見ていきます。
まずmatch?
ですが、ここで実際のレスポンスのステータスコードとの比較をしています。
続いてmatch_headers?
ですが、ここでは実際のレスポンスのヘッダーとの比較ですね。比較の処理自体は先ほどのmatch?
で行われていることがわかります。
例えば、headers: { 'www-authenticate': /invalid token/ }
のように定義された場合は、実際のwww-authenticate
の値と/invalid token/
が比較されることになります。
最後にmatch_body?
ですが、ここでは実際のレスポンスボディのJSON文字列との比較になります。
json
はJsonPath
のシンタックスで定義できるので、そちらとの比較処理を先ほどと同じくmatch?
で行っています。
例えば、json: { '$.errors.code': 10..19 }
であれば、$.errors.code
で実際のボディの値を取り出し、10から19の間に含まれるかを見ています。
ステータスコード、ヘッダー、ボディの順に見ていって、定義と実際のレスポンスが一致しなければ何もせずnilを返しています。一致した場合にはgenerate_error_handler
に処理が移ります。
まず、blockが定義されていればblock_caller
が呼ばれます。
error_handling
には以下のようにblockを渡せるので、blockが渡された場合には、それを実行した上でerror_raiser
をcallするlambdaが定義されています。
error_handling status_code: 500..599, raise: MyApiClient::ServerError do |_params, logger|
logger.warn 'Server error occurred.'
end
次に、withが定義されていればmethod_caller
が呼ばれます。
error_handling
には以下のようにwithでメソッドを渡せるので、メソッドが渡された場合には、それを実行した上でerror_raiser
をcallするlambdaが定義されています。
error_handling json: { '$.errors.code': 10..19 },
raise: MyApiClient::ClientError,
with: :my_error_handling
最後に、error_raiser
ですが、blockもwithもなければ、これだけが呼ばれることになります。_raise
をraiseするlambdaですね。
再びMyApiClient::ErrorHandling
に戻るのですが、
Generator
のcall
メソッドにoptions
に加えてinstance
とresponse
も渡していました。response
は実際のレスポンスなので必要なのですが、instance
はmethod_caller
でwithで定義されたメソッドを呼び出すために必要だったことがわかりますね。
MyApiClient::Request
ここまでerror_handling
の実装を追ってみたのですが、APIを実行した際にerror_handlers
がどのように使われているかをさらっと見て終わりにしたいと思います。
MyApiClient::Request
から辿っていくと、APIを実行しているのはMyApiClient::Request::Executor
のcall
メソッドであることがわかります。
verify(response)
でレスポンスの検証をしています。
find_error_handler
を見ればいいようです。
error_handlers
をreverse_each
で定義した順とは逆順にcall
しています。call
した結果がnilの場合、つまりGenerator
のmatch?
で一致しなかった場合は何もせず、一致した場合はGenerator
のgenerate_error_handler
が呼び出されることになります。
さらに、ここまで見たことでlambdaを使って定義していた理由もわかったかと思います。ロードされた時点で実行するわけではなく、実際にAPIリクエストしてレスポンスが返ってきたタイミングで実行したいからですね。
おわりに
すべてを取り上げることはできなかったですが、error_handling
メソッドに焦点を当てて読んだ内容を記事にしてみました。
お役に立てば幸いです。
Discussion