my_api_client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|