my_api_client 0.21.0 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +23 -7
  3. data/.rubocop_todo.yml +1 -1
  4. data/CHANGELOG.md +27 -2
  5. data/Gemfile +2 -0
  6. data/Gemfile.lock +23 -21
  7. data/README.jp.md +10 -10
  8. data/README.md +392 -8
  9. data/gemfiles/rails_7.0.gemfile +15 -0
  10. data/lib/generators/rails/templates/api_client.rb.erb +3 -5
  11. data/lib/generators/rspec/templates/api_client_spec.rb.erb +0 -8
  12. data/lib/my_api_client/version.rb +1 -1
  13. data/my_api/Gemfile +1 -1
  14. data/my_api/Gemfile.lock +68 -66
  15. data/my_api/spec/spec_helper.rb +1 -1
  16. data/my_api_client.gemspec +3 -0
  17. data/rails_app/rails_5.2/Gemfile.lock +3 -3
  18. data/rails_app/rails_6.0/Gemfile.lock +15 -9
  19. data/rails_app/rails_6.1/Gemfile +2 -2
  20. data/rails_app/rails_6.1/Gemfile.lock +3 -3
  21. data/rails_app/rails_6.1/Rakefile +3 -1
  22. data/rails_app/rails_6.1/bin/bundle +28 -20
  23. data/rails_app/rails_6.1/bin/rails +4 -2
  24. data/rails_app/rails_6.1/bin/rake +4 -2
  25. data/rails_app/rails_6.1/bin/setup +3 -1
  26. data/rails_app/rails_6.1/config.ru +3 -1
  27. data/rails_app/rails_7.0/Gemfile +13 -0
  28. data/rails_app/rails_7.0/Gemfile.lock +147 -0
  29. data/rails_app/rails_7.0/README.md +24 -0
  30. data/rails_app/rails_7.0/Rakefile +8 -0
  31. data/rails_app/rails_7.0/app/controllers/application_controller.rb +4 -0
  32. data/rails_app/rails_7.0/app/models/application_record.rb +5 -0
  33. data/rails_app/rails_7.0/bin/bundle +122 -0
  34. data/rails_app/rails_7.0/bin/rails +6 -0
  35. data/rails_app/rails_7.0/bin/rake +6 -0
  36. data/rails_app/rails_7.0/bin/setup +35 -0
  37. data/rails_app/rails_7.0/config/application.rb +41 -0
  38. data/rails_app/rails_7.0/config/boot.rb +5 -0
  39. data/rails_app/rails_7.0/config/credentials.yml.enc +1 -0
  40. data/rails_app/rails_7.0/config/database.yml +25 -0
  41. data/rails_app/rails_7.0/config/environment.rb +7 -0
  42. data/rails_app/rails_7.0/config/environments/development.rb +58 -0
  43. data/rails_app/rails_7.0/config/environments/production.rb +70 -0
  44. data/rails_app/rails_7.0/config/environments/test.rb +52 -0
  45. data/rails_app/rails_7.0/config/initializers/cors.rb +17 -0
  46. data/rails_app/rails_7.0/config/initializers/filter_parameter_logging.rb +8 -0
  47. data/rails_app/rails_7.0/config/initializers/inflections.rb +17 -0
  48. data/rails_app/rails_7.0/config/locales/en.yml +33 -0
  49. data/rails_app/rails_7.0/config/routes.rb +8 -0
  50. data/rails_app/rails_7.0/config.ru +8 -0
  51. data/rails_app/rails_7.0/db/seeds.rb +8 -0
  52. data/rails_app/rails_7.0/public/robots.txt +1 -0
  53. data/rails_app/rails_7.0/spec/rails_helper.rb +14 -0
  54. data/rails_app/rails_7.0/spec/spec_helper.rb +13 -0
  55. metadata +34 -4
data/README.md CHANGED
@@ -4,9 +4,16 @@
4
4
 
5
5
  # MyApiClient
6
6
 
7
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/my_api_client`. To experiment with that code, run `bin/console` for an interactive prompt.
7
+ 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.
8
8
 
9
- TODO: Delete this and the text above, and describe your gem
9
+ 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.
10
+
11
+ [toc]
12
+
13
+ ## Supported Versions
14
+
15
+ * Ruby 2.6, 2.7, 3.0
16
+ * Rails 5.2, 6.0, 6.1, 7.0
10
17
 
11
18
  ## Installation
12
19
 
@@ -16,17 +23,394 @@ Add this line to your application's Gemfile:
16
23
  gem 'my_api_client'
17
24
  ```
18
25
 
19
- And then execute:
26
+ If you are using Ruby on Rails, you can use the `generator` function.
20
27
 
21
- $ bundle
28
+ ```sh
29
+ $ rails g api_client path/to/resource get:path/to/resource --endpoint https://example.com
22
30
 
23
- Or install it yourself as:
24
-
25
- $ gem install my_api_client
31
+ create app/api_clients/application_api_client.rb
32
+ create app/api_clients/path/to/resource_api_client.rb
33
+ invoke rspec
34
+ create spec/api_clients/path/to/resource_api_client_spec.rb
35
+ ```
26
36
 
27
37
  ## Usage
28
38
 
29
- TODO: Write usage instructions here
39
+ ### Basic
40
+
41
+ The simplest usage example is shown below:
42
+
43
+ ```ruby
44
+ class ExampleApiClient < MyApiClient::Base
45
+ endpoint 'https://example.com/v1'
46
+
47
+ attr_reader :access_token
48
+
49
+ def initialize(access_token:)
50
+ @access_token = access_token
51
+ end
52
+
53
+ # GET https://example.com/v1/users
54
+ #
55
+ # @return [Sawyer::Response] HTTP response parameter
56
+ def get_users
57
+ get 'users', headers: headers, query: { key: 'value' }
58
+ end
59
+
60
+ # POST https://example.com/v1/users
61
+ #
62
+ # @param name [String] Username which want to create
63
+ # @return [Sawyer::Response] HTTP response parameter
64
+ def post_user(name:)
65
+ post 'users', headers: headers, body: { name: name }
66
+ end
67
+
68
+ private
69
+
70
+ def headers
71
+ {
72
+ 'Content-Type': 'application/json;charset=UTF-8',
73
+ 'Authorization': "Bearer #{access_token}",
74
+ }
75
+ end
76
+ end
77
+
78
+ api_clinet = ExampleApiClient.new(access_token: 'access_token')
79
+ api_clinet.get_users #=> #<Sawyer::Response>
80
+ ```
81
+
82
+ 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`.
83
+
84
+ 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.
85
+
86
+ 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`.
87
+
88
+ ### Pagination
89
+
90
+ Some APIs include a URL in the response to get the continuation of the result.
91
+
92
+ MyApiClient provides a method called `#pageable_get` to handle such APIs as enumerable. An example is shown below:
93
+
94
+ ```ruby
95
+ class MyPaginationApiClient < ApplicationApiClient
96
+ endpoint 'https://example.com/v1'
97
+
98
+ # GET pagination?page=1
99
+ def pagination
100
+ pageable_get 'pagination', paging: '$.links.next', headers: headers, query: { page: 1 }
101
+ end
102
+
103
+ private
104
+
105
+ def headers
106
+ { 'Content-Type': 'application/json;charset=UTF-8' }
107
+ end
108
+ end
109
+ ```
110
+
111
+ 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.
112
+
113
+ For example, in the following response, `$.link.next` indicates `"https://example.com/pagination?page=3"`:
114
+
115
+ ```json
116
+ {
117
+ "links": {
118
+ "next": "https://example.com/pagination?page=3",
119
+ "previous": "https://example.com/pagination?page=1",
120
+ },
121
+ "page": 2
122
+ }
123
+ ```
124
+
125
+ `#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`:
126
+
127
+ ```ruby
128
+ api_clinet = MyPaginationApiClient.new
129
+ api_clinet.pagination.each do |response|
130
+ # Do something.
131
+ end
132
+
133
+ result = api_clinet.pagination
134
+ result.next # => 1st page result
135
+ result.next # => 2nd page result
136
+ result.next # => 3rd page result
137
+ ```
138
+
139
+ Note that `#each` is repeated until the value of `paging` becomes `nil`. You can set the upper limit of pagination by combining with `#take`.
140
+
141
+ You can also use `#pget` as an alias for `#pageable_get`:
142
+
143
+ ```ruby
144
+ # GET pagination?page=1
145
+ def pagination
146
+ pget 'pagination', paging: '$.links.next', headers: headers, query: { page: 1 }
147
+ end
148
+ ```
149
+
150
+ ### Error handling
151
+
152
+ 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:
153
+
154
+ ```ruby
155
+ class ExampleApiClient < MyApiClient::Base
156
+ endpoint 'https://example.com'
157
+
158
+ error_handling status_code: 400..499,
159
+ raise: MyApiClient::ClientError
160
+
161
+ error_handling status_code: 500..599, raise: MyApiClient::ServerError do |_params, logger|
162
+ logger.warn 'Server error occurred.'
163
+ end
164
+
165
+ error_handling json: { '$.errors.code': 10..19 },
166
+ raise: MyApiClient::ClientError,
167
+ with: :my_error_handling
168
+
169
+ # Omission
170
+
171
+ private
172
+
173
+ # @param params [MyApiClient::Params::Params] HTTP reqest and response params
174
+ # @param logger [MyApiClient::Request::Logger] Logger for a request processing
175
+ def my_error_handling(params, logger)
176
+ logger.warn "Response Body: #{params.response.body.inspect}"
177
+ end
178
+ end
179
+ ```
180
+
181
+ I will explain one by one. First, about the one that specifies `status_code` as follows:
182
+
183
+ ```ruby
184
+ error_handling status_code: 400..499, raise: MyApiClient::ClientError
185
+ ```
186
+
187
+ 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`.
188
+
189
+ Note that `Integer` `Range`, and `Regexp` can be specified for `status_code`.
190
+
191
+ 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.
192
+
193
+ Next, about the case of specifying `block`:
194
+
195
+ ```ruby
196
+ error_handling status_code: 500..599, raise: MyApiClient::ServerError do |_params, logger|
197
+ logger.warn 'Server error occurred.'
198
+ end
199
+ ```
200
+
201
+ 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.
202
+
203
+ `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:
204
+
205
+ ```text
206
+ API request `GET https://example.com/path/to/resouce`: "Server error occurred."
207
+ ```
208
+
209
+ ```ruby
210
+ error_handling json: { '$.errors.code': 10..19 }, with: :my_error_handling
211
+ ```
212
+
213
+ 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.
214
+
215
+ In the above case, it matches JSON as below:
216
+
217
+ ```json
218
+ {
219
+ "erros": {
220
+ "code": 10,
221
+ "message": "Some error has occurred."
222
+ }
223
+ }
224
+ ```
225
+
226
+ 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.
227
+
228
+ ```ruby
229
+ # @param params [MyApiClient::Params::Params] HTTP req and res params
230
+ # @param logger [MyApiClient::Request::Logger] Logger for a request processing
231
+ def my_error_handling(params, logger)
232
+ logger.warn "Response Body: #{params.response.body.inspect}"
233
+ end
234
+ ```
235
+
236
+ #### Default error handling
237
+
238
+ 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.
239
+
240
+ Also, `retry_on` is defined by default for `MyApiClient::NetworkError`.
241
+
242
+ Both can be overridden, so define `error_handling` as needed.
243
+
244
+ They are defined [here](https://github.com/ryz310/my_api_client/blob/master/lib/my_api_client/default_error_handlers.rb).
245
+
246
+ #### Use Symbol
247
+
248
+ ```ruby
249
+ error_handling json: { '$.errors.code': :negative? }
250
+ ```
251
+
252
+ 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.
253
+
254
+ #### forbid_nil
255
+
256
+ ```ruby
257
+ error_handling status_code: 200, json: :forbid_nil
258
+ ```
259
+
260
+ 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.
261
+
262
+ #### MyApiClient::Params::Params
263
+
264
+ WIP
265
+
266
+ #### MyApiClient::Error
267
+
268
+ 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`.
269
+
270
+ This exception class has a method called `#params`, which allows you to refer to request and response parameters.
271
+
272
+ ```ruby
273
+ begin
274
+ api_client.request
275
+ rescue MyApiClient::Error => e
276
+ e.params.inspect
277
+ # => {
278
+ # :request=>"#<MyApiClient::Params::Request#inspect>",
279
+ # :response=>"#<Sawyer::Response#inspect>",
280
+ # }
281
+ end
282
+ ```
283
+
284
+ #### Bugsnag breadcrumbs
285
+
286
+ 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.
287
+
288
+ ### Retry
289
+
290
+ Next, I would like to introduce the retry function provided by MyApiClient.
291
+
292
+ ```ruby
293
+ class ExampleApiClient < MyApiClient::Base
294
+ endpoint 'https://example.com'
295
+
296
+ retry_on MyApiClient::NetworkError, wait: 0.1.seconds, attempts: 3
297
+ retry_on MyApiClient::ApiLimitError, wait: 30.seconds, attempts: 3
298
+
299
+ error_handling json: { '$.errors.code': 20 }, raise: MyApiClient::ApiLimitError
300
+ end
301
+ ```
302
+
303
+ 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.
304
+
305
+ 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`.
306
+
307
+ 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`.
308
+
309
+ 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.
310
+
311
+ #### Convenient usage
312
+
313
+ You can omit the definition of `retry_on` by adding the `retry` option to `error_handling`.
314
+ For example, the following two codes have the same meaning:
315
+
316
+ ```ruby
317
+ retry_on MyApiClient::ApiLimitError, wait: 30.seconds, attempts: 3
318
+ error_handling json: { '$.errors.code': 20 },
319
+ raise: MyApiClient::ApiLimitError
320
+ ```
321
+
322
+ ```ruby
323
+ error_handling json: { '$.errors.code': 20 },
324
+ raise: MyApiClient::ApiLimitError,
325
+ retry: { wait: 30.seconds, attempts: 3 }
326
+ ```
327
+
328
+ If you do not need to specify `wait` or` attempts` in `retry_on`, it works with `retry: true`:
329
+
330
+ ```ruby
331
+ error_handling json: { '$.errors.code': 20 },
332
+ raise: MyApiClient::ApiLimitError,
333
+ retry: true
334
+ ```
335
+
336
+ Keep the following in mind when using the `retry` option:
337
+
338
+ * The `raise` option must be specified for `error_handling`
339
+ * Definition of `error_handling` using `block` is prohibited
340
+
341
+ #### MyApiClient::NetworkError
342
+
343
+ 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`:
344
+
345
+ ```ruby
346
+ begin
347
+ api_client.request
348
+ rescue MyApiClient::NetworkError => e
349
+ e.original_error # => #<Net::OpenTimeout>
350
+ e.params.response # => nil
351
+ end
352
+ ```
353
+
354
+ 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.
355
+
356
+ ### Timeout
357
+
358
+ WIP
359
+
360
+ ### Logger
361
+
362
+ WIP
363
+
364
+ ## RSpec
365
+
366
+ ### Setup
367
+
368
+ Supports testing with RSpec.
369
+ Add the following code to `spec/spec_helper.rb` (or `spec/rails_helper.rb`):
370
+
371
+ ```ruby
372
+ require 'my_api_client/rspec'
373
+ ```
374
+
375
+ ### Testing
376
+
377
+ Suppose you have defined a `ApiClient` like this:
378
+
379
+ ```ruby
380
+ class ExampleApiClient < MyApiClient::Base
381
+ endpoint 'https://example.com/v1'
382
+
383
+ error_handling status_code: 200, json: { '$.errors.code': 10 },
384
+ raise: MyApiClient::ClientError
385
+
386
+ attr_reader :access_token
387
+
388
+ def initialize(access_token:)
389
+ @access_token = access_token
390
+ end
391
+
392
+ # GET https://example.com/v1/users
393
+ def get_users(condition:)
394
+ get 'users', headers: headers, query: { search: condition }
395
+ end
396
+
397
+ private
398
+
399
+ def headers
400
+ {
401
+ 'Content-Type': 'application/json;charset=UTF-8',
402
+ 'Authorization': "Bearer #{access_token}",
403
+ }
404
+ end
405
+ end
406
+ ```
407
+
408
+
409
+ WIP
410
+
411
+ ### Stubbing
412
+
413
+ WIP
30
414
 
31
415
  ## Development
32
416
 
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file was generated by Appraisal
4
+
5
+ source 'https://rubygems.org'
6
+
7
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
8
+
9
+ gem 'activesupport', '~> 7.0.0'
10
+
11
+ group :integrations, optional: true do
12
+ gem 'bugsnag', '>= 6.11.0'
13
+ end
14
+
15
+ gemspec
@@ -3,9 +3,7 @@
3
3
  class <%= "#{class_name}ApiClient" %> < ::ApplicationApiClient
4
4
  endpoint '<%= options[:endpoint] %>'
5
5
 
6
- # error_handling json: { '$.errors.code': 10 } do |params, logger|
7
- # # Behavior when detected an error.
8
- # end
6
+ # error_handling json: { '$.errors.code': 10 }, raise: MyApiClient::Error
9
7
 
10
8
  def initialize
11
9
  end
@@ -13,8 +11,8 @@ class <%= "#{class_name}ApiClient" %> < ::ApplicationApiClient
13
11
  <% yeild_request_arguments do |action, http_method, pathname| -%>
14
12
  # <%= "#{http_method.upcase} #{pathname}" %>
15
13
  #
16
- # @return [Sawyer::Resource] Description of the API response
17
- # @raise [MyApiClient::Error] Description of the error
14
+ # @return [Sawyer::Resource] description_of_the_api_response
15
+ # @raise [MyApiClient::Error] description_of_the_error
18
16
  # @see Reference of the API
19
17
  def <%= action %>
20
18
  <% if http_method == 'get' -%>
@@ -26,14 +26,6 @@ RSpec.describe <%= "#{class_name}ApiClient" %>, <%= type_metatag(:api_client) %>
26
26
  .when_receive(status_code: 400)
27
27
  end
28
28
  end
29
-
30
- context 'when the API returns 500 Internal Server Error' do
31
- it do
32
- expect { api_request }
33
- .to be_handled_as_an_error(MyApiClient::ServerError::InternalServerError)
34
- .when_receive(status_code: 500)
35
- end
36
- end
37
29
  end
38
30
  <% yeild_request_arguments do |action, http_method, pathname| -%>
39
31
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MyApiClient
4
- VERSION = '0.21.0'
4
+ VERSION = '0.22.0'
5
5
  end
data/my_api/Gemfile CHANGED
@@ -7,7 +7,7 @@ gem 'jets'
7
7
  gem 'dynomite'
8
8
 
9
9
  # See: https://github.com/boltops-tools/jets/issues/523
10
- gem 'nokogiri', '~> 1.12.2'
10
+ gem 'nokogiri', '~> 1.12.5'
11
11
 
12
12
  # development and test groups are not bundled as part of the deployment
13
13
  group :development, :test do