recurly 3.3.1 → 3.4.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 +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
|