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