my_api_client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.circleci/config.yml +172 -0
- data/.envrc.skeleton +1 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +46 -0
- data/.rubocop_todo.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +123 -0
- data/LICENSE.txt +21 -0
- data/README.jp.md +232 -0
- data/README.md +45 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/release +34 -0
- data/bin/setup +8 -0
- data/lib/generators/rails/USAGE +11 -0
- data/lib/generators/rails/api_client_generator.rb +32 -0
- data/lib/generators/rails/templates/api_client.rb.erb +38 -0
- data/lib/generators/rails/templates/application_api_client.rb.erb +34 -0
- data/lib/generators/rspec/USAGE +8 -0
- data/lib/generators/rspec/api_client_generator.rb +30 -0
- data/lib/generators/rspec/templates/api_client_spec.rb.erb +13 -0
- data/lib/my_api_client.rb +34 -0
- data/lib/my_api_client/base.rb +36 -0
- data/lib/my_api_client/config.rb +20 -0
- data/lib/my_api_client/error_handling.rb +86 -0
- data/lib/my_api_client/errors.rb +66 -0
- data/lib/my_api_client/exceptions.rb +57 -0
- data/lib/my_api_client/logger.rb +36 -0
- data/lib/my_api_client/params/params.rb +26 -0
- data/lib/my_api_client/params/request.rb +29 -0
- data/lib/my_api_client/request.rb +85 -0
- data/lib/my_api_client/version.rb +5 -0
- data/my_api_client.gemspec +42 -0
- metadata +288 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ApplicationApiClient < MyApiClient::Base
|
4
|
+
# This is a super class for all API Client classes.
|
5
|
+
# Almost settings are inherited to child classes.
|
6
|
+
|
7
|
+
# Set the log output destination. The default is `Logger.new(STDOUT)`.
|
8
|
+
logger = Rails.logger
|
9
|
+
|
10
|
+
# Set the maximum number of seconds to wait on HTTP connection. If the
|
11
|
+
# connection does not open even after this number of seconds, the exception
|
12
|
+
# `MyApiClient::NetworkError` will raise. Setting nil will not time out.
|
13
|
+
# The default is 60 seconds.
|
14
|
+
#
|
15
|
+
# http_open_timeout 2.seconds
|
16
|
+
|
17
|
+
# Set the maximum number of seconds to block at one HTTP read. If it does not
|
18
|
+
# read even after this number of seconds, it will raise the exception
|
19
|
+
# MyApiClient::NetworkError. Setting nil will not time out. The default is 60
|
20
|
+
# seconds.
|
21
|
+
#
|
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
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'generators/rspec'
|
4
|
+
|
5
|
+
module Rspec
|
6
|
+
module Generators
|
7
|
+
# rails g rspec:api_client
|
8
|
+
class ApiClientGenerator < Base
|
9
|
+
source_root File.expand_path('templates', __dir__)
|
10
|
+
|
11
|
+
argument :endpoint,
|
12
|
+
type: :string,
|
13
|
+
default: 'https://example.com',
|
14
|
+
banner: '{schema and hostname}'
|
15
|
+
argument :requests,
|
16
|
+
type: :array,
|
17
|
+
default: %w[get_resource:get:path/to/resource post_resource:post:path/to/resource],
|
18
|
+
banner: '{action}:{method}:{path} {action}:{method}:{path}'
|
19
|
+
|
20
|
+
class_option :api_client_specs, type: :boolean, default: true
|
21
|
+
|
22
|
+
def generate_api_client_spec
|
23
|
+
return unless options[:api_client_specs]
|
24
|
+
|
25
|
+
file_path = File.join('spec/api_clients', "#{route_url.singularize}_api_client_spec.rb")
|
26
|
+
template 'api_client_spec.rb.erb', file_path
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
|
5
|
+
RSpec.describe <%= "#{class_name}ApiClient" %>, <%= type_metatag(:api_client) %> do
|
6
|
+
let(:api_client) { described_class.new }
|
7
|
+
|
8
|
+
<% requests.each do |request| -%>
|
9
|
+
<% action, http_method, pathname = request.sepalate(':') -%>
|
10
|
+
describe '#<%= action %>' do
|
11
|
+
end
|
12
|
+
<% end -%>
|
13
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'jsonpath'
|
5
|
+
require 'active_support'
|
6
|
+
require 'active_support/core_ext'
|
7
|
+
require 'sawyer'
|
8
|
+
require 'my_api_client/version'
|
9
|
+
require 'my_api_client/config'
|
10
|
+
require 'my_api_client/error_handling'
|
11
|
+
require 'my_api_client/exceptions'
|
12
|
+
require 'my_api_client/logger'
|
13
|
+
require 'my_api_client/errors'
|
14
|
+
require 'my_api_client/params/params'
|
15
|
+
require 'my_api_client/params/request'
|
16
|
+
require 'my_api_client/request'
|
17
|
+
require 'my_api_client/base'
|
18
|
+
|
19
|
+
if Sawyer::VERSION < '0.8.2'
|
20
|
+
module Sawyer
|
21
|
+
# NOTE: Old sawyer does not have attribute reader for response body.
|
22
|
+
# But new version sawyer is conflict some gems (e.g. octkit).
|
23
|
+
class Response
|
24
|
+
attr_reader :body, :env
|
25
|
+
|
26
|
+
alias _original_initialize initialize
|
27
|
+
|
28
|
+
def initialize(agent, res, options = {})
|
29
|
+
@body = res.body
|
30
|
+
_original_initialize(agent, res, options)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MyApiClient
|
4
|
+
# Description of Base
|
5
|
+
class Base
|
6
|
+
include MyApiClient::Config
|
7
|
+
include MyApiClient::ErrorHandling
|
8
|
+
include MyApiClient::Exceptions
|
9
|
+
include MyApiClient::Request
|
10
|
+
|
11
|
+
class_attribute :logger, instance_writer: false, default: ::Logger.new(STDOUT)
|
12
|
+
class_attribute :error_handlers, instance_writer: false, default: []
|
13
|
+
|
14
|
+
# NOTE: This class **MUST NOT** implement #initialize method. Because it
|
15
|
+
# will become constraint that need call #super in the #initialize at
|
16
|
+
# definition of the child classes.
|
17
|
+
|
18
|
+
HTTP_METHODS = %i[get post patch delete].freeze
|
19
|
+
|
20
|
+
HTTP_METHODS.each do |http_method|
|
21
|
+
class_eval <<~METHOD, __FILE__, __LINE__ + 1
|
22
|
+
# Description of #undefined
|
23
|
+
#
|
24
|
+
# @param pathname [String]
|
25
|
+
# @param headers [Hash, nil]
|
26
|
+
# @param query [Hash, nil]
|
27
|
+
# @param body [Hash, nil]
|
28
|
+
# @return [Sawyer::Resouce] description_of_returned_object
|
29
|
+
def #{http_method}(pathname, headers: nil, query: nil, body: nil)
|
30
|
+
_request :#{http_method}, pathname, headers, query, body, logger
|
31
|
+
end
|
32
|
+
METHOD
|
33
|
+
end
|
34
|
+
alias put patch
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MyApiClient
|
4
|
+
# Description of Config
|
5
|
+
module Config
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
CONFIG_METHODS = %i[endpoint http_read_timeout http_open_timeout].freeze
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
CONFIG_METHODS.each do |config_method|
|
12
|
+
class_eval <<~METHOD, __FILE__, __LINE__ + 1
|
13
|
+
def #{config_method}(#{config_method})
|
14
|
+
define_method :#{config_method}, -> { #{config_method} }
|
15
|
+
end
|
16
|
+
METHOD
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MyApiClient
|
4
|
+
# Provides `error_handling` as DSL.
|
5
|
+
#
|
6
|
+
# @note
|
7
|
+
# You need to define `class_attribute: error_handler, default: []` for the
|
8
|
+
# included class.
|
9
|
+
# @example
|
10
|
+
# error_handling status_code: 400..499, raise: MyApiClient::ClientError
|
11
|
+
# error_handling status_code: 500..599 do |params, logger|
|
12
|
+
# logger.warn 'Server error occurred.'
|
13
|
+
# raise MyApiClient::ServerError, params
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# error_handling json: { '$.errors.code': 10..19 }, with: :my_error_handling
|
17
|
+
# error_handling json: { '$.errors.code': 20 }, raise: MyApiClient::ApiLimitError
|
18
|
+
# error_handling json: { '$.errors.message': /Sorry/ }, raise: MyApiClient::ServerError
|
19
|
+
module ErrorHandling
|
20
|
+
extend ActiveSupport::Concern
|
21
|
+
|
22
|
+
class_methods do
|
23
|
+
# Description of .error_handling
|
24
|
+
#
|
25
|
+
# @param status_code [String, Range, Integer, Regexp] default: nil
|
26
|
+
# @param json [Hash] default: nil
|
27
|
+
# @param with [Symbol] default: nil
|
28
|
+
# @param raise [MyApiClient::Error] default: MyApiClient::Error
|
29
|
+
# @param block [Proc] describe_block_here
|
30
|
+
def error_handling(status_code: nil, json: nil, with: nil, raise: MyApiClient::Error)
|
31
|
+
temp = error_handlers.dup
|
32
|
+
temp << lambda { |response|
|
33
|
+
if match?(status_code, response.status) && match_all?(json, response.body)
|
34
|
+
if block_given?
|
35
|
+
->(params, logger) { yield params, logger }
|
36
|
+
elsif with
|
37
|
+
with
|
38
|
+
else
|
39
|
+
->(params, _logger) { raise raise, params }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
}
|
43
|
+
self.error_handlers = temp
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def match?(operator, target)
|
49
|
+
return true if operator.nil?
|
50
|
+
|
51
|
+
case operator
|
52
|
+
when String, Integer
|
53
|
+
operator == target
|
54
|
+
when Range
|
55
|
+
operator.include?(target)
|
56
|
+
when Regexp
|
57
|
+
operator =~ target.to_s
|
58
|
+
else
|
59
|
+
false
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def match_all?(json, response_body)
|
64
|
+
return true if json.nil?
|
65
|
+
return false if response_body.blank?
|
66
|
+
|
67
|
+
json.all? do |path, operator|
|
68
|
+
target = JsonPath.new(path.to_s).first(response_body)
|
69
|
+
match?(operator, target)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# The error handlers defined later takes precedence
|
75
|
+
#
|
76
|
+
# @param response [Sawyer::Response] describe_params_here
|
77
|
+
# @return [Proc, Symbol, nil] description_of_returned_object
|
78
|
+
def error_handling(response)
|
79
|
+
error_handlers.reverse_each do |error_handler|
|
80
|
+
result = error_handler.call(response)
|
81
|
+
return result unless result.nil?
|
82
|
+
end
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MyApiClient
|
4
|
+
# The ancestor class for all API request error
|
5
|
+
class Error < StandardError
|
6
|
+
attr_reader :params
|
7
|
+
|
8
|
+
# Description of #initialize
|
9
|
+
#
|
10
|
+
# @param params [MyApiClient::Params::Params] describe_params_here
|
11
|
+
# @param error_message [String] default: nil
|
12
|
+
def initialize(params, error_message = nil)
|
13
|
+
@params = params
|
14
|
+
super error_message
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns contents as string for to be readable for human
|
18
|
+
#
|
19
|
+
# @return [String] Contents as string
|
20
|
+
def inspect
|
21
|
+
{ error: super, params: params }.inspect
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
NETWORK_ERRORS = [
|
26
|
+
Faraday::ClientError,
|
27
|
+
OpenSSL::SSL::SSLError,
|
28
|
+
Net::OpenTimeout,
|
29
|
+
Net::ReadTimeout,
|
30
|
+
SocketError,
|
31
|
+
].freeze
|
32
|
+
|
33
|
+
# Raises it when occurred to some network error
|
34
|
+
class NetworkError < Error
|
35
|
+
attr_reader :original_error
|
36
|
+
|
37
|
+
# Description of #initialize
|
38
|
+
#
|
39
|
+
# @param params [MyApiClient::Params::Params] describe_params_here
|
40
|
+
# @param original_error [StandardError] Some network error
|
41
|
+
def initialize(params, original_error)
|
42
|
+
@original_error = original_error
|
43
|
+
super params, original_error.message
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns contents as string for to be readable for human
|
47
|
+
#
|
48
|
+
# @return [String] Contents as string
|
49
|
+
def inspect
|
50
|
+
{ error: original_error, params: params }.inspect
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# NOTE: The built-in error classes are following. Although they are prepared
|
55
|
+
# to save the trouble of defining, but you can create any error classes
|
56
|
+
# which inherit the ancestor error class.
|
57
|
+
|
58
|
+
# For 4xx client error
|
59
|
+
class ClientError < Error; end
|
60
|
+
|
61
|
+
# For 5xx server error
|
62
|
+
class ServerError < Error; end
|
63
|
+
|
64
|
+
# For API request limit error
|
65
|
+
class ApiLimitError < Error; end
|
66
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MyApiClient
|
4
|
+
# Description of Exceptions
|
5
|
+
module Exceptions
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
include ActiveSupport::Rescuable
|
8
|
+
|
9
|
+
# Description of #call
|
10
|
+
#
|
11
|
+
# @param args [Array<Object>] describe_args_here
|
12
|
+
# @return [Object] description_of_returned_object
|
13
|
+
def call(*args)
|
14
|
+
@args = args
|
15
|
+
send(*args)
|
16
|
+
rescue StandardError => e
|
17
|
+
@retry_count ||= 0
|
18
|
+
raise unless rescue_with_handler(e)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :retry_count, :method_name, :args
|
24
|
+
|
25
|
+
class_methods do
|
26
|
+
def retry_on(*exception, wait: 1.second, attempts: 3)
|
27
|
+
rescue_from(*exception) do |error|
|
28
|
+
if retry_count < attempts
|
29
|
+
retry_calling(wait)
|
30
|
+
elsif block_given?
|
31
|
+
yield self, error
|
32
|
+
else
|
33
|
+
raise error
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Description of #discard_on
|
39
|
+
#
|
40
|
+
# @note
|
41
|
+
# !! It is implemented following ActiveJob, but I think this method is
|
42
|
+
# not useful in this gem. !!
|
43
|
+
# @param exception [Type] describe_exception_here
|
44
|
+
def discard_on(*exception)
|
45
|
+
rescue_from(*exception) do |error|
|
46
|
+
yield self, error if block_given?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def retry_calling(wait)
|
52
|
+
sleep(wait)
|
53
|
+
@retry_count += 1
|
54
|
+
call(*args)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MyApiClient
|
4
|
+
# Description of Logger
|
5
|
+
class Logger
|
6
|
+
attr_reader :logger, :method, :pathname
|
7
|
+
|
8
|
+
LOG_LEVEL = %i[debug info warn error fatal].freeze
|
9
|
+
|
10
|
+
# Description of #initialize
|
11
|
+
#
|
12
|
+
# @param logger [::Logger] describe_logger_here
|
13
|
+
# @param faraday [Faraday::Connection] describe_faraday_here
|
14
|
+
# @param method [String] HTTP method
|
15
|
+
# @param pathname [String] The path name
|
16
|
+
def initialize(logger, faraday, method, pathname)
|
17
|
+
@logger = logger
|
18
|
+
@method = method.to_s.upcase
|
19
|
+
@pathname = faraday.build_exclusive_url(pathname)
|
20
|
+
end
|
21
|
+
|
22
|
+
LOG_LEVEL.each do |level|
|
23
|
+
class_eval <<~METHOD, __FILE__, __LINE__ + 1
|
24
|
+
def #{level}(message)
|
25
|
+
logger.#{level}(format(message))
|
26
|
+
end
|
27
|
+
METHOD
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def format(message)
|
33
|
+
"API request `#{method} #{pathname}`: \"#{message}\""
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MyApiClient
|
4
|
+
module Params
|
5
|
+
# Description of Params
|
6
|
+
class Params
|
7
|
+
attr_reader :request, :response
|
8
|
+
|
9
|
+
# Description of #initialize
|
10
|
+
#
|
11
|
+
# @param request [MyApiClient::Params::Request] describe_request_here
|
12
|
+
# @param response [Sawyer::Response, nil] describe_response_here
|
13
|
+
def initialize(request, response)
|
14
|
+
@request = request
|
15
|
+
@response = response
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns contents as string for to be readable for human
|
19
|
+
#
|
20
|
+
# @return [String] Contents as string
|
21
|
+
def inspect
|
22
|
+
{ request: request, response: response }.inspect
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|