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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MyApiClient
4
+ # For API request limit error
5
+ class ApiLimitError < Error; end
6
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MyApiClient
4
+ # For 4xx client error
5
+ class ClientError < Error
6
+ # 400 Bad Request
7
+ class BadRequest < ClientError; end
8
+
9
+ # 401 Unauthorized
10
+ class Unauthorized < ClientError; end
11
+
12
+ # 402 Payment Required
13
+ class PaymentRequired < ClientError; end
14
+
15
+ # 403 Forbidden
16
+ class Forbidden < ClientError; end
17
+
18
+ # 404 Not Found
19
+ class NotFound < ClientError; end
20
+
21
+ # 405 Method Not Allowed
22
+ class MethodNotAllowed < ClientError; end
23
+
24
+ # 406 Not Acceptable
25
+ class NotAcceptable < ClientError; end
26
+
27
+ # 407 Proxy Authentication Required
28
+ class ProxyAuthenticationRequired < ClientError; end
29
+
30
+ # 408 Request Timeout
31
+ class RequestTimeout < ClientError; end
32
+
33
+ # 409 Conflict
34
+ class Conflict < ClientError; end
35
+
36
+ # 410 Gone
37
+ class Gone < ClientError; end
38
+
39
+ # 411 Length Required
40
+ class LengthRequired < ClientError; end
41
+
42
+ # 412 Precondition Failed
43
+ class PreconditionFailed < ClientError; end
44
+
45
+ # 413 Payload Too Large
46
+ class RequestEntityTooLarge < ClientError; end
47
+
48
+ # 414 URI Too Long
49
+ class RequestUriTooLong < ClientError; end
50
+
51
+ # 415 Unsupported Media Type
52
+ class UnsupportedMediaType < ClientError; end
53
+
54
+ # 416 Range Not Satisfiable
55
+ class RequestedRangeNotSatisfiable < ClientError; end
56
+
57
+ # 417 Expectation Failed
58
+ class ExpectationFailed < ClientError; end
59
+
60
+ # 418 I'm a teapot
61
+ class IamTeapot < ClientError; end
62
+
63
+ # 421 Misdirected Request
64
+ class MisdirectedRequest < ClientError; end
65
+
66
+ # 422 Unprocessable Entity
67
+ class UnprocessableEntity < ClientError; end
68
+
69
+ # 423 Locked
70
+ class Locked < ClientError; end
71
+
72
+ # 424 Failed Dependency
73
+ class FailedDependency < ClientError; end
74
+
75
+ # 425 Too Early
76
+ class TooEarly < ClientError; end
77
+
78
+ # 426 Upgrade Required
79
+ class UpgradeRequired < ClientError; end
80
+
81
+ # 428 Precondition Required
82
+ class PreconditionRequired < ClientError; end
83
+
84
+ # 429 Too Many Requests
85
+ class TooManyRequests < ClientError; end
86
+
87
+ # 431 Request Header Fields Too Large
88
+ class RequestHeaderFieldsTooLarge < ClientError; end
89
+
90
+ # 451 Unavailable for Legal Reasons
91
+ class UnavailableForLegalReasons < ClientError; end
92
+ end
93
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MyApiClient
4
+ NETWORK_ERRORS = [
5
+ Faraday::TimeoutError,
6
+ Faraday::ConnectionFailed,
7
+ Faraday::SSLError,
8
+ OpenSSL::SSL::SSLError,
9
+ Net::OpenTimeout,
10
+ Net::ReadTimeout,
11
+ SocketError,
12
+ ].freeze
13
+
14
+ # Raises it when occurred to some network error
15
+ class NetworkError < Error
16
+ attr_reader :original_error
17
+
18
+ # Initialize the error class
19
+ #
20
+ # @param params [MyApiClient::Params::Params]
21
+ # The request and response parameters
22
+ # @param original_error [StandardError]
23
+ # Some network error
24
+ def initialize(params, original_error)
25
+ @original_error = original_error
26
+ super params, original_error.message
27
+ end
28
+
29
+ # Returns contents as string for to be readable for human
30
+ #
31
+ # @return [String] Contents as string
32
+ def inspect
33
+ { error: original_error, params: params }.inspect
34
+ end
35
+
36
+ # Generate metadata for bugsnag.
37
+ #
38
+ # @return [Hash] Metadata for bugsnag
39
+ def metadata
40
+ super.merge(original_error: original_error.inspect)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MyApiClient
4
+ # For 5xx server error
5
+ class ServerError < Error
6
+ # 500 Internal Server Error
7
+ class InternalServerError < ServerError; end
8
+
9
+ # 501 Not Implemented
10
+ class NotImplemented < ServerError; end
11
+
12
+ # 502 Bad Gateway
13
+ class BadGateway < ServerError; end
14
+
15
+ # 503 Service Unavailable
16
+ class ServiceUnavailable < ServerError; end
17
+
18
+ # 504 Gateway Timeout
19
+ class GatewayTimeout < ServerError; end
20
+
21
+ # 505 HTTP Version Not Supported
22
+ class HttpVersionNotSupported < ServerError; end
23
+
24
+ # 506 Variant Also Negotiates
25
+ class VariantAlsoNegotiates < ServerError; end
26
+
27
+ # 507 Insufficient Storage
28
+ class InsufficientStorage < ServerError; end
29
+
30
+ # 508 Loop Detected
31
+ class LoopDetected < ServerError; end
32
+
33
+ # 509 Bandwidth Limit Exceeded
34
+ class BandwidthLimitExceeded < ServerError; end
35
+
36
+ # 510 Not Extended
37
+ class NotExtended < ServerError; end
38
+
39
+ # 511 Network Authentication Required
40
+ class NetworkAuthenticationRequired < ServerError; end
41
+ end
42
+ end
@@ -4,20 +4,18 @@ module MyApiClient
4
4
  module Params
5
5
  # Description of Params
6
6
  class Request
7
- attr_reader :method, :pathname, :headers, :query, :body
7
+ attr_reader :method, :uri, :headers, :body
8
8
 
9
9
  # Description of #initialize
10
10
  #
11
11
  # @param method [Symbol] describe_method_here
12
- # @param pathname [String] describe_pathname_here
12
+ # @param uri [URI] describe_uri_here
13
13
  # @param headers [Hash, nil] describe_headers_here
14
- # @param query [Hash, nil] describe_query_here
15
14
  # @param body [Hash, nil] describe_body_here
16
- def initialize(method, pathname, headers, query, body)
15
+ def initialize(method, uri, headers, body)
17
16
  @method = method
18
- @pathname = pathname
17
+ @uri = uri
19
18
  @headers = headers
20
- @query = query
21
19
  @body = body
22
20
  end
23
21
 
@@ -25,7 +23,7 @@ module MyApiClient
25
23
  #
26
24
  # @return [Array<Object>] Arguments for Sawyer::Agent#call
27
25
  def to_sawyer_args
28
- [method, pathname, body, { headers: headers, query: query }]
26
+ [method, uri.to_s, body, { headers: headers }]
29
27
  end
30
28
 
31
29
  # Generate metadata for bugsnag.
@@ -34,9 +32,8 @@ module MyApiClient
34
32
  # @return [Hash] Metadata for bugsnag
35
33
  def metadata
36
34
  {
37
- line: "#{method.upcase} #{pathname}",
35
+ line: "#{method.upcase} #{uri}",
38
36
  headers: headers,
39
- query: query,
40
37
  body: body,
41
38
  }.compact
42
39
  end
@@ -46,7 +43,7 @@ module MyApiClient
46
43
  #
47
44
  # @return [String] Contents as string
48
45
  def inspect
49
- { method: method, pathname: pathname, headers: headers, query: query, body: body }.inspect
46
+ { method: method, uri: uri.to_s, headers: headers, body: body }.inspect
50
47
  end
51
48
  end
52
49
  end
@@ -1,87 +1,65 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MyApiClient
4
- # Description of Request
4
+ # Provides HTTP request method.
5
5
  module Request
6
- # Description of #_request
7
- #
8
- # @param http_method [Symbol] describe_http_method_here
9
- # @param pathname [String] describe_pathname_here
10
- # @param headers [Hash, nil] describe_headers_here
11
- # @param query [Hash, nil] describe_query_here
12
- # @param body [Hash, nil] describe_body_here
13
- # @param logger [::Logger] describe_logger_here
14
- # @return [Sawyer::Resource] description_of_returned_object
15
- # rubocop:disable Metrics/ParameterLists
16
- def _request(http_method, pathname, headers, query, body, logger)
17
- processed_path = [common_path, pathname].join('/').gsub('//', '/')
18
- request_params = Params::Request.new(http_method, processed_path, headers, query, body)
19
- agent # Initializes for faraday
20
- request_logger = Logger.new(logger, faraday, http_method, processed_path)
21
- call(:_execute, request_params, request_logger)
22
- end
23
- # rubocop:enable Metrics/ParameterLists
6
+ include Basic
7
+ include Pagination
24
8
 
25
9
  private
26
10
 
27
- # Description of #agent
28
- #
29
- # @return [Sawyer::Agent] description_of_returned_object
30
- def agent
31
- @agent ||= Sawyer::Agent.new(schema_and_hostname, faraday: faraday)
32
- end
33
-
34
- # Description of #faraday
11
+ # Executes HTTP request with relative URI.
35
12
  #
36
- # @return [Faraday::Connection] description_of_returned_object
37
- def faraday
38
- @faraday ||=
39
- Faraday.new(
40
- nil,
41
- request: {
42
- timeout: (http_read_timeout if respond_to?(:http_read_timeout)),
43
- open_timeout: (http_open_timeout if respond_to?(:http_open_timeout)),
44
- }.compact
45
- )
13
+ # @param http_method [Symbol]
14
+ # HTTP method. e.g. `:get`, `:post`, `:put`, `:patch` and `:delete`.
15
+ # @param pathname [String]
16
+ # Pathname of the request target URL.
17
+ # It's joined with the defined by `endpoint`.
18
+ # @param headers [Hash, nil]
19
+ # Request headers.
20
+ # @param query [Hash, nil]
21
+ # Query string.
22
+ # @param body [Hash, nil]
23
+ # Request body.
24
+ # @return [Sawyer::Response]
25
+ # Response instance.
26
+ def _request_with_relative_uri(http_method, pathname, headers, query, body)
27
+ query_strings = query.present? ? '?' + query&.to_query : ''
28
+ uri = URI.join(File.join(endpoint, pathname), query_strings)
29
+ _request_with_absolute_uri(http_method, uri, headers, body)
46
30
  end
47
31
 
48
- # Description of #_execute
32
+ # Executes HTTP request with absolute URI.
49
33
  #
50
- # @param request_params [MyApiClient::Params::Request] describe_request_params_here
51
- # @param request_logger [MyApiClient::Logger] describe_request_logger_here
52
- # @return [Sawyer::Resource] description_of_returned_object
53
- # @raise [MyApiClient::Error]
54
- def _execute(request_params, request_logger)
55
- request_logger.info('Start')
56
- response = agent.call(*request_params.to_sawyer_args)
57
- request_logger.info("Duration #{response.timing} sec")
58
- params = Params::Params.new(request_params, response)
59
- _verify(params, request_logger)
60
- rescue *NETWORK_ERRORS => e
61
- params ||= Params::Params.new(request_params, nil)
62
- request_logger.error("Network Error (#{e.message})")
63
- raise MyApiClient::NetworkError.new(params, e)
64
- rescue MyApiClient::Error => e
65
- request_logger.warn("Failure (#{response.status})")
66
- raise e
67
- else
68
- request_logger.info("Success (#{response.status})")
69
- response.data
34
+ # @param http_method [Symbol]
35
+ # HTTP method. e.g. `:get`, `:post`, `:put`, `:patch` and `:delete`.
36
+ # @param uri [URI]
37
+ # Request target URI including query strings.
38
+ # @param headers [Hash, nil]
39
+ # Request headers.
40
+ # @param body [Hash, nil]
41
+ # Request body.
42
+ # @return [Sawyer::Response]
43
+ # Response instance.
44
+ def _request_with_absolute_uri(http_method, uri, headers, body)
45
+ Executor.call(
46
+ instance: self,
47
+ request_params: Params::Request.new(http_method, uri, headers, body),
48
+ request_logger: Logger.new(logger, http_method, uri),
49
+ faraday_options: faraday_options
50
+ )
70
51
  end
71
52
 
72
- # Description of #_verify
53
+ # Generates options for the faraday instance.
73
54
  #
74
- # @param params [MyApiClient::Params::Params] describe_params_here
75
- # @param request_logger [MyApiClient::Logger] describe_request_logger_here
76
- # @return [nil] description_of_returned_object
77
- # @raise [MyApiClient::Error]
78
- def _verify(params, request_logger)
79
- case error_handler = error_handling(params.response)
80
- when Proc
81
- error_handler.call(params, request_logger)
82
- when Symbol
83
- send(error_handler, params, request_logger)
84
- end
55
+ # @return [Hash] Generated options.
56
+ def faraday_options
57
+ {
58
+ request: {
59
+ timeout: (http_read_timeout if respond_to?(:http_read_timeout)),
60
+ open_timeout: (http_open_timeout if respond_to?(:http_open_timeout)),
61
+ }.compact,
62
+ }
85
63
  end
86
64
  end
87
65
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MyApiClient
4
+ module Request
5
+ # Provides basic HTTP request method.
6
+ module Basic
7
+ HTTP_METHODS = %i[get post put patch delete].freeze
8
+
9
+ HTTP_METHODS.each do |http_method|
10
+ class_eval <<~METHOD, __FILE__, __LINE__ + 1
11
+ # Executes HTTP request with #{http_method.upcase} method
12
+ #
13
+ # @param pathname [String]
14
+ # Pathname of the request target URL.
15
+ # It's joined with the defined by `endpoint`.
16
+ # @param headers [Hash, nil]
17
+ # Request headers.
18
+ # @param query [Hash, nil]
19
+ # Query string.
20
+ # @param body [Hash, nil]
21
+ # Request body. You should not specify it when use GET method.
22
+ # @return [Sawyer::Resource]
23
+ # Response body instance.
24
+ def #{http_method}(pathname, headers: nil, query: nil, body: nil)
25
+ response = call(:_request_with_relative_uri, :#{http_method}, pathname, headers, query, body)
26
+ response.data
27
+ end
28
+ METHOD
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MyApiClient
4
+ module Request
5
+ # Executes HTTP request with specified parameters.
6
+ class Executor < ServiceAbstract
7
+ # @param instance [MyApiClient::Base]
8
+ # The my_api_client instance.
9
+ # The instance method will be called on error handling.
10
+ # @param request_params [MyApiClient::Params::Request]
11
+ # Request parameter instance.
12
+ # @param request_logger [MyApiClient::Logger]
13
+ # Request logger instance.
14
+ # @param faraday_options [Hash]
15
+ # Options for the faraday instance. Mainly used for timeout settings.
16
+ # @return [Sawyer::Response]
17
+ # Response instance.
18
+ # @raise [MyApiClient::Error]
19
+ # Raises on invalid response or network errors.
20
+ def initialize(instance:, request_params:, request_logger:, faraday_options:)
21
+ @instance = instance
22
+ @request_params = request_params
23
+ @request_logger = request_logger
24
+ faraday = Faraday.new(nil, faraday_options)
25
+ @agent = Sawyer::Agent.new('', faraday: faraday)
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :instance, :request_params, :request_logger, :agent
31
+
32
+ def call
33
+ request_logger.info('Start')
34
+ response = api_request
35
+ request_logger.info("Duration #{response.timing} sec")
36
+ verify(response)
37
+ rescue MyApiClient::Error => e
38
+ request_logger.warn("Failure (#{e.message})")
39
+ raise
40
+ else
41
+ request_logger.info("Success (#{response.status})")
42
+ response
43
+ end
44
+
45
+ # Executes HTTP request to the API.
46
+ #
47
+ # @return [Sawyer::Response]
48
+ # Response instance.
49
+ # @raise [MyApiClient::NetworkError]
50
+ # Raises on any network errors.
51
+ def api_request
52
+ agent.call(*request_params.to_sawyer_args)
53
+ rescue *NETWORK_ERRORS => e
54
+ params = Params::Params.new(request_params, nil)
55
+ raise MyApiClient::NetworkError.new(params, e)
56
+ end
57
+
58
+ # Verifies the response.
59
+ #
60
+ # @param response_params [Sawyer::Response]
61
+ # The target response.
62
+ # @return [nil]
63
+ # Returns nil when a valid response.
64
+ # @raise [MyApiClient::Error]
65
+ # Raises on any invalid response.
66
+ def verify(response_params)
67
+ params = Params::Params.new(request_params, response_params)
68
+ find_error_handler(response_params)&.call(params, request_logger)
69
+ end
70
+
71
+ # Executes response verifyment. If an invalid response is detected, return
72
+ # the error handler procedure. The error handlers defined later takes precedence.
73
+ #
74
+ # @param response_params [Sawyer::Response]
75
+ # The target response.
76
+ # @return [nil]
77
+ # Returns nil when a valid response.
78
+ # @return [Proc]
79
+ # Returns Proc when a invalid response.
80
+ def find_error_handler(response_params)
81
+ instance.error_handlers.reverse_each do |error_handler|
82
+ result = error_handler.call(instance, response_params)
83
+ return result unless result.nil?
84
+ end
85
+ nil
86
+ end
87
+ end
88
+ end
89
+ end