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.
data/lib/recurly.rb CHANGED
@@ -7,6 +7,7 @@ require "recurly/requests"
7
7
  require "recurly/resources"
8
8
  require "recurly/http"
9
9
  require "recurly/errors"
10
+ require "recurly/connection_pool"
10
11
  require "recurly/client"
11
12
 
12
13
  module Recurly
@@ -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
- BASE_URL = "https://v3.recurly.com/"
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
- req = HTTP::Request.new(:get, pager.next, nil)
54
- faraday_resp = run_request(req, headers)
55
- handle_response! req, faraday_resp
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::Request.new(:get, path, nil)
72
- faraday_resp = run_request(request, headers)
73
- handle_response! request, faraday_resp
74
- rescue Faraday::ClientError => ex
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::Request.new(:post, path, JSON.dump(request_data))
82
- faraday_resp = run_request(request, headers)
83
- handle_response! request, faraday_resp
84
- rescue Faraday::ClientError => ex
85
- raise_network_error!(ex)
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::Request.new(:put, path)
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
- logger.info("PUT BODY #{JSON.dump(request_data)}")
94
- request.body = JSON.dump(request_data)
101
+ json_body = JSON.dump(request_data)
102
+ logger.info("PUT BODY #{json_body}")
103
+ request.body = json_body
95
104
  end
96
- faraday_resp = run_request(request, headers)
97
- handle_response! request, faraday_resp
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::Request.new(:delete, path, nil)
105
- faraday_resp = run_request(request, headers)
106
- handle_response! request, faraday_resp
107
- rescue Faraday::ClientError => ex
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
- def run_request(request, headers)
122
- read_headers @conn.run_request(request.method, request.path, request.body, headers)
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, faraday_resp)
126
- response = HTTP::Response.new(faraday_resp, request)
127
- raise_api_error!(response) unless (200...300).include?(response.status)
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 BINARY_TYPES.include?(response.content_type)
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
- JSONParser.parse(self, response.body)
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 raise_network_error!(ex)
143
- error_class = case ex
144
- when Faraday::TimeoutError
145
- Errors::TimeoutError
146
- when Faraday::ConnectionFailed
147
- Errors::ConnectionFailedError
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
- raise error_class, ex.message
155
- end
207
+ error_class = Errors::APIError.from_response(http_response)
156
208
 
157
- def raise_api_error!(response)
158
- error = JSONParser.parse(self, response.body)
159
- error_class = Errors::APIError.error_class(error.type)
160
- raise error_class.new(response, error)
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
- def set_faraday_connection(api_key)
213
- options = {
214
- url: BASE_URL,
215
- request: { timeout: 60, open_timeout: 50 },
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
@@ -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.status
12
- @request_id = resp.headers["x-request-id"]
13
- @rate_limit = resp.headers["x-ratelimit-limit"].to_i
14
- @rate_limit_remaining = resp.headers["x-ratelimit-remaining"].to_i
15
- @rate_limit_reset = Time.at(resp.headers["x-ratelimit-reset"].to_i).to_datetime
16
- @content_type = resp.headers["content-type"].split(";").first if resp.headers["content-type"]
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
@@ -1,3 +1,3 @@
1
1
  module Recurly
2
- VERSION = "3.3.1"
2
+ VERSION = "3.4.0"
3
3
  end
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.3.1
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-20 00:00:00.000000000 Z
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.3
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