my_api_client 0.13.0 → 0.16.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +102 -36
  3. data/.dependabot/config.yml +34 -0
  4. data/.envrc.skeleton +1 -0
  5. data/.rubocop.yml +8 -10
  6. data/.rubocop_challenge.yml +3 -0
  7. data/.rubocop_todo.yml +5 -10
  8. data/CHANGELOG.md +229 -0
  9. data/Gemfile.lock +48 -43
  10. data/README.jp.md +98 -23
  11. data/bin/console +4 -0
  12. data/example/api_clients/application_api_client.rb +13 -0
  13. data/example/api_clients/my_error_api_client.rb +34 -0
  14. data/example/api_clients/my_errors.rb +27 -0
  15. data/example/api_clients/my_pagination_api_client.rb +18 -0
  16. data/example/api_clients/my_rest_api_client.rb +48 -0
  17. data/example/api_clients/my_status_api_client.rb +22 -0
  18. data/lib/generators/rails/templates/application_api_client.rb.erb +0 -11
  19. data/lib/my_api_client.rb +10 -2
  20. data/lib/my_api_client/base.rb +2 -18
  21. data/lib/my_api_client/config.rb +0 -29
  22. data/lib/my_api_client/default_error_handlers.rb +64 -0
  23. data/lib/my_api_client/error_handling.rb +13 -23
  24. data/lib/my_api_client/error_handling/generator.rb +30 -10
  25. data/lib/my_api_client/error_handling/{process_retry_option.rb → retry_option_processor.rb} +1 -1
  26. data/lib/my_api_client/errors.rb +0 -53
  27. data/lib/my_api_client/errors/api_limit_error.rb +6 -0
  28. data/lib/my_api_client/errors/client_error.rb +93 -0
  29. data/lib/my_api_client/errors/network_error.rb +43 -0
  30. data/lib/my_api_client/errors/server_error.rb +42 -0
  31. data/lib/my_api_client/params/request.rb +7 -10
  32. data/lib/my_api_client/request.rb +48 -70
  33. data/lib/my_api_client/request/basic.rb +32 -0
  34. data/lib/my_api_client/request/executor.rb +89 -0
  35. data/lib/my_api_client/request/logger.rb +37 -0
  36. data/lib/my_api_client/request/pagination.rb +39 -0
  37. data/lib/my_api_client/rspec/matcher_helper.rb +2 -2
  38. data/lib/my_api_client/rspec/matchers/be_handled_as_an_error.rb +2 -0
  39. data/lib/my_api_client/rspec/matchers/request_to.rb +3 -4
  40. data/lib/my_api_client/version.rb +1 -1
  41. data/my_api/.envrc.skeleton +3 -0
  42. data/my_api/.gitignore +14 -0
  43. data/my_api/.jetskeep +1 -0
  44. data/my_api/.rspec +3 -0
  45. data/my_api/.ruby-version +1 -0
  46. data/my_api/Gemfile +23 -0
  47. data/my_api/Gemfile.lock +243 -0
  48. data/my_api/Procfile +7 -0
  49. data/my_api/README.md +54 -0
  50. data/my_api/Rakefile +4 -0
  51. data/my_api/app/controllers/application_controller.rb +5 -0
  52. data/my_api/app/controllers/error_controller.rb +21 -0
  53. data/my_api/app/controllers/pagination_controller.rb +58 -0
  54. data/my_api/app/controllers/rest_controller.rb +60 -0
  55. data/my_api/app/controllers/status_controller.rb +11 -0
  56. data/my_api/app/helpers/application_helper.rb +5 -0
  57. data/my_api/app/jobs/application_job.rb +7 -0
  58. data/my_api/app/models/application_item.rb +5 -0
  59. data/my_api/config.ru +7 -0
  60. data/my_api/config/application.rb +73 -0
  61. data/my_api/config/dynamodb.yml +22 -0
  62. data/my_api/config/environments/development.rb +9 -0
  63. data/my_api/config/environments/production.rb +11 -0
  64. data/my_api/config/environments/test.rb +9 -0
  65. data/my_api/config/routes.rb +17 -0
  66. data/my_api/db/.gitkeep +0 -0
  67. data/my_api/public/404.html +67 -0
  68. data/my_api/public/422.html +67 -0
  69. data/my_api/public/500.html +66 -0
  70. data/my_api/public/favicon.ico +0 -0
  71. data/my_api/public/index.html +91 -0
  72. data/my_api/spec/controllers/error_controller_spec.rb +43 -0
  73. data/my_api/spec/controllers/pagination_controller_spec.rb +73 -0
  74. data/my_api/spec/controllers/rest_controller_spec.rb +99 -0
  75. data/my_api/spec/controllers/status_controller_spec.rb +47 -0
  76. data/my_api/spec/fixtures/payloads/posts-index.json +51 -0
  77. data/my_api/spec/fixtures/payloads/posts-show.json +53 -0
  78. data/my_api/spec/spec_helper.rb +31 -0
  79. data/my_api_client.gemspec +1 -1
  80. metadata +62 -9
  81. data/lib/my_api_client/logger.rb +0 -36
  82. data/renovate.json +0 -5
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'application_api_client'
4
+
5
+ # An usage example of the `my_api_client`.
6
+ # See also: my_api/app/controllers/rest_controller.rb
7
+ class MyRestApiClient < ApplicationApiClient
8
+ # GET rest
9
+ def get_posts(order: :asc)
10
+ order = :desc unless order == :asc
11
+ query = { order: order }
12
+ get 'rest', query: query, headers: headers
13
+ end
14
+
15
+ # GET rest/:id
16
+ def get_post(id:)
17
+ get "rest/#{id}", headers: headers
18
+ end
19
+
20
+ # POST rest
21
+ def post_post(title:)
22
+ body = { title: title }
23
+ post 'rest', body: body, headers: headers
24
+ end
25
+
26
+ # PUT rest/:id
27
+ def put_post(id:, title:)
28
+ body = { title: title }
29
+ put "rest/#{id}", body: body, headers: headers
30
+ end
31
+
32
+ # PATCH rest/:id
33
+ def patch_post(id:, title:)
34
+ body = { title: title }
35
+ patch "rest/#{id}", body: body, headers: headers
36
+ end
37
+
38
+ # DELETE rest/:id
39
+ def delete_post(id:)
40
+ delete "rest/#{id}", headers: headers
41
+ end
42
+
43
+ private
44
+
45
+ def headers
46
+ { 'Content-Type': 'application/json;charset=UTF-8' }
47
+ end
48
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'application_api_client'
4
+
5
+ # An usage example of the `my_api_client`.
6
+ # See also: my_api/app/controllers/status_controller.rb
7
+ class MyStatusApiClient < ApplicationApiClient
8
+ error_handling status_code: 400, raise: MyErrors::BadRequest
9
+ error_handling status_code: 401, raise: MyErrors::Unauthorized
10
+ error_handling status_code: 403, raise: MyErrors::Forbidden
11
+
12
+ # GET status/:status
13
+ def get_status(status:)
14
+ get "status/#{status}", headers: headers
15
+ end
16
+
17
+ private
18
+
19
+ def headers
20
+ { 'Content-Type': 'application/json;charset=UTF-8' }
21
+ end
22
+ end
@@ -20,15 +20,4 @@ class ApplicationApiClient < MyApiClient::Base
20
20
  # seconds.
21
21
  #
22
22
  # http_read_timeout 3.seconds
23
-
24
- # Catch the exception and re-execution after any seconds, like as ActiveJob.
25
- # Please note that it is executed as a synchronous process unlike ActiveJob.
26
- #
27
- # retry_on MyApiClient::NetworkError, wait: 5.seconds, attempts: 3
28
-
29
- # Set a HTTP response verifyment and behavior. If conflicting conditions are
30
- # set, the processing defined later takes precedence
31
- #
32
- # error_handling status_code: 400..499, raise: MyApiClient::ClientError
33
- # error_handling status_code: 500..599, raise: MyApiClient::ServerError
34
23
  end
@@ -11,11 +11,19 @@ require 'my_api_client/service_abstract'
11
11
  require 'my_api_client/version'
12
12
  require 'my_api_client/config'
13
13
  require 'my_api_client/error_handling/generator'
14
- require 'my_api_client/error_handling/process_retry_option'
14
+ require 'my_api_client/error_handling/retry_option_processor'
15
15
  require 'my_api_client/error_handling'
16
16
  require 'my_api_client/exceptions'
17
- require 'my_api_client/logger'
17
+ require 'my_api_client/request/logger'
18
+ require 'my_api_client/request/executor'
19
+ require 'my_api_client/request/basic'
20
+ require 'my_api_client/request/pagination'
18
21
  require 'my_api_client/errors'
22
+ require 'my_api_client/errors/api_limit_error'
23
+ require 'my_api_client/errors/client_error'
24
+ require 'my_api_client/errors/network_error'
25
+ require 'my_api_client/errors/server_error'
26
+ require 'my_api_client/default_error_handlers'
19
27
  require 'my_api_client/params/params'
20
28
  require 'my_api_client/params/request'
21
29
  require 'my_api_client/request'
@@ -18,26 +18,10 @@ module MyApiClient
18
18
  self.error_handlers = []
19
19
  end
20
20
 
21
+ include MyApiClient::DefaultErrorHandlers
22
+
21
23
  # NOTE: This class **MUST NOT** implement #initialize method. Because it
22
24
  # will become constraint that need call #super in the #initialize at
23
25
  # definition of the child classes.
24
-
25
- HTTP_METHODS = %i[get post patch delete].freeze
26
-
27
- HTTP_METHODS.each do |http_method|
28
- class_eval <<~METHOD, __FILE__, __LINE__ + 1
29
- # Description of ##{http_method}
30
- #
31
- # @param pathname [String]
32
- # @param headers [Hash, nil]
33
- # @param query [Hash, nil]
34
- # @param body [Hash, nil]
35
- # @return [Sawyer::Resouce] description_of_returned_object
36
- def #{http_method}(pathname, headers: nil, query: nil, body: nil)
37
- _request :#{http_method}, pathname, headers, query, body, logger
38
- end
39
- METHOD
40
- end
41
- alias put patch
42
26
  end
43
27
  end
@@ -16,34 +16,5 @@ module MyApiClient
16
16
  METHOD
17
17
  end
18
18
  end
19
-
20
- # Extracts schema and hostname from endpoint
21
- #
22
- # @example Extracts schema and hostname from 'https://example.com/path/to/api'
23
- # schema_and_hostname # => 'https://example.com'
24
- # @return [String] description_of_returned_object
25
- def schema_and_hostname
26
- if _uri.default_port == _uri.port
27
- "#{_uri.scheme}://#{_uri.host}"
28
- else
29
- "#{_uri.scheme}://#{_uri.host}:#{_uri.port}"
30
- end
31
- end
32
-
33
- # Extracts pathname from endpoint
34
- #
35
- # @example Extracts pathname from 'https://example.com/path/to/api'
36
- # common_path # => 'path/to/api'
37
- # @return [String] The pathanem
38
- def common_path
39
- _uri.path
40
- end
41
-
42
- private
43
-
44
- # @return [URI] Returns a memoized URI instance
45
- def _uri
46
- @_uri ||= URI.parse(endpoint)
47
- end
48
19
  end
49
20
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MyApiClient
4
+ # Provides default error handlers.
5
+ module DefaultErrorHandlers
6
+ extend ActiveSupport::Concern
7
+
8
+ # rubocop:disable Metrics/BlockLength
9
+ included do
10
+ # NOTE: The built-in error handlers are following. Although they are prepared
11
+ # to save the trouble of defining, but you can override any error handlers
12
+ # in your API client class.
13
+ error_handling status_code: 400..499, raise: ClientError
14
+ error_handling status_code: 400, raise: ClientError::BadRequest
15
+ error_handling status_code: 401, raise: ClientError::Unauthorized
16
+ error_handling status_code: 402, raise: ClientError::PaymentRequired
17
+ error_handling status_code: 403, raise: ClientError::Forbidden
18
+ error_handling status_code: 404, raise: ClientError::NotFound
19
+ error_handling status_code: 405, raise: ClientError::MethodNotAllowed
20
+ error_handling status_code: 406, raise: ClientError::NotAcceptable
21
+ error_handling status_code: 407, raise: ClientError::ProxyAuthenticationRequired
22
+ error_handling status_code: 408, raise: ClientError::RequestTimeout
23
+ error_handling status_code: 409, raise: ClientError::Conflict
24
+ error_handling status_code: 410, raise: ClientError::Gone
25
+ error_handling status_code: 411, raise: ClientError::LengthRequired
26
+ error_handling status_code: 412, raise: ClientError::PreconditionFailed
27
+ error_handling status_code: 413, raise: ClientError::RequestEntityTooLarge
28
+ error_handling status_code: 414, raise: ClientError::RequestUriTooLong
29
+ error_handling status_code: 415, raise: ClientError::UnsupportedMediaType
30
+ error_handling status_code: 416, raise: ClientError::RequestedRangeNotSatisfiable
31
+ error_handling status_code: 417, raise: ClientError::ExpectationFailed
32
+ error_handling status_code: 418, raise: ClientError::IamTeapot
33
+ error_handling status_code: 421, raise: ClientError::MisdirectedRequest
34
+ error_handling status_code: 422, raise: ClientError::UnprocessableEntity
35
+ error_handling status_code: 423, raise: ClientError::Locked
36
+ error_handling status_code: 424, raise: ClientError::FailedDependency
37
+ error_handling status_code: 425, raise: ClientError::TooEarly
38
+ error_handling status_code: 426, raise: ClientError::UpgradeRequired
39
+ error_handling status_code: 428, raise: ClientError::PreconditionRequired
40
+ error_handling status_code: 429, raise: ClientError::TooManyRequests
41
+ error_handling status_code: 431, raise: ClientError::RequestHeaderFieldsTooLarge
42
+ error_handling status_code: 451, raise: ClientError::UnavailableForLegalReasons
43
+
44
+ error_handling status_code: 500..599, raise: ServerError
45
+ error_handling status_code: 500, raise: ServerError::InternalServerError
46
+ error_handling status_code: 501, raise: ServerError::NotImplemented
47
+ error_handling status_code: 502, raise: ServerError::BadGateway
48
+ error_handling status_code: 503, raise: ServerError::ServiceUnavailable
49
+ error_handling status_code: 504, raise: ServerError::GatewayTimeout
50
+ error_handling status_code: 505, raise: ServerError::HttpVersionNotSupported
51
+ error_handling status_code: 506, raise: ServerError::VariantAlsoNegotiates
52
+ error_handling status_code: 507, raise: ServerError::InsufficientStorage
53
+ error_handling status_code: 508, raise: ServerError::LoopDetected
54
+ error_handling status_code: 509, raise: ServerError::BandwidthLimitExceeded
55
+ error_handling status_code: 510, raise: ServerError::NotExtended
56
+ error_handling status_code: 511, raise: ServerError::NetworkAuthenticationRequired
57
+
58
+ # Catch the exception and re-execution after any seconds, like as ActiveJob.
59
+ # Please note that it is executed as a synchronous process unlike ActiveJob.
60
+ retry_on NetworkError, wait: 0.3.seconds, attempts: 3
61
+ end
62
+ # rubocop:enable Metrics/BlockLength
63
+ end
64
+ end
@@ -9,9 +9,8 @@ module MyApiClient
9
9
  # @example
10
10
  # error_handling status_code: 200, json: :forbid_nil
11
11
  # error_handling status_code: 400..499, raise: MyApiClient::ClientError
12
- # error_handling status_code: 500..599 do |params, logger|
12
+ # error_handling status_code: 500..599, raise: MyApiClient::ServerError do |params, logger|
13
13
  # logger.warn 'Server error occurred.'
14
- # raise MyApiClient::ServerError, params
15
14
  # end
16
15
  #
17
16
  # error_handling json: { '$.errors.code': 10..19 }, with: :my_error_handling
@@ -29,10 +28,11 @@ module MyApiClient
29
28
  # @option status_code [String, Range, Integer, Regexp]
30
29
  # Verifies response HTTP status code and raises error if matched
31
30
  # @option json [Hash, Symbol]
32
- # Verifies response body as JSON and raises error if matched.
33
- # If specified `:forbid_nil`, it forbid `nil` on response_body.
31
+ # Specify the validation target value path included in the response body
32
+ # as JsonPath expression.
33
+ # If specified `:forbid_nil`, it forbid `nil` at the response body.
34
34
  # @option with [Symbol]
35
- # Calls specified method when error detected
35
+ # Calls specified method before raising exception when error detected.
36
36
  # @option raise [MyApiClient::Error]
37
37
  # Raises specified error when an invalid response detected.
38
38
  # Should be inherited `MyApiClient::Error` class.
@@ -40,30 +40,20 @@ module MyApiClient
40
40
  # @option retry [TrueClass, Hash]
41
41
  # If the error detected, retries the API request. Requires `raise` option.
42
42
  # You can set `true` or `retry_on` options (`wait` and `attempts`).
43
- # @yield [MyApiClient::Params::Params, MyApiClient::Logger]
44
- # Executes the block when error detected.
43
+ # @yield [MyApiClient::Params::Params, MyApiClient::Request::Logger]
44
+ # Executes the block before raising exception when error detected.
45
45
  # Forbid to be used with the` retry` option.
46
46
  def error_handling(**options, &block)
47
47
  options[:block] = block
48
- retry_options = ProcessRetryOption.call(error_handling_options: options)
48
+ retry_options = RetryOptionProcessor.call(error_handling_options: options)
49
49
  retry_on(options[:raise], **retry_options) if retry_options
50
50
 
51
- temp = error_handlers.dup
52
- temp << ->(response) { Generator.call(**options.merge(response: response)) }
53
- self.error_handlers = temp
51
+ new_error_handlers = error_handlers.dup
52
+ new_error_handlers << lambda { |instance, response|
53
+ Generator.call(**options.merge(instance: instance, response: response))
54
+ }
55
+ self.error_handlers = new_error_handlers
54
56
  end
55
57
  end
56
-
57
- # The error handlers defined later takes precedence
58
- #
59
- # @param response [Sawyer::Response] describe_params_here
60
- # @return [Proc, Symbol, nil] description_of_returned_object
61
- def error_handling(response)
62
- error_handlers.reverse_each do |error_handler|
63
- result = error_handler.call(response)
64
- return result unless result.nil?
65
- end
66
- nil
67
- end
68
58
  end
69
59
  end
@@ -4,10 +4,12 @@ module MyApiClient
4
4
  module ErrorHandling
5
5
  # Generates an error handler proc (or symbol)
6
6
  class Generator < ServiceAbstract
7
- ARGUMENTS = %i[response status_code json with raise block].freeze
7
+ ARGUMENTS = %i[instance response status_code json with raise block].freeze
8
8
 
9
9
  # @param options [Hash]
10
10
  # Options for this generator
11
+ # @option instance [MyApiClient::Base]
12
+ # The API client class.
11
13
  # @option response [Sawyer::Response]
12
14
  # The target of verifying
13
15
  # @option status_code [String, Range, Integer, Regexp]
@@ -21,10 +23,8 @@ module MyApiClient
21
23
  # Raises specified error when error detected. default: MyApiClient::Error
22
24
  # @option block [Proc]
23
25
  # Executes the block when error detected
24
- # @return [Proc]
25
- # Returns value as `Proc` if given `raise` or `block` option
26
- # @return [Symbol]
27
- # Returns value as `Symbol` if given `with` option
26
+ # @return [Proc, nil]
27
+ # Returns the error handler as "Proc". If no error occurs, return `nil`.
28
28
  def initialize(**options)
29
29
  options[:raise] ||= MyApiClient::Error
30
30
  verify_and_set_arguments(**options)
@@ -38,15 +38,37 @@ module MyApiClient
38
38
  return unless match?(_status_code, _response.status)
39
39
  return unless match_all?(_json, _response.body)
40
40
 
41
+ generate_error_handler
42
+ end
43
+
44
+ def generate_error_handler
41
45
  if _block
42
- ->(params, logger) { _block.call(params, logger) }
46
+ block_caller
43
47
  elsif _with
44
- _with
48
+ method_caller
45
49
  else
46
- ->(params, _) { raise _raise, params }
50
+ error_raiser
47
51
  end
48
52
  end
49
53
 
54
+ def block_caller
55
+ lambda { |params, logger|
56
+ _block.call(params, logger)
57
+ error_raiser.call(params, logger)
58
+ }
59
+ end
60
+
61
+ def method_caller
62
+ lambda { |params, logger|
63
+ _instance.send(_with, params, logger)
64
+ error_raiser.call(params, logger)
65
+ }
66
+ end
67
+
68
+ def error_raiser
69
+ ->(params, _) { raise _raise, params }
70
+ end
71
+
50
72
  # Verify given options and raise error if they are incorrect.
51
73
  # If not, set them to instance variables.
52
74
  #
@@ -63,7 +85,6 @@ module MyApiClient
63
85
  end
64
86
  end
65
87
 
66
- # rubocop:disable Metrics/CyclomaticComplexity
67
88
  def match?(operator, target)
68
89
  case operator
69
90
  when nil
@@ -80,7 +101,6 @@ module MyApiClient
80
101
  raise "Unexpected operator type was given: #{operator.inspect}"
81
102
  end
82
103
  end
83
- # rubocop:enable Metrics/CyclomaticComplexity
84
104
 
85
105
  def match_all?(json, response_body)
86
106
  return true if json.nil?
@@ -3,7 +3,7 @@
3
3
  module MyApiClient
4
4
  module ErrorHandling
5
5
  # Processes the `retry` option.
6
- class ProcessRetryOption < ServiceAbstract
6
+ class RetryOptionProcessor < ServiceAbstract
7
7
  # @param error_handling_options [Hash]
8
8
  # Options for the retry.
9
9
  # @option raise [MyApiClient::Error]
@@ -25,57 +25,4 @@ module MyApiClient
25
25
  { error: super, params: params }.inspect
26
26
  end
27
27
  end
28
-
29
- NETWORK_ERRORS = [
30
- Faraday::TimeoutError,
31
- Faraday::ConnectionFailed,
32
- Faraday::SSLError,
33
- OpenSSL::SSL::SSLError,
34
- Net::OpenTimeout,
35
- Net::ReadTimeout,
36
- SocketError,
37
- ].freeze
38
-
39
- # Raises it when occurred to some network error
40
- class NetworkError < Error
41
- attr_reader :original_error
42
-
43
- # Initialize the error class
44
- #
45
- # @param params [MyApiClient::Params::Params]
46
- # The request and response parameters
47
- # @param original_error [StandardError]
48
- # Some network error
49
- def initialize(params, original_error)
50
- @original_error = original_error
51
- super params, original_error.message
52
- end
53
-
54
- # Returns contents as string for to be readable for human
55
- #
56
- # @return [String] Contents as string
57
- def inspect
58
- { error: original_error, params: params }.inspect
59
- end
60
-
61
- # Generate metadata for bugsnag.
62
- #
63
- # @return [Hash] Metadata for bugsnag
64
- def metadata
65
- super.merge(original_error: original_error.inspect)
66
- end
67
- end
68
-
69
- # NOTE: The built-in error classes are following. Although they are prepared
70
- # to save the trouble of defining, but you can create any error classes
71
- # which inherit the ancestor error class.
72
-
73
- # For 4xx client error
74
- class ClientError < Error; end
75
-
76
- # For 5xx server error
77
- class ServerError < Error; end
78
-
79
- # For API request limit error
80
- class ApiLimitError < Error; end
81
28
  end