recurly 3.3.1 → 3.4.0

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