From 3f205f4ac9bdd1a540eadd055e6050a2bac9080f Mon Sep 17 00:00:00 2001 From: ryz310 Date: Sun, 1 Mar 2026 17:34:55 +0900 Subject: [PATCH 1/3] docs: fill WIP sections in README files --- README.jp.md | 62 +++++++++++++- README.md | 236 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 290 insertions(+), 8 deletions(-) diff --git a/README.jp.md b/README.jp.md index 6afb0d59..4ac94772 100644 --- a/README.jp.md +++ b/README.jp.md @@ -289,7 +289,29 @@ error_handling status_code: 200, json: :forbid_nil #### MyApiClient::Params::Params -WIP +`MyApiClient::Params::Params` は、リクエスト情報とレスポンス情報をひとつにまとめるための Value Object です。 +このインスタンスはエラーハンドリング(`block` / `with`)に渡され、`MyApiClient::Error#params` からも参照できます。 + +- `#request`: `MyApiClient::Params::Request`(HTTP method, URL, headers, body) +- `#response`: `Sawyer::Response`(ネットワークエラー時は `nil`) + +また、`#metadata`(`#to_bugsnag` の alias)を使うと、通知やログに使いやすい形式でリクエスト/レスポンス情報を取得できます。 + +```ruby +begin + api_client.request +rescue MyApiClient::Error => e + e.params.metadata + # => { + # request_line: "GET https://example.com/v1/users?search=foo", + # request_headers: { "Authorization" => "Bearer token" }, + # response_status: 429, + # response_headers: { "content-type" => "application/json" }, + # response_body: { errors: [{ code: 20 }] }, + # duration: 0.123 + # } +end +``` #### MyApiClient::Error @@ -383,11 +405,45 @@ end ### Timeout -WIP +API Client クラスごとに HTTP タイムアウトを設定できます。 + +```ruby +class ApplicationApiClient < MyApiClient::Base + http_open_timeout 2.seconds + http_read_timeout 3.seconds +end +``` + +- `http_open_timeout`: 接続確立までの待機時間 +- `http_read_timeout`: レスポンス読み取り時の待機時間 + +これらは内部的に Faraday の `open_timeout` / `timeout` に渡されます。 +タイムアウトが発生した場合は `MyApiClient::NetworkError` として扱われます。 ### Logger -WIP +各 API Client クラスは `self.logger` を設定できます。 +デフォルトは `Logger.new($stdout)` で、Rails では次のように設定するのが一般的です。 + +```ruby +class ApplicationApiClient < MyApiClient::Base + self.logger = Rails.logger +end +``` + +実際のログ出力は `MyApiClient::Request::Logger` がラップし、HTTP method と URL を含めた形式で出力します。 + +```text +API request `GET https://example.com/v1/users`: "Start" +API request `GET https://example.com/v1/users`: "Duration 100.0 msec" +API request `GET https://example.com/v1/users`: "Success (200)" +``` + +失敗時は以下のように出力されます。 + +```text +API request `GET https://example.com/v1/users`: "Failure (Net::OpenTimeout)" +``` ## One request for one class diff --git a/README.md b/README.md index 9a1a3200..2cf77409 100644 --- a/README.md +++ b/README.md @@ -280,7 +280,29 @@ It seems that some services expect an empty Response Body to be returned from th #### MyApiClient::Params::Params -WIP +`MyApiClient::Params::Params` is a value object that combines request and response details. +An instance of this class is passed to error handlers (`block`/`with`) and is also available from `MyApiClient::Error#params`. + +- `#request`: `MyApiClient::Params::Request` (method, URL, headers, and body) +- `#response`: `Sawyer::Response` (or `nil` for network errors) + +It also provides `#metadata` (`#to_bugsnag` alias), which merges request/response data into a single hash for logging and error reporting. + +```ruby +begin + api_client.request +rescue MyApiClient::Error => e + e.params.metadata + # => { + # request_line: "GET https://example.com/v1/users?search=foo", + # request_headers: { "Authorization" => "Bearer token" }, + # response_status: 429, + # response_headers: { "content-type" => "application/json" }, + # response_body: { errors: [{ code: 20 }] }, + # duration: 0.123 + # } +end +``` #### MyApiClient::Error @@ -374,11 +396,45 @@ Note that a normal exception is raised depending on the result of the request, b ### Timeout -WIP +You can configure HTTP timeout values per API client class: + +```ruby +class ApplicationApiClient < MyApiClient::Base + http_open_timeout 2.seconds + http_read_timeout 3.seconds +end +``` + +- `http_open_timeout`: maximum wait time to open a connection +- `http_read_timeout`: maximum wait time for each HTTP read + +Internally, these are passed to Faraday request options as `open_timeout` and `timeout`. +If a timeout occurs, it is wrapped and raised as `MyApiClient::NetworkError`. ### Logger -WIP +Each API client class has a configurable logger (`self.logger`). +By default, MyApiClient uses `Logger.new($stdout)`, and in Rails apps you typically set: + +```ruby +class ApplicationApiClient < MyApiClient::Base + self.logger = Rails.logger +end +``` + +MyApiClient wraps this logger with `MyApiClient::Request::Logger` and prefixes messages with request information: + +```text +API request `GET https://example.com/v1/users`: "Start" +API request `GET https://example.com/v1/users`: "Duration 100.0 msec" +API request `GET https://example.com/v1/users`: "Success (200)" +``` + +On failure, it logs: + +```text +API request `GET https://example.com/v1/users`: "Failure (Net::OpenTimeout)" +``` ## RSpec @@ -424,11 +480,181 @@ class ExampleApiClient < MyApiClient::Base end ``` -WIP +When you define a new API client, these are the two main test targets: + +1. It sends the expected HTTP request (method, URL, headers/query/body) +2. It handles error responses as expected (`error_handling`) + +MyApiClient provides custom matchers for both. + +#### 1. Request assertion (`request_to` + `with`) + +Use `request_to` to assert method/URL and `with` to assert `headers`, `query`, or `body`. +`expect` must receive a block. + +```ruby +RSpec.describe ExampleApiClient, type: :api_client do + let(:api_client) { described_class.new(access_token: 'access token') } + let(:headers) do + { + 'Content-Type': 'application/json;charset=UTF-8', + 'Authorization': 'Bearer access token', + } + end + + describe '#get_users' do + it do + expect { api_client.get_users(condition: 'condition') } + .to request_to(:get, 'https://example.com/v1/users') + .with(headers: headers, query: { search: 'condition' }) + end + end +end +``` + +#### 2. Error handling assertion (`be_handled_as_an_error` + `when_receive`) + +Use `be_handled_as_an_error` to assert the raised error class, and `when_receive` to provide mock response input (`status_code`, `headers`, `body`). + +```ruby +it do + expect { api_client.get_users(condition: 'condition') } + .to be_handled_as_an_error(MyApiClient::ClientError) + .when_receive(status_code: 200, body: { errors: { code: 10 } }.to_json) +end +``` + +You can also assert that a response is *not* handled as an error: + +```ruby +it do + expect { api_client.get_users(condition: 'condition') } + .not_to be_handled_as_an_error(MyApiClient::ClientError) + .when_receive(status_code: 200, body: { users: [{ id: 1 }] }.to_json) +end +``` + +If the client has `retry_on`, you can assert retry count with `after_retry(...).times`: + +```ruby +it do + expect { api_client.get_users(condition: 'condition') } + .to be_handled_as_an_error(MyApiClient::ApiLimitError) + .after_retry(3).times + .when_receive(status_code: 200, body: { errors: { code: 20 } }.to_json) +end +``` ### Stubbing -WIP +Use `stub_api_client_all` or `stub_api_client` to stub API client methods without real HTTP. + +#### `response` option + +```ruby +class ExampleApiClient < MyApiClient::Base + endpoint 'https://example.com' + + def request(user_id:) + get "users/#{user_id}" + end +end + +stub_api_client_all( + ExampleApiClient, + request: { response: { id: 12_345 } } +) + +response = ExampleApiClient.new.request(user_id: 1) +response.id # => 12345 +``` + +`response` can be omitted as shorthand: + +```ruby +stub_api_client_all( + ExampleApiClient, + request: { id: 12_345 } +) +``` + +#### Proc response + +You can generate response data from request arguments: + +```ruby +stub_api_client_all( + ExampleApiClient, + request: ->(params) { { id: params[:user_id] } } +) +``` + +#### Return value of `stub_api_client_all` / `stub_api_client` + +Both methods return a spy object, so you can assert received calls: + +```ruby +def execute_api_request + ExampleApiClient.new.request(user_id: 1) +end + +api_client = stub_api_client_all(ExampleApiClient, request: nil) +execute_api_request +expect(api_client).to have_received(:request).with(user_id: 1) +``` + +#### `raise` option + +To test error paths, use the `raise` option: + +```ruby +stub_api_client_all(ExampleApiClient, request: { raise: MyApiClient::Error }) +expect { ExampleApiClient.new.request(user_id: 1) }.to raise_error(MyApiClient::Error) +``` + +You can combine `raise`, `response`, and `status_code`: + +```ruby +stub_api_client_all( + ExampleApiClient, + request: { + raise: MyApiClient::Error, + response: { message: 'error' }, + status_code: 429, + } +) + +begin + ExampleApiClient.new.request(user_id: 1) +rescue MyApiClient::Error => e + e.params.response.data.to_h # => { message: "error" } + e.params.response.status # => 429 +end +``` + +#### `pageable` option + +For `#pageable_get` (`#pget`), you can stub page-by-page responses: + +```ruby +stub_api_client_all( + MyPaginationApiClient, + pagination: { + pageable: [ + { page: 1 }, + { page: 2 }, + { page: 3 }, + ], + } +) + +MyPaginationApiClient.new.pagination.each do |response| + response.page #=> 1, 2, 3 +end +``` + +Each page entry supports the same options (`response`, `raise`, `Proc`, etc.). +You can also pass an `Enumerator` for endless pagination stubs. ## Development From ec92b64496a6196524c2c5056302640301e18fca Mon Sep 17 00:00:00 2001 From: ryz310 Date: Sun, 1 Mar 2026 17:52:41 +0900 Subject: [PATCH 2/3] docs: refine README wording in English and Japanese --- README.jp.md | 54 +++++++++++++++--------------- README.md | 94 ++++++++++++++++++++++++++-------------------------- 2 files changed, 74 insertions(+), 74 deletions(-) diff --git a/README.jp.md b/README.jp.md index 4ac94772..6f4512b6 100644 --- a/README.jp.md +++ b/README.jp.md @@ -10,7 +10,7 @@ MyApiClient は API リクエストクラスを作成するための汎用的な ただし、 Sawyer はダミーデータの作成が難しかったり、他の gem で競合することがよくあるので、将来的には依存しないように変更していくかもしれません。 -また、 Ruby on Rails で利用することを想定してますが、それ以外の環境でも動作するように作っています。不具合などあれば Issue ページからご報告下さい。 +また、 Ruby on Rails での利用を想定していますが、それ以外の環境でも動作するように作っています。不具合などがあれば Issue ページからご報告ください。 [toc] @@ -63,7 +63,7 @@ class ExampleApiClient < MyApiClient::Base # POST https://example.com/v1/users # - # @param name [String] Username which want to create + # @param name [String] Username to create # @return [Sawyer::Resource] HTTP resource parameter def post_user(name:) post 'users', headers: headers, body: { name: name } @@ -79,11 +79,11 @@ class ExampleApiClient < MyApiClient::Base end end -api_clinet = ExampleApiClient.new(access_token: 'access_token') -api_clinet.get_users #=> # +api_client = ExampleApiClient.new(access_token: 'access_token') +api_client.get_users #=> # ``` -クラス定義の最初に記述される `endpoint` にはリクエスト URL の共通部分を定義します。後述の各メソッドで後続の path を定義しますが、上記の例だと `get 'users'` と定義すると、 `GET https://example.com/v1/users` というリクエストが実行されます。 +クラス定義の最初に記述する `endpoint` には、リクエスト URL の共通部分を定義します。後述の各メソッドで後続のパスを定義し、上記の例で `get 'users'` と書くと `GET https://example.com/v1/users` というリクエストが実行されます。 次に、 `#initialize` を定義します。上記の例のように Access Token や API Key などを設定することを想定します。必要なければ定義の省略も可能です。 @@ -112,9 +112,9 @@ class MyPaginationApiClient < ApplicationApiClient end ``` -上記の例の場合、最初に `GET https://example.com/v1/pagination?page=1` に対してリクエストが実行され、続けてレスポンス JSON の `$.link.next` に含まれる URL に対して enumerable にリクエストを実行します。 +上記の例では、最初に `GET https://example.com/v1/pagination?page=1` に対してリクエストが実行され、続けてレスポンス JSON の `$.links.next` に含まれる URL に対して enumerable にリクエストを実行します。 -例えば以下のようなレスポンスであれば、`$.link.next` は `"https://example.com/pagination?page=3"` を示します。 +例えば以下のようなレスポンスであれば、`$.links.next` は `"https://example.com/pagination?page=3"` を示します。 ```json { @@ -129,12 +129,12 @@ end そして `#pageable_get` は [Enumerator::Lazy](https://docs.ruby-lang.org/ja/latest/class/Enumerator=3a=3aLazy.html) を返すので、 `#each` や `#next` を実行することで次の結果を取得できます。 ```ruby -api_clinet = MyPaginationApiClient.new -api_clinet.pagination.each do |response| +api_client = MyPaginationApiClient.new +api_client.pagination.each do |response| # Do something. end -result = api_clinet.pagination +result = api_client.pagination result.next # => 1st page result result.next # => 2nd page result result.next # => 3rd page result @@ -174,7 +174,7 @@ class ExampleApiClient < MyApiClient::Base private - # @param params [MyApiClient::Params::Params] HTTP reqest and response params + # @param params [MyApiClient::Params::Params] HTTP request 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}" @@ -204,10 +204,10 @@ error_handling status_code: 500..599, raise: MyApiClient::ServerError do |_param end ``` -上記の例であれば、ステータスコードが `500..599` の場合に `MyApiClient::ServerError` を発生させる前に `block` の内容が実行れます。引数の `params` にはリクエスト情報とレスポンス情報が含まれています。`logger` はログ出力用インスタンスですが、このインスタンスを使ってログ出力すると、以下のようにリクエスト情報がログ出力に含まれるようになり、デバッグの際に便利です。 +上記の例であれば、ステータスコードが `500..599` の場合に `MyApiClient::ServerError` を発生させる前に `block` の内容が実行されます。引数の `params` にはリクエスト情報とレスポンス情報が含まれています。`logger` はログ出力用インスタンスですが、このインスタンスを使ってログ出力すると、以下のようにリクエスト情報がログ出力に含まれるようになり、デバッグの際に便利です。 ```text -API request `GET https://example.com/path/to/resouce`: "Server error occurred." +API request `GET https://example.com/path/to/resource`: "Server error occurred." ``` `json` には `Hash` の Key に [JSONPath](https://goessner.net/articles/JsonPath/) を指定して、レスポンス JSON から任意の値を取得し、 Value とマッチするかどうかでエラーハンドリングできます。Value には `String` `Integer` `Range` `Regexp` が指定可能です。上記の場合であれば、以下のような JSON にマッチします。 @@ -218,7 +218,7 @@ error_handling json: { '$.errors.code': 10..19 }, with: :my_error_handling ```json { - "erros": { + "errors": { "code": 10, "message": "Some error has occurred." } @@ -252,7 +252,7 @@ end #### Default error handling -`my_api_client` では、標準でステータスコード 400 ~ 500 番台のレスポンスを例外として処理するようにしています。ステータスコードが 400 番台場合は `MyApiClient::ClientError`、 500 番台の場合は `MyApiClient::ServerError` を継承した例外クラスが raise されます。 +`my_api_client` では、標準でステータスコード 400 ~ 500 番台のレスポンスを例外として処理するようにしています。ステータスコードが 400 番台の場合は `MyApiClient::ClientError`、500 番台の場合は `MyApiClient::ServerError` を継承した例外クラスが raise されます。 また、 `MyApiClient::NetworkError` に対しても標準で `retry_on` が定義されています。 @@ -272,7 +272,7 @@ error_handling json: { '$.errors.code': :negative? } ```json { - "erros": { + "errors": { "code": -1, "message": "Some error has occurred." } @@ -285,7 +285,7 @@ error_handling json: { '$.errors.code': :negative? } error_handling status_code: 200, json: :forbid_nil ``` -一部のサービスではサーバーから何らかの Response Body が返ってくる事を期待しているにも関わらず、空の結果が結果が返ってくるというケースがあるようです。こちらも実験的な機能ですが、そういったケースを検出するために `json: :forbid_nil` オプションを用意しました。通常の場合、Response Body が空の場合はエラー判定をしませんが、このオプションを指定するとエラーとして検知する様になります。正常応答が空となる API も存在するので、誤検知にご注意下さい。 +一部のサービスではサーバーから何らかの Response Body が返ってくることを期待しているにも関わらず、空の結果が返ってくるケースがあります。こちらも実験的な機能ですが、そういったケースを検出するために `json: :forbid_nil` オプションを用意しました。通常は Response Body が空の場合にエラー判定しませんが、このオプションを指定するとエラーとして検知するようになります。正常応答が空となる API も存在するので、誤検知にはご注意ください。 #### MyApiClient::Params::Params @@ -447,7 +447,7 @@ API request `GET https://example.com/v1/users`: "Failure (Net::OpenTimeout)" ## One request for one class -多くの場合、同一ホストの API は リクエストヘッダーやエラー情報が同じ構造になっているため、上記のように一つのクラス内に複数の API を定義する設計が理にかなっていますが、 API 毎に個別に定義したい場合は、以下のように 1 つのクラスに 1 の API という構造で設計することも可能です。 +多くの場合、同一ホストの API はリクエストヘッダーやエラー情報が同じ構造になっているため、上記のように 1 つのクラス内に複数の API を定義する設計が理にかなっています。一方で、API ごとに個別に定義したい場合は、以下のように「1 クラス 1 API」という構造で設計することも可能です。 ```ruby class ExampleApiClient < MyApiClient::Base @@ -487,7 +487,7 @@ class PostUserApiClient < ExampleApiClient # POST https://example.com/users # - # @param name [String] Username which want to create + # @param name [String] Username to create # @return [Sawyer::Resource] HTTP resource parameter def request(name:) post 'users', headers: headers, body: { name: name } @@ -548,7 +548,7 @@ end #### 1. 指定したエンドポイントに対して、任意のパラメータを使ったリクエストが実行されること -例えば上述の `#get_users` の内部では、入力引数を用いて検索クエリが組み立てられていたり、 Header に `access_token` を利用したりしています。これらの値が正しくリクエストに用いられているかどうかのテストが必要となります。 +例えば上述の `#get_users` の内部では、入力引数を用いて検索クエリが組み立てられていたり、ヘッダーに `access_token` を利用したりしています。これらの値が正しくリクエストに使われているかどうかをテストする必要があります。 この場合 `request_to` と `with` という Custom Matcher を利用することで簡単にテストを記述することが出来ます。 `expect` にはブロック `{}` を指定する必要がある点にご注意ください。他にも `with` には `body` というキーワード引数も指定できます。 @@ -566,7 +566,7 @@ RSpec.describe ExampleApiClient, type: :api_client do it do expect { api_client.get_users(condition: 'condition') } .to request_to(:get, 'https://example.com/v1/users') - .with(headers: headers, query: { condition: 'condition' }) + .with(headers: headers, query: { search: 'condition' }) end end end @@ -596,7 +596,7 @@ end it do expect { api_client.get_users(condition: 'condition') } .not_to be_handled_as_an_error(MyApiClient::ClientError) - .when_receive(status_code: 200, body: { users: { id: 1 } }.to_json) + .when_receive(status_code: 200, body: { users: [{ id: 1 }] }.to_json) end ``` @@ -666,7 +666,7 @@ response.id # => 12345 #### Proc -リクスエストパラメータを使ったレスポンスを返すようにスタブ化したい場合は、 `Proc` を利用することで実現できます。 +リクエストパラメータを使ったレスポンスを返すようにスタブ化したい場合は、`Proc` を利用することで実現できます。 ```ruby stub_api_client_all( @@ -687,7 +687,7 @@ def execute_api_request ExampleApiClient.new.request(user_id: 1) end -api_clinet = stub_api_client_all(ExampleApiClient, request: nil) +api_client = stub_api_client_all(ExampleApiClient, request: nil) execute_api_request expect(api_client).to have_received(:request).with(user_id: 1) ``` @@ -792,7 +792,7 @@ $ docker compose run --rm test bundle exec rspec $ docker compose down --volumes --remove-orphans ``` -integration spec のみ実行したい場合は以下を使ってください。 +integration specs のみ実行したい場合は以下を使ってください。 ```sh $ docker compose up -d --build my_api @@ -805,7 +805,7 @@ $ docker compose down --volumes --remove-orphans この gem のリリースには [gem_comet](https://github.com/ryz310/gem_comet) を利用しています。 `gem_comet` の README.md にも使い方が載っていますが、備忘録のため、こちらにもリリースフローを記載しておきます。 -### Preparement +### Preparation 以下のコマンドで `.envrc` を作成し、 `GITHUB_ACCESS_TOKEN` を設定します。 @@ -856,7 +856,7 @@ See: [33a2d17](https://github.com/ryz310/my_api_client/pull/297/commits/33a2d170 この PR にこれからリリースする gem に対する変更が全て載っています。 変更内容の最終確認をして、 CI も通ったことを確認したら `Release v{VERSION}` を merge します。 -あとは Circle CI 側で gem のリリースが自動実行されるので、暫く待ちましょう。 +あとは Circle CI 側で gem のリリースが自動実行されるので、しばらく待ちましょう。 お疲れさまでした :tea: diff --git a/README.md b/README.md index 2cf77409..89eb1037 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ # MyApiClient -This gem is an API client builder. It provides generic functionality for creating API request classes. It has a structure based on [Sawyer](https://github.com/lostisland/sawyer) and [Faraday](https://github.com/lostisland/faraday) with enhanced error handling functions. +This gem is an API client builder that provides generic functionality for defining API request classes. Its architecture is based on [Sawyer](https://github.com/lostisland/sawyer) and [Faraday](https://github.com/lostisland/faraday), with enhanced error-handling features. -It is supposed to be used in Ruby on Rails, but it is made to work in other environments. If you have any problems, please report them from the Issue page. +It is primarily designed for Ruby on Rails, but it also works in other environments. If you find any issues, please report them on the Issues page. [toc] @@ -27,7 +27,7 @@ Add this line to your application's Gemfile: gem 'my_api_client' ``` -If you are using Ruby on Rails, you can use the `generator` function. +If you are using Ruby on Rails, you can use the generator. ```sh $ rails g api_client path/to/resource get:path/to/resource --endpoint https://example.com @@ -63,7 +63,7 @@ class ExampleApiClient < MyApiClient::Base # POST https://example.com/v1/users # - # @param name [String] Username which want to create + # @param name [String] Username to create # @return [Sawyer::Resource] HTTP resource parameter def post_user(name:) post 'users', headers: headers, body: { name: name } @@ -79,21 +79,21 @@ class ExampleApiClient < MyApiClient::Base end end -api_clinet = ExampleApiClient.new(access_token: 'access_token') -api_clinet.get_users #=> # +api_client = ExampleApiClient.new(access_token: 'access_token') +api_client.get_users #=> # ``` -The `endpoint` defines the intersection of the request URL. Each method described below defines a subsequent path. In the above example, `get 'users'` will request to `GET https://example.com/v1/users`. +`endpoint` defines the base URL for requests. Each method then adds a relative path. In the example above, `get 'users'` sends `GET https://example.com/v1/users`. -Next, define `#initialize`. Suppose you want to set an Access Token, API Key, etc. as in the example above. You can omit the definition if you don't need it. +Next, define `#initialize` to set values such as an access token or API key. You can omit it if you do not need any instance state. -Then define `#get_users` and `#post_user`. It's a good idea to give the method name the title of the API. I'm calling `#get` and `#post` inside the method, which is the HTTP Method at the time of the request. You can also use `#patch` `#put` `#delete`. +Then define methods such as `#get_users` and `#post_user`. Inside those methods, call HTTP helpers like `#get` and `#post`. You can also use `#patch`, `#put`, and `#delete`. ### Pagination -Some APIs include a URL in the response to get the continuation of the result. +Some APIs include a URL for the next page in the response. -MyApiClient provides a method called `#pageable_get` to handle such APIs as enumerable. An example is shown below: +MyApiClient provides `#pageable_get` to treat such APIs as an enumerable. An example is shown below: ```ruby class MyPaginationApiClient < ApplicationApiClient @@ -112,9 +112,9 @@ class MyPaginationApiClient < ApplicationApiClient end ``` -In the above example, the request is first made for `GET https://example.com/v1/pagination?page=1`, followed by the URL contained in the response JSON `$.link.next`. Make a request to enumerable. +In the example above, the first request is `GET https://example.com/v1/pagination?page=1`. It then continues requesting the URL in `$.links.next` from each response. -For example, in the following response, `$.link.next` indicates `"https://example.com/pagination?page=3"`: +For example, in the response below, `$.links.next` points to `"https://example.com/pagination?page=3"`: ```json { @@ -126,15 +126,15 @@ For example, in the following response, `$.link.next` indicates `"https://exampl } ``` -`#pageable_get` returns [Enumerator::Lazy](https://docs.ruby-lang.org/ja/latest/class/Enumerator=3a=3aLazy.html), so you can get the following result by `#each` or `#next`: +`#pageable_get` returns [Enumerator::Lazy](https://docs.ruby-lang.org/ja/latest/class/Enumerator=3a=3aLazy.html), so you can iterate using `#each` or `#next`: ```ruby -api_clinet = MyPaginationApiClient.new -api_clinet.pagination.each do |response| +api_client = MyPaginationApiClient.new +api_client.pagination.each do |response| # Do something. end -result = api_clinet.pagination +result = api_client.pagination result.next # => 1st page result result.next # => 2nd page result result.next # => 3rd page result @@ -153,7 +153,7 @@ end ### Error handling -MyApiClient allows you to define error handling that raises an exception depending on the content of the response. Here, as an example, error handling is defined in the above code: +MyApiClient lets you define error handling rules that raise exceptions based on response content. For example: ```ruby class ExampleApiClient < MyApiClient::Base @@ -174,7 +174,7 @@ class ExampleApiClient < MyApiClient::Base private - # @param params [MyApiClient::Params::Params] HTTP reqest and response params + # @param params [MyApiClient::Params::Params] HTTP request 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}" @@ -182,19 +182,19 @@ class ExampleApiClient < MyApiClient::Base end ``` -I will explain one by one. First, about the one that specifies `status_code` as follows: +Let's go through each option. First, this rule checks `status_code`: ```ruby error_handling status_code: 400..499, raise: MyApiClient::ClientError ``` -This will cause `MyApiClient::ClientError` to occur as an exception if the status code of the response is `400..499` for all requests from `ExampleApiClient`. Error handling also applies to classes that inherit from `ExampleApiClient`. +This raises `MyApiClient::ClientError` when the response status code is in `400..499` for requests from `ExampleApiClient`. Error handling rules are also inherited by child classes. -Note that `Integer` `Range`, and `Regexp` can be specified for `status_code`. +You can specify `Integer`, `Range`, or `Regexp` for `status_code`. -A class that inherits `MyApiClient::Error` can be specified for `raise`. Please check [here](https://github.com/ryz310/my_api_client/blob/master/lib/my_api_client/errors) for the error class defined as standard in `my_api_client`. If `raise` is omitted, `MyApiClient::Error` will be raised. +A class inheriting from `MyApiClient::Error` can be specified for `raise`. See [here](https://github.com/ryz310/my_api_client/blob/master/lib/my_api_client/errors) for built-in error classes. If `raise` is omitted, `MyApiClient::Error` is raised. -Next, about the case of specifying `block`: +Next, here is an example using a block: ```ruby error_handling status_code: 500..599, raise: MyApiClient::ServerError do |_params, logger| @@ -202,38 +202,38 @@ error_handling status_code: 500..599, raise: MyApiClient::ServerError do |_param end ``` -In the above example, if the status code is `500..599`, the contents of `block` will be executed before raising `MyApiClient::ServerError`. The argument `params` contains request and response information. +In this example, when the status code is `500..599`, the block runs before `MyApiClient::ServerError` is raised. The `params` argument includes both request and response information. -`logger` is an instance for log output. If you log output using this instance, the request information will be included in the log output as shown below, which is convenient for debugging: +`logger` is a request-scoped logger. If you log with this instance, request information is automatically included, which is useful for debugging: ```text -API request `GET https://example.com/path/to/resouce`: "Server error occurred." +API request `GET https://example.com/path/to/resource`: "Server error occurred." ``` ```ruby error_handling json: { '$.errors.code': 10..19 }, with: :my_error_handling ``` -For `json`, specify [JSONPath](https://goessner.net/articles/JsonPath/) for the Key of `Hash`, get an arbitrary value from the response JSON, and check whether it matches value. You can handle errors. You can specify `String` `Integer` `Range` and `Regexp` for value. +For `json`, use [JSONPath](https://goessner.net/articles/JsonPath/) as the hash key, extract values from response JSON, and match them against expected values. You can specify `String`, `Integer`, `Range`, or `Regexp` as matcher values. -In the above case, it matches JSON as below: +In this case, it matches JSON such as: ```json { - "erros": { + "errors": { "code": 10, "message": "Some error has occurred." } } ``` -For `headers`, specify response header for the Key of `Hash`, get an arbitrary value from the response header, and check whether it matches value. You can handle errors. You can specify `String` and `Regexp` for value. +For `headers`, specify a response-header key and match its value. You can specify `String` or `Regexp` as matcher values. ```ruby error_handling headers: { 'www-authenticate': /invalid token/ }, with: :my_error_handling ``` -In the above case, it matches response header as below: +In this case, it matches response headers such as: ```text cache-control: no-cache, no-store, max-age=0, must-revalidate @@ -242,7 +242,7 @@ www-authenticate: Bearer error="invalid_token", error_description="invalid token content-length: 104 ``` -By specifying the instance method name in `with`, when an error is detected, any method can be executed before raising an exception. The arguments passed to the method are `params` and `logger` as in the `block` definition. Note that `block` and` with` cannot be used at the same time. +By specifying an instance method name in `with`, you can run arbitrary logic before raising an exception. The method receives `params` and `logger`, just like a block. Note that `block` and `with` cannot be used together. ```ruby # @param params [MyApiClient::Params::Params] HTTP req and res params @@ -254,7 +254,7 @@ end #### Default error handling -In MyApiClient, the response of status code 400 ~ 500 series is handled as an exception by default. If the status code is in the 400s, an exception class that inherits `MyApiClient::ClientError` is raised, and in the 500s, an exception class that inherits `MyApiClient::ServerError` is raised. +By default, MyApiClient treats 4xx and 5xx responses as exceptions. In the 4xx range, it raises an exception class inheriting from `MyApiClient::ClientError`; in the 5xx range, it raises one inheriting from `MyApiClient::ServerError`. Also, `retry_on` is defined by default for `MyApiClient::NetworkError`. @@ -268,7 +268,7 @@ They are defined [here](https://github.com/ryz310/my_api_client/blob/master/lib/ error_handling json: { '$.errors.code': :negative? } ``` -Although it is an experimental function, by specifying `Symbol` for value of `status` or `json`, you can call a method for the result value and judge the result. In the above case, it matches the following JSON. If `#negative?` does not exist in the target object, the method will not be called. +This is an experimental feature. By specifying a `Symbol` as the value for `status` or `json`, MyApiClient calls that method on the extracted value and uses the result for matching. In the example above, it matches the following JSON. If `#negative?` does not exist on the target object, the method is not called. #### forbid_nil @@ -276,7 +276,7 @@ Although it is an experimental function, by specifying `Symbol` for value of `st error_handling status_code: 200, json: :forbid_nil ``` -It seems that some services expect an empty Response Body to be returned from the server, but an empty result is returned. This is also an experimental feature, but we have provided the `json: :forbid_nil` option to detect such cases. Normally, if the response body is empty, no error judgment is made, but if this option is specified, it will be detected as an error. Please be careful about false positives because some APIs have an empty normal response. +Some services expect a non-empty response body but occasionally receive an empty one. This experimental option, `json: :forbid_nil`, helps detect that case. Normally, an empty response body is not treated as an error, but with this option it is. Be careful of false positives, because some APIs intentionally return empty responses. #### MyApiClient::Params::Params @@ -306,7 +306,7 @@ end #### MyApiClient::Error -If the response of the API request matches the matcher defined in `error_handling`, the exception handling specified in `raise` will occur. This exception class must inherit `MyApiClient::Error`. +If an API response matches a rule defined in `error_handling`, the exception class specified in `raise` is triggered. This exception class must inherit from `MyApiClient::Error`. This exception class has a method called `#params`, which allows you to refer to request and response parameters. @@ -324,11 +324,11 @@ end #### Bugsnag breadcrumbs -If you are using [Bugsnag-Ruby v6.11.0](https://github.com/bugsnag/bugsnag-ruby/releases/tag/v6.11.0) or later, [breadcrumbs function](https://docs. bugsnag.com/platforms/ruby/other/#logging-breadcrumbs) is automatically supported. With this function, `Bugsnag.leave_breadcrumb` is called internally when `MyApiClient::Error` occurs, and you can check the request information, response information, etc. when an error occurs from the Bugsnag console. +If you are using [Bugsnag-Ruby v6.11.0](https://github.com/bugsnag/bugsnag-ruby/releases/tag/v6.11.0) or later, the [breadcrumbs feature](https://docs.bugsnag.com/platforms/ruby/other/#logging-breadcrumbs) is supported automatically. When `MyApiClient::Error` occurs, `Bugsnag.leave_breadcrumb` is called internally, so you can inspect request and response details in the Bugsnag console. ### Retry -Next, I would like to introduce the retry function provided by MyApiClient. +Next, let's look at retry support in MyApiClient. ```ruby class ExampleApiClient < MyApiClient::Base @@ -341,13 +341,13 @@ class ExampleApiClient < MyApiClient::Base end ``` -If the API request is executed many times, a network error may occur due to a line malfunction. In some cases, the network will be unavailable for a long time, but in many cases it will be a momentary error. In MyApiClient, network exceptions are collectively raised as `MyApiClient::NetworkError`. The details of this exception will be described later, but by using `retry_on`, it is possible to supplement arbitrary exception handling like `ActiveJob` and retry the API request a certain number of times and after a certain period of time. +When an API request is executed many times, network errors can occur. Sometimes the network is unavailable for a long time, but often the error is temporary. In MyApiClient, network-related exceptions are wrapped as `MyApiClient::NetworkError`. Using `retry_on`, you can handle such exceptions and retry requests with configurable wait time and attempt count, similar to `ActiveJob`. -Note that `retry_on MyApiClient::NetworkError` is implemented as standard, so it will be applied automatically without any special definition. Please define and use it only when you want to set an arbitrary value for `wait` or `attempts`. +`retry_on MyApiClient::NetworkError` is enabled by default, so you do not need to define it unless you want custom `wait` or `attempts` values. -However, unlike `ActiveJob`, it retries in synchronous processing, so I think that there is not much opportunity to use it other than retrying in case of a momentary network interruption. As in the above example, there may be cases where you retry in preparation for API Rate Limit, but it may be better to handle this with `ActiveJob`. +Unlike `ActiveJob`, retries are performed synchronously. In practice, this is most useful for short-lived network interruptions. You can also retry for API rate limits as in the example above, but handling that with `ActiveJob` may be a better fit depending on your workload. -By the way, `discard_on` is also implemented, but since the author himself has not found an effective use, I will omit the details. Please let me know if there is a good way to use it. +`discard_on` is also implemented, but details are omitted here because a strong use case has not been identified yet. #### Convenient usage @@ -366,7 +366,7 @@ error_handling json: { '$.errors.code': 20 }, retry: { wait: 30.seconds, attempts: 3 } ``` -If you do not need to specify `wait` or` attempts` in `retry_on`, it works with `retry: true`: +If you do not need to specify `wait` or `attempts` in `retry_on`, you can use `retry: true`: ```ruby error_handling json: { '$.errors.code': 20 }, @@ -381,7 +381,7 @@ Keep the following in mind when using the `retry` option: #### MyApiClient::NetworkError -As mentioned above, in MyApiClient, network exceptions are collectively `raised` as `MyApiClient::NetworkError`. Like the other exceptions, it has `MyApiClient::Error` as its parent class. A list of exception classes treated as `MyApiClient::NetworkError` can be found in `MyApiClient::NETWORK_ERRORS`. You can also refer to the original exception with `#original_error`: +As mentioned above, MyApiClient wraps network exceptions as `MyApiClient::NetworkError`. Like other client errors, its parent class is `MyApiClient::Error`. The list of wrapped exception classes is available in `MyApiClient::NETWORK_ERRORS`. You can inspect the original exception via `#original_error`: ```ruby begin @@ -392,7 +392,7 @@ rescue MyApiClient::NetworkError => e end ``` -Note that a normal exception is raised depending on the result of the request, but since this exception is raised during the request, the exception instance does not include the response parameter. +Unlike normal API errors that are raised after receiving a response, this exception is raised during request execution. Therefore, the exception instance does not include response parameters. ### Timeout @@ -449,7 +449,7 @@ require 'my_api_client/rspec' ### Testing -Suppose you have defined a `ApiClient` like this: +Suppose you have defined an `ApiClient` like this: ```ruby class ExampleApiClient < MyApiClient::Base From 9870981855d9fd96bd76980da8c42be5078b0542 Mon Sep 17 00:00:00 2001 From: ryz310 Date: Sun, 1 Mar 2026 17:59:04 +0900 Subject: [PATCH 3/3] docs: update AGENTS retrospective and README rules --- AGENTS.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 5faed6c9..374ae67a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,6 +76,20 @@ - Propose a KPT retrospective when a task reaches a completion point, such as after creating a Pull Request. - Based on the KPT results, propose updates to `AGENTS.md`. - The timing of the retrospective may be decided by Codex when it is appropriate. +- Output destination rule: KPT content must be shared only in this Codex conversation unless the user explicitly requests another destination. +- Do not post KPT content to GitHub comments/issues/PRs unless the user explicitly requests it. + +## README Update Checklist +- When editing `README.md` or `README.jp.md`, run a typo/consistency sweep before finishing. +- Check common typo patterns: `api_clinet`, `erros`, `reqest`, `resouce`. +- Verify sample request parameters match method signatures and examples (e.g. query/body keys). +- Verify JSONPath examples match sample JSON structures (e.g. `links` vs `link`). + +## GitHub CLI Body Safety +- When using `gh pr create`, `gh pr comment`, or similar commands with markdown body text, avoid inline shell strings that include backticks. +- Prefer one of the following: + - Use plain text without backticks in inline `--body`. + - Write the body to a temporary file and pass it via file-based options (or equivalent safe method) to prevent shell command substitution. ## Runbook - Dependabot PR review and auto-merge operation steps are documented in `docs/runbooks/dependabot_pr_auto_merge.md`.