recurly 3.3.1 → 3.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.bumpversion.cfg +1 -1
- data/CHANGELOG.md +24 -15
- data/lib/data/ca-certificates.crt +3464 -29
- data/lib/recurly.rb +1 -0
- data/lib/recurly/client.rb +115 -83
- data/lib/recurly/connection_pool.rb +40 -0
- data/lib/recurly/errors.rb +30 -0
- data/lib/recurly/errors/network_errors.rb +2 -0
- data/lib/recurly/http.rb +10 -6
- data/lib/recurly/version.rb +1 -1
- data/recurly.gemspec +0 -3
- metadata +4 -38
- data/lib/recurly/client/adapter.rb +0 -39
data/lib/recurly.rb
CHANGED
data/lib/recurly/client.rb
CHANGED
@@ -1,18 +1,22 @@
|
|
1
|
-
require "faraday"
|
2
1
|
require "logger"
|
3
2
|
require "erb"
|
3
|
+
require "net/https"
|
4
|
+
require "base64"
|
4
5
|
require_relative "./schema/json_parser"
|
5
6
|
require_relative "./schema/file_parser"
|
6
|
-
require_relative "./client/adapter"
|
7
7
|
|
8
8
|
module Recurly
|
9
9
|
class Client
|
10
10
|
require_relative "./client/operations"
|
11
11
|
|
12
|
-
|
12
|
+
BASE_HOST = "v3.recurly.com"
|
13
|
+
BASE_PORT = 443
|
14
|
+
CA_FILE = File.join(File.dirname(__FILE__), "../data/ca-certificates.crt")
|
13
15
|
BINARY_TYPES = [
|
14
16
|
"application/pdf",
|
15
17
|
]
|
18
|
+
JSON_CONTENT_TYPE = "application/json"
|
19
|
+
MAX_RETRIES = 3
|
16
20
|
|
17
21
|
# Initialize a client. It requires an API key.
|
18
22
|
#
|
@@ -42,17 +46,19 @@ module Recurly
|
|
42
46
|
# @param subdomain [String] Optional subdomain for the site you wish to be scoped to. Providing this makes all the `site_id` parameters optional.
|
43
47
|
def initialize(api_key:, site_id: nil, subdomain: nil, **options)
|
44
48
|
set_site_id(site_id, subdomain)
|
49
|
+
set_api_key(api_key)
|
45
50
|
set_options(options)
|
46
|
-
set_faraday_connection(api_key)
|
47
51
|
|
48
52
|
# execute block with this client if given
|
49
53
|
yield(self) if block_given?
|
50
54
|
end
|
51
55
|
|
52
56
|
def next_page(pager)
|
53
|
-
|
54
|
-
|
55
|
-
|
57
|
+
path = extract_path(pager.next)
|
58
|
+
request = Net::HTTP::Get.new path
|
59
|
+
set_headers(request)
|
60
|
+
http_response = run_request(request)
|
61
|
+
handle_response! request, http_response
|
56
62
|
end
|
57
63
|
|
58
64
|
protected
|
@@ -68,44 +74,44 @@ module Recurly
|
|
68
74
|
|
69
75
|
def get(path, **options)
|
70
76
|
path = scope_by_site(path, **options)
|
71
|
-
request = HTTP::
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
raise_network_error!(ex)
|
77
|
+
request = Net::HTTP::Get.new path
|
78
|
+
set_headers(request, options[:headers])
|
79
|
+
http_response = run_request(request, options)
|
80
|
+
handle_response! request, http_response
|
76
81
|
end
|
77
82
|
|
78
83
|
def post(path, request_data, request_class, **options)
|
79
84
|
request_class.new(request_data).validate!
|
80
85
|
path = scope_by_site(path, **options)
|
81
|
-
request = HTTP::
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
+
request = Net::HTTP::Post.new path
|
87
|
+
request.set_content_type(JSON_CONTENT_TYPE)
|
88
|
+
set_headers(request, options[:headers])
|
89
|
+
request.body = JSON.dump(request_data)
|
90
|
+
http_response = run_request(request, options)
|
91
|
+
handle_response! request, http_response
|
86
92
|
end
|
87
93
|
|
88
94
|
def put(path, request_data = nil, request_class = nil, **options)
|
89
95
|
path = scope_by_site(path, **options)
|
90
|
-
request = HTTP::
|
96
|
+
request = Net::HTTP::Put.new path
|
97
|
+
request.set_content_type(JSON_CONTENT_TYPE)
|
98
|
+
set_headers(request, options[:headers])
|
91
99
|
if request_data
|
92
100
|
request_class.new(request_data).validate!
|
93
|
-
|
94
|
-
|
101
|
+
json_body = JSON.dump(request_data)
|
102
|
+
logger.info("PUT BODY #{json_body}")
|
103
|
+
request.body = json_body
|
95
104
|
end
|
96
|
-
|
97
|
-
handle_response! request,
|
98
|
-
rescue Faraday::ClientError => ex
|
99
|
-
raise_network_error!(ex)
|
105
|
+
http_response = run_request(request, options)
|
106
|
+
handle_response! request, http_response
|
100
107
|
end
|
101
108
|
|
102
109
|
def delete(path, **options)
|
103
110
|
path = scope_by_site(path, **options)
|
104
|
-
request = HTTP::
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
raise_network_error!(ex)
|
111
|
+
request = Net::HTTP::Delete.new path
|
112
|
+
set_headers(request, options[:headers])
|
113
|
+
http_response = run_request(request, options)
|
114
|
+
handle_response! request, http_response
|
109
115
|
end
|
110
116
|
|
111
117
|
protected
|
@@ -118,18 +124,70 @@ module Recurly
|
|
118
124
|
# @return [Logger]
|
119
125
|
attr_reader :logger
|
120
126
|
|
121
|
-
|
122
|
-
|
127
|
+
@connection_pool = Recurly::ConnectionPool.new
|
128
|
+
|
129
|
+
class << self
|
130
|
+
# @return [Recurly::ConnectionPool]
|
131
|
+
attr_accessor :connection_pool
|
132
|
+
end
|
133
|
+
|
134
|
+
def run_request(request, options = {})
|
135
|
+
self.class.connection_pool.with_connection do |http|
|
136
|
+
set_http_options(http, options)
|
137
|
+
|
138
|
+
retries = 0
|
139
|
+
|
140
|
+
begin
|
141
|
+
http.start unless http.started?
|
142
|
+
http.request(request)
|
143
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH, Errno::ECONNABORTED,
|
144
|
+
Errno::EPIPE, Errno::ETIMEDOUT, Net::OpenTimeout, EOFError, SocketError => ex
|
145
|
+
retries += 1
|
146
|
+
if retries < MAX_RETRIES
|
147
|
+
retry
|
148
|
+
end
|
149
|
+
|
150
|
+
if ex.kind_of?(Net::OpenTimeout) || ex.kind_of?(Errno::ETIMEDOUT)
|
151
|
+
raise Recurly::Errors::TimeoutError, "Request timed out"
|
152
|
+
end
|
153
|
+
|
154
|
+
raise Recurly::Errors::ConnectionFailedError, "Failed to connect to Recurly: #{ex.message}"
|
155
|
+
rescue Net::ReadTimeout, Timeout::Error
|
156
|
+
raise Recurly::Errors::TimeoutError, "Request timed out"
|
157
|
+
rescue OpenSSL::SSL::SSLError => ex
|
158
|
+
raise Recurly::Errors::SSLError, ex.message
|
159
|
+
rescue StandardError => ex
|
160
|
+
raise Recurly::Errors::NetworkError, ex.message
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def set_headers(request, additional_headers = {})
|
166
|
+
request["Accept"] = "application/vnd.recurly.#{api_version}".chomp # got this method from operations.rb
|
167
|
+
request["Authorization"] = "Basic #{Base64.encode64(@api_key)}".chomp
|
168
|
+
request["User-Agent"] = "Recurly/#{VERSION}; #{RUBY_DESCRIPTION}"
|
169
|
+
|
170
|
+
# TODO this is undocumented until we finalize it
|
171
|
+
additional_headers.each { |header, v| request[header] = v } if additional_headers
|
172
|
+
end
|
173
|
+
|
174
|
+
def set_http_options(http, options)
|
175
|
+
http.open_timeout = options[:open_timeout] || 20
|
176
|
+
http.read_timeout = options[:read_timeout] || 60
|
177
|
+
|
178
|
+
http.set_debug_output(logger) if @log_level <= Logger::INFO && !http.started?
|
123
179
|
end
|
124
180
|
|
125
|
-
def handle_response!(request,
|
126
|
-
response = HTTP::Response.new(
|
127
|
-
raise_api_error!(response) unless
|
181
|
+
def handle_response!(request, http_response)
|
182
|
+
response = HTTP::Response.new(http_response, request)
|
183
|
+
raise_api_error!(http_response, response) unless http_response.kind_of?(Net::HTTPSuccess)
|
128
184
|
resource = if response.body
|
129
|
-
if
|
185
|
+
if http_response.content_type.include?(JSON_CONTENT_TYPE)
|
186
|
+
JSONParser.parse(self, response.body)
|
187
|
+
elsif BINARY_TYPES.include?(http_response.content_type)
|
130
188
|
FileParser.parse(response.body)
|
131
189
|
else
|
132
|
-
|
190
|
+
raise Recurly::Errors::InvalidResponseError, "Unexpected content type: #{http_response.content_type}"
|
133
191
|
end
|
134
192
|
else
|
135
193
|
Resources::Empty.new
|
@@ -139,25 +197,21 @@ module Recurly
|
|
139
197
|
resource
|
140
198
|
end
|
141
199
|
|
142
|
-
def
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
when Faraday::SSLError
|
149
|
-
Errors::SSLError
|
150
|
-
else
|
151
|
-
Errors::NetworkError
|
152
|
-
end
|
200
|
+
def raise_api_error!(http_response, response)
|
201
|
+
if response.content_type.include?(JSON_CONTENT_TYPE)
|
202
|
+
error = JSONParser.parse(self, response.body)
|
203
|
+
error_class = Errors::APIError.error_class(error.type)
|
204
|
+
raise error_class.new(response, error)
|
205
|
+
end
|
153
206
|
|
154
|
-
|
155
|
-
end
|
207
|
+
error_class = Errors::APIError.from_response(http_response)
|
156
208
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
209
|
+
if error_class <= Recurly::Errors::APIError
|
210
|
+
error = Recurly::Resources::Error.new(message: "#{http_response.code}: #{http_response.message}")
|
211
|
+
raise error_class.new(response, error)
|
212
|
+
else
|
213
|
+
raise error_class, "#{http_response.code}: #{http_response.message}"
|
214
|
+
end
|
161
215
|
end
|
162
216
|
|
163
217
|
def read_headers(response)
|
@@ -167,14 +221,6 @@ module Recurly
|
|
167
221
|
response
|
168
222
|
end
|
169
223
|
|
170
|
-
def headers
|
171
|
-
{
|
172
|
-
"Accept" => "application/vnd.recurly.#{api_version}", # got this method from operations.rb
|
173
|
-
"Content-Type" => "application/json",
|
174
|
-
"User-Agent" => "Recurly/#{VERSION}; #{RUBY_DESCRIPTION}",
|
175
|
-
}.merge(@extra_headers)
|
176
|
-
end
|
177
|
-
|
178
224
|
def interpolate_path(path, **options)
|
179
225
|
options.each do |k, v|
|
180
226
|
# Check to see that we are passing the correct data types
|
@@ -201,6 +247,10 @@ module Recurly
|
|
201
247
|
end
|
202
248
|
end
|
203
249
|
|
250
|
+
def set_api_key(api_key)
|
251
|
+
@api_key = api_key
|
252
|
+
end
|
253
|
+
|
204
254
|
def scope_by_site(path, **options)
|
205
255
|
if site = site_id || options[:site_id]
|
206
256
|
"/sites/#{site}#{path}"
|
@@ -209,34 +259,16 @@ module Recurly
|
|
209
259
|
end
|
210
260
|
end
|
211
261
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
ssl: { verify: true },
|
217
|
-
}
|
218
|
-
# Let's not use the bundled cert in production yet
|
219
|
-
# but we will use these certs for any other staging or dev environment
|
220
|
-
unless BASE_URL.end_with?(".recurly.com")
|
221
|
-
options[:ssl][:ca_file] = File.join(File.dirname(__FILE__), "../data/ca-certificates.crt")
|
222
|
-
end
|
223
|
-
|
224
|
-
@conn = Faraday.new(options) do |faraday|
|
225
|
-
if [Logger::DEBUG, Logger::INFO].include?(@log_level)
|
226
|
-
faraday.response :logger
|
227
|
-
end
|
228
|
-
faraday.basic_auth(api_key, "")
|
229
|
-
configure_net_adapter(faraday)
|
230
|
-
end
|
262
|
+
# Returns just the path and parameters so we can safely reuse the connection
|
263
|
+
def extract_path(uri_or_path)
|
264
|
+
uri = URI(uri_or_path)
|
265
|
+
uri.kind_of?(URI::HTTP) ? uri.request_uri : uri_or_path
|
231
266
|
end
|
232
267
|
|
233
268
|
def set_options(options)
|
234
269
|
@log_level = options[:log_level] || Logger::WARN
|
235
270
|
@logger = Logger.new(STDOUT)
|
236
271
|
@logger.level = @log_level
|
237
|
-
|
238
|
-
# TODO this is undocumented until we finalize it
|
239
|
-
@extra_headers = options[:headers] || {}
|
240
272
|
end
|
241
273
|
end
|
242
274
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require "net/https"
|
2
|
+
|
3
|
+
module Recurly
|
4
|
+
class ConnectionPool
|
5
|
+
def initialize
|
6
|
+
@mutex = Mutex.new
|
7
|
+
@pool = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def with_connection
|
11
|
+
http = nil
|
12
|
+
@mutex.synchronize do
|
13
|
+
http = @pool.pop
|
14
|
+
end
|
15
|
+
|
16
|
+
# create connection if the pool was empty
|
17
|
+
http ||= init_http_connection
|
18
|
+
|
19
|
+
response = yield http
|
20
|
+
|
21
|
+
if http.started?
|
22
|
+
@mutex.synchronize do
|
23
|
+
@pool.push(http)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
response
|
28
|
+
end
|
29
|
+
|
30
|
+
def init_http_connection
|
31
|
+
http = Net::HTTP.new(Client::BASE_HOST, Client::BASE_PORT)
|
32
|
+
http.use_ssl = true
|
33
|
+
http.ca_file = Client::CA_FILE
|
34
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
35
|
+
http.keep_alive_timeout = 600
|
36
|
+
|
37
|
+
http
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/recurly/errors.rb
CHANGED
@@ -17,6 +17,36 @@ module Recurly
|
|
17
17
|
Errors.const_get(class_name)
|
18
18
|
end
|
19
19
|
|
20
|
+
# When the response does not have a JSON body, this determines the appropriate
|
21
|
+
# Error class based on the response code. This may occur when a load balancer
|
22
|
+
# returns an error before it reaches Recurly's API.
|
23
|
+
# @param response [Net::Response]
|
24
|
+
# @return [Errors::APIError,Errors::NetworkError]
|
25
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
26
|
+
def self.from_response(response)
|
27
|
+
case response
|
28
|
+
when Net::HTTPBadRequest # 400
|
29
|
+
Recurly::Errors::BadRequestError
|
30
|
+
when Net::HTTPUnauthorized, Net::HTTPForbidden # 401, 403
|
31
|
+
Recurly::Errors::UnauthorizedError
|
32
|
+
when Net::HTTPRequestTimeOut # 408
|
33
|
+
Recurly::Errors::TimeoutError
|
34
|
+
when Net::HTTPTooManyRequests # 429
|
35
|
+
Recurly::Errors::RateLimitedError
|
36
|
+
when Net::HTTPInternalServerError # 500
|
37
|
+
Recurly::Errors::InternalServerError
|
38
|
+
when Net::HTTPServiceUnavailable # 503
|
39
|
+
Recurly::Errors::UnavailableError
|
40
|
+
when Net::HTTPGatewayTimeOut # 504
|
41
|
+
Recurly::Errors::TimeoutError
|
42
|
+
when Net::HTTPServerError # 5xx
|
43
|
+
Recurly::Errors::UnavailableError
|
44
|
+
else
|
45
|
+
Recurly::Errors::APIError
|
46
|
+
end
|
47
|
+
end
|
48
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
49
|
+
|
20
50
|
def initialize(response, error)
|
21
51
|
super(error.message)
|
22
52
|
@response = response
|
@@ -1,8 +1,10 @@
|
|
1
1
|
module Recurly
|
2
2
|
module Errors
|
3
3
|
class NetworkError < StandardError; end
|
4
|
+
class InvalidResponseError < NetworkError; end
|
4
5
|
class TimeoutError < NetworkError; end
|
5
6
|
class ConnectionFailedError < NetworkError; end
|
6
7
|
class SSLError < NetworkError; end
|
8
|
+
class UnavailableError < NetworkError; end
|
7
9
|
end
|
8
10
|
end
|
data/lib/recurly/http.rb
CHANGED
@@ -8,12 +8,16 @@ module Recurly
|
|
8
8
|
|
9
9
|
def initialize(resp, request)
|
10
10
|
@request = request
|
11
|
-
@status = resp.
|
12
|
-
@request_id = resp
|
13
|
-
@rate_limit = resp
|
14
|
-
@rate_limit_remaining = resp
|
15
|
-
@rate_limit_reset = Time.at(resp
|
16
|
-
|
11
|
+
@status = resp.code.to_i
|
12
|
+
@request_id = resp["x-request-id"]
|
13
|
+
@rate_limit = resp["x-ratelimit-limit"].to_i
|
14
|
+
@rate_limit_remaining = resp["x-ratelimit-remaining"].to_i
|
15
|
+
@rate_limit_reset = Time.at(resp["x-ratelimit-reset"].to_i).to_datetime
|
16
|
+
if resp["content-type"]
|
17
|
+
@content_type = resp["content-type"].split(";").first
|
18
|
+
else
|
19
|
+
@content_type = resp.content_type
|
20
|
+
end
|
17
21
|
if resp.body && !resp.body.empty?
|
18
22
|
@body = resp.body
|
19
23
|
else
|
data/lib/recurly/version.rb
CHANGED
data/recurly.gemspec
CHANGED
@@ -21,9 +21,6 @@ Gem::Specification.new do |spec|
|
|
21
21
|
# spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
22
|
spec.require_paths = ["lib"]
|
23
23
|
|
24
|
-
spec.add_dependency "faraday", ">= 0.8.11", "< 1.0.0"
|
25
|
-
|
26
|
-
spec.add_development_dependency "net-http-persistent", "~> 2.9.4"
|
27
24
|
spec.add_development_dependency "bundler", "~> 2.0"
|
28
25
|
spec.add_development_dependency "rake", "~> 12.3.3"
|
29
26
|
spec.add_development_dependency "rspec", "~> 3.0"
|
metadata
CHANGED
@@ -1,49 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: recurly
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Recurly
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-03-
|
11
|
+
date: 2020-03-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: faraday
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: 0.8.11
|
20
|
-
- - "<"
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: 1.0.0
|
23
|
-
type: :runtime
|
24
|
-
prerelease: false
|
25
|
-
version_requirements: !ruby/object:Gem::Requirement
|
26
|
-
requirements:
|
27
|
-
- - ">="
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
version: 0.8.11
|
30
|
-
- - "<"
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: 1.0.0
|
33
|
-
- !ruby/object:Gem::Dependency
|
34
|
-
name: net-http-persistent
|
35
|
-
requirement: !ruby/object:Gem::Requirement
|
36
|
-
requirements:
|
37
|
-
- - "~>"
|
38
|
-
- !ruby/object:Gem::Version
|
39
|
-
version: 2.9.4
|
40
|
-
type: :development
|
41
|
-
prerelease: false
|
42
|
-
version_requirements: !ruby/object:Gem::Requirement
|
43
|
-
requirements:
|
44
|
-
- - "~>"
|
45
|
-
- !ruby/object:Gem::Version
|
46
|
-
version: 2.9.4
|
47
13
|
- !ruby/object:Gem::Dependency
|
48
14
|
name: bundler
|
49
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -168,8 +134,8 @@ files:
|
|
168
134
|
- lib/data/ca-certificates.crt
|
169
135
|
- lib/recurly.rb
|
170
136
|
- lib/recurly/client.rb
|
171
|
-
- lib/recurly/client/adapter.rb
|
172
137
|
- lib/recurly/client/operations.rb
|
138
|
+
- lib/recurly/connection_pool.rb
|
173
139
|
- lib/recurly/errors.rb
|
174
140
|
- lib/recurly/errors/api_errors.rb
|
175
141
|
- lib/recurly/errors/network_errors.rb
|
@@ -320,7 +286,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
320
286
|
- !ruby/object:Gem::Version
|
321
287
|
version: '0'
|
322
288
|
requirements: []
|
323
|
-
rubygems_version: 3.0.
|
289
|
+
rubygems_version: 3.0.6
|
324
290
|
signing_key:
|
325
291
|
specification_version: 4
|
326
292
|
summary: The ruby client for Recurly's V3 API
|