network-client 1.1.6 → 2.0.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/Gemfile.lock +1 -1
- data/lib/network-client.rb +2 -2
- data/lib/network/client.rb +347 -0
- data/lib/network/version.rb +3 -0
- data/network-client.gemspec +2 -2
- metadata +4 -4
- data/lib/network-client/core.rb +0 -239
- data/lib/network-client/version.rb +0 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69a4eea5e01f0c07a0728a987d1e8722edf1b228
|
|
4
|
+
data.tar.gz: b1a9d49d2f9e73c9a88eb3bd3956b2a1ebfe7202
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba909db3bb7de0357673b06bd444b107810bdf1796afd2dbf2e8fc3669c072157dbd0f31af601903c0ec895c55419ae83534e7d56710ce147433a724c0479cef
|
|
7
|
+
data.tar.gz: cd044bc44966fa7292741f9088a9be74ce2409e4099bfba6ec94fcd3582e755a071e09db9ce838f99966cc87612812896efc1174fa54659b1525b195ba4b3ff0
|
data/Gemfile.lock
CHANGED
data/lib/network-client.rb
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
require_relative "network
|
|
2
|
-
require_relative "network
|
|
1
|
+
require_relative "network/client"
|
|
2
|
+
require_relative "network/version"
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'openssl'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'logger'
|
|
5
|
+
|
|
6
|
+
module Network
|
|
7
|
+
class Client
|
|
8
|
+
DEFAULT_HEADERS = { 'accept' => 'application/json',
|
|
9
|
+
'Content-Type' => 'application/json' }.freeze
|
|
10
|
+
# The success response template.
|
|
11
|
+
#
|
|
12
|
+
# Represents the return of rest-like methods holding two values:
|
|
13
|
+
# HTTP response code, and body <em>(parsed as json if request type is json)</em>.
|
|
14
|
+
Response = Struct.new(:code, :body)
|
|
15
|
+
|
|
16
|
+
# Stamp in front of each log written by client +@logger+.
|
|
17
|
+
LOG_TAG = '[NETWORK CLIENT]:'.freeze
|
|
18
|
+
|
|
19
|
+
attr_reader :username, :password, :default_headers, :logger, :tries, :user_agent,
|
|
20
|
+
:bearer_token, :auth_token_header
|
|
21
|
+
|
|
22
|
+
# Error list for retrying strategy.
|
|
23
|
+
# Initially contains common errors encountered usually in net calls.
|
|
24
|
+
attr_accessor :errors_to_recover
|
|
25
|
+
|
|
26
|
+
# Error list for stop and propagate strategy.
|
|
27
|
+
# Takes priority over +@errors_to_recover+.
|
|
28
|
+
# Do not assign ancestor error classes here that prevent retry for descendant ones.
|
|
29
|
+
attr_accessor :errors_to_propagate
|
|
30
|
+
|
|
31
|
+
##
|
|
32
|
+
# Construct and prepare client for requests targeting +endpoint+.
|
|
33
|
+
#
|
|
34
|
+
# == Parameters:
|
|
35
|
+
#
|
|
36
|
+
# [*endpoint*] +string+ Uri for the host with schema and port.
|
|
37
|
+
# any other segment like paths will be discarded.
|
|
38
|
+
# [*tries*] +integer+ to specify how many is to repeat failed calls. Default is 2.
|
|
39
|
+
# [*headers*] +hash+ to contain any common HTTP headers to be set in client calls.
|
|
40
|
+
# [*username*] +string+ for HTTP basic authentication. Applies on all requests. Default to nil.
|
|
41
|
+
# [*password*] +string+ for HTTP basic authentication. Applies on all requests. Default to nil.
|
|
42
|
+
# [*user_agent*] +string+ Specifies the _User-Agent_ header value when making requests.
|
|
43
|
+
# *User-Agent* header value provided within +headers+ parameter in +initialize+ or on one of
|
|
44
|
+
# request methods will take precedence over +user_agent+ parameter.
|
|
45
|
+
#
|
|
46
|
+
# == Example:
|
|
47
|
+
# require "network-client"
|
|
48
|
+
#
|
|
49
|
+
# github_client = Network::Client.new(endpoint: 'https://api.github.com')
|
|
50
|
+
# github_client.get '/emojis'
|
|
51
|
+
#
|
|
52
|
+
# #=> { "+1": "https://assets-cdn.github.com/images/icons/emoji/unicode/1f44d.png?v7",
|
|
53
|
+
# "-1": "https://assets-cdn.github.com/images/icons/emoji/unicode/1f44e.png?v7",
|
|
54
|
+
# ... }
|
|
55
|
+
#
|
|
56
|
+
def initialize(endpoint:, tries: 2, headers: {}, username: nil, password: nil,
|
|
57
|
+
user_agent: 'network-client gem')
|
|
58
|
+
@uri = URI.parse(endpoint)
|
|
59
|
+
@tries = tries
|
|
60
|
+
|
|
61
|
+
set_http_client
|
|
62
|
+
set_default_headers(headers)
|
|
63
|
+
set_basic_auth(username, password)
|
|
64
|
+
set_logger
|
|
65
|
+
define_error_strategies
|
|
66
|
+
set_user_agent(headers['User-Agent'] || user_agent)
|
|
67
|
+
set_bearer_auth
|
|
68
|
+
set_custom_token_auth
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# Perform a get request on the targeted client +endpoint+.
|
|
73
|
+
#
|
|
74
|
+
# == Parameters:
|
|
75
|
+
# [*path*] +string+ path on client's target host.
|
|
76
|
+
# [*params*] request parameters to be url encoded. Can be +hash+ or pair of values +array+.
|
|
77
|
+
# [*headers*] +hash+ set of http request headers.
|
|
78
|
+
#
|
|
79
|
+
# == Returns:
|
|
80
|
+
# http response data contained in +Response+ struct.
|
|
81
|
+
#
|
|
82
|
+
def get(path, params: {}, headers: {})
|
|
83
|
+
request_json :get, path, params, headers
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
##
|
|
87
|
+
# Perform a post request on the targeted client +endpoint+.
|
|
88
|
+
#
|
|
89
|
+
# == Parameters:
|
|
90
|
+
# [*path*] +string+ path on client's target host.
|
|
91
|
+
# [*params*] +hash+ request parameters to json encoded in request body.
|
|
92
|
+
# [*headers*] +hash+ set of http request headers.
|
|
93
|
+
#
|
|
94
|
+
# == Returns:
|
|
95
|
+
# http response data contained in +Response+ struct.
|
|
96
|
+
#
|
|
97
|
+
def post(path, params: {}, headers: {})
|
|
98
|
+
request_json :post, path, params, headers
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
##
|
|
102
|
+
# Perform a patch request on the targeted client +endpoint+.
|
|
103
|
+
#
|
|
104
|
+
# == Parameters:
|
|
105
|
+
# [*path*] +string+ path on client's target host.
|
|
106
|
+
# [*params*] +hash+ request parameters to json encoded in request body.
|
|
107
|
+
# [*headers*] +hash+ set of http request headers.
|
|
108
|
+
#
|
|
109
|
+
# == Returns:
|
|
110
|
+
# http response data contained in +Response+ struct.
|
|
111
|
+
#
|
|
112
|
+
def patch(path, params: {}, headers: {})
|
|
113
|
+
request_json :patch, path, params, headers
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
##
|
|
117
|
+
# Perform a put request on the targeted client +endpoint+.
|
|
118
|
+
#
|
|
119
|
+
# == Parameters:
|
|
120
|
+
# [*path*] +string+ path on client's target host.
|
|
121
|
+
# [*params*] +hash+ request parameters to json encoded in request body.
|
|
122
|
+
# [*headers*] +hash+ set of http request headers.
|
|
123
|
+
#
|
|
124
|
+
# == Returns:
|
|
125
|
+
# http response data cotained in +Response+ strcut.
|
|
126
|
+
#
|
|
127
|
+
def put(path, params: {}, headers: {})
|
|
128
|
+
request_json :put, path, params, headers
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
##
|
|
132
|
+
# Perform a delete request on the targeted client +endpoint+.
|
|
133
|
+
#
|
|
134
|
+
# == Parameters:
|
|
135
|
+
# [*path*] +string+ path on client's target host.
|
|
136
|
+
# [*params*] +hash+ request parameters to json encoded in request body.
|
|
137
|
+
# [*headers*] +hash+ set of http request headers.
|
|
138
|
+
#
|
|
139
|
+
# == Returns:
|
|
140
|
+
# http response data contained in +Response+ struct.
|
|
141
|
+
#
|
|
142
|
+
def delete(path, params: {}, headers: {})
|
|
143
|
+
request_json :delete, path, params, headers
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def get_html(path, params: {}, headers: {})
|
|
147
|
+
raise NotImplementedError
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def post_form(path, params: {}, headers: {})
|
|
151
|
+
raise NotImplementedError
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
##
|
|
155
|
+
# Sets the client logger object.
|
|
156
|
+
# Execution is yielded to passed +block+ to set, customize, and returning a logger instance.
|
|
157
|
+
#
|
|
158
|
+
# == Returns:
|
|
159
|
+
# +logger+ instance variable.
|
|
160
|
+
#
|
|
161
|
+
def set_logger
|
|
162
|
+
@logger = if block_given?
|
|
163
|
+
yield
|
|
164
|
+
elsif defined?(Rails)
|
|
165
|
+
Rails.logger
|
|
166
|
+
else
|
|
167
|
+
logger = Logger.new(STDOUT)
|
|
168
|
+
logger.level = Logger::DEBUG
|
|
169
|
+
logger
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def set_basic_auth(username, password)
|
|
174
|
+
@username = username.nil? ? '' : username
|
|
175
|
+
@password = password.nil? ? '' : password
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
##
|
|
179
|
+
# Assigns authentication bearer type token for use in standard HTTP authorization header.
|
|
180
|
+
#
|
|
181
|
+
# == Parameters:
|
|
182
|
+
# [*token*] +string+ bearer token value.
|
|
183
|
+
#
|
|
184
|
+
# == Returns:
|
|
185
|
+
# [@bearer_token] +string+ the newly assigned +@bearer_token+ value.
|
|
186
|
+
#
|
|
187
|
+
def set_bearer_auth(token: '')
|
|
188
|
+
@bearer_token = token
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
##
|
|
192
|
+
# Assigns custom authentication token for use in standard HTTP authorization header.
|
|
193
|
+
# This takes precedence over Bearer authentication if both are set.
|
|
194
|
+
#
|
|
195
|
+
# == Parameters:
|
|
196
|
+
# [*header_value*] +string+ full authorization header value. _(e.g. Token token=123)_.
|
|
197
|
+
#
|
|
198
|
+
# == Returns:
|
|
199
|
+
# [@auth_token_header] +string+ the newly assigned +@auth_token_header+ value.
|
|
200
|
+
#
|
|
201
|
+
def set_custom_token_auth(header_value: '')
|
|
202
|
+
@auth_token_header = header_value
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
##
|
|
206
|
+
# Assigns a new +User-Agent+ header to be sent in any subsequent request.
|
|
207
|
+
#
|
|
208
|
+
# == Parameters:
|
|
209
|
+
# [*new_user_agent*] +string+ the user-agent header value.
|
|
210
|
+
#
|
|
211
|
+
# == Returns:
|
|
212
|
+
# [@user_agent] +string+ the newly assigned +User-Agent+ header value.
|
|
213
|
+
#
|
|
214
|
+
def set_user_agent(new_user_agent)
|
|
215
|
+
@user_agent = @default_headers['User-Agent'] = new_user_agent
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
private
|
|
219
|
+
|
|
220
|
+
def set_http_client
|
|
221
|
+
@http = Net::HTTP.new(@uri.host, @uri.port)
|
|
222
|
+
@http.use_ssl = @uri.scheme == 'https' ? true : false
|
|
223
|
+
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def set_default_headers(headers)
|
|
227
|
+
@default_headers = DEFAULT_HEADERS.merge(headers)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def define_error_strategies
|
|
231
|
+
@errors_to_recover = [Net::HTTPTooManyRequests,
|
|
232
|
+
Net::HTTPServerError,
|
|
233
|
+
Net::ProtocolError,
|
|
234
|
+
Net::HTTPBadResponse,
|
|
235
|
+
Net::ReadTimeout,
|
|
236
|
+
Net::OpenTimeout,
|
|
237
|
+
Errno::ECONNREFUSED,
|
|
238
|
+
Errno::ETIMEDOUT,
|
|
239
|
+
OpenSSL::SSL::SSLError,
|
|
240
|
+
SocketError]
|
|
241
|
+
@errors_to_propagate = [Net::HTTPRequestURITooLarge,
|
|
242
|
+
Net::HTTPMethodNotAllowed]
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def request_json(http_method, path, params, headers)
|
|
246
|
+
response = request(http_method, path, params, headers)
|
|
247
|
+
body = parse_as_json(response.body)
|
|
248
|
+
Response.new(response.code.to_i, body)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def request(http_method, path, params, headers)
|
|
252
|
+
path = formulate_path(path)
|
|
253
|
+
path = encode_path_params(path, params) if http_method == :get
|
|
254
|
+
|
|
255
|
+
headers = @default_headers.merge(headers)
|
|
256
|
+
headers = authenticate(headers)
|
|
257
|
+
|
|
258
|
+
request = Net::HTTP::const_get(http_method.to_s.capitalize.to_sym).new(path, headers)
|
|
259
|
+
request.body = params.to_s unless http_method == :get
|
|
260
|
+
|
|
261
|
+
basic_auth(request)
|
|
262
|
+
|
|
263
|
+
response = http_request(request)
|
|
264
|
+
|
|
265
|
+
unless Net::HTTPSuccess === response
|
|
266
|
+
log "endpoint responded with non-success #{response.code} code.\nResponse: #{response.body}"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
response
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def basic_auth(request)
|
|
273
|
+
request.basic_auth(@username, @password) unless @username.empty? && @password.empty?
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def authenticate(headers)
|
|
277
|
+
headers['Authorization'] = "Bearer #{bearer_token}" unless bearer_token.empty?
|
|
278
|
+
headers['Authorization'] = auth_token_header unless auth_token_header.empty?
|
|
279
|
+
headers
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def http_request(request)
|
|
283
|
+
tries_count ||= @tries
|
|
284
|
+
finished = ->() { (tries_count -= 1).zero? }
|
|
285
|
+
|
|
286
|
+
begin
|
|
287
|
+
response = @http.request(request)
|
|
288
|
+
end until !recoverable?(response) || finished.call
|
|
289
|
+
response
|
|
290
|
+
|
|
291
|
+
rescue *@errors_to_propagate => error
|
|
292
|
+
log "Request Failed. \nReason: #{error.message}"
|
|
293
|
+
raise
|
|
294
|
+
|
|
295
|
+
rescue *@errors_to_recover => error
|
|
296
|
+
warn_on_retry "#{error.message}"
|
|
297
|
+
finished.call ? raise : retry
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def recoverable?(response)
|
|
301
|
+
if @errors_to_recover.any? { |error_class| response.is_a?(error_class) }
|
|
302
|
+
warn_on_retry "#{response.class} response type."
|
|
303
|
+
true
|
|
304
|
+
else
|
|
305
|
+
false
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def parse_as_json(response_body)
|
|
310
|
+
body = response_body
|
|
311
|
+
body = body.nil? || body.empty? ? body : JSON.parse(body)
|
|
312
|
+
|
|
313
|
+
rescue JSON::ParserError => error
|
|
314
|
+
log "Parsing response body as JSON failed! Returning raw body. \nDetails: \n#{error.message}"
|
|
315
|
+
body
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def encode_path_params(path, params)
|
|
319
|
+
if params.nil? || params.empty?
|
|
320
|
+
path
|
|
321
|
+
else
|
|
322
|
+
params = stringify_keys(params)
|
|
323
|
+
encoded = URI.encode_www_form(params)
|
|
324
|
+
[path, encoded].join("?")
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def formulate_path(path)
|
|
329
|
+
path = '/' if path.nil? || path.empty?
|
|
330
|
+
path.strip! if path.respond_to?(:strip)
|
|
331
|
+
path.prepend('/') unless path.chars.first == '/'
|
|
332
|
+
path
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def log(message)
|
|
336
|
+
@logger.error("\n#{LOG_TAG} #{message}.")
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def warn_on_retry(message)
|
|
340
|
+
@logger.warn("\n#{LOG_TAG} #{message} \nRetrying now ..")
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def stringify_keys(params)
|
|
344
|
+
params.respond_to?(:keys) ? params.collect { |k, v| [k.to_s, v] }.to_h : params
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
data/network-client.gemspec
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
lib = File.expand_path('../lib', __FILE__)
|
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
|
|
5
|
-
require 'network
|
|
5
|
+
require 'network/version'
|
|
6
6
|
require 'date'
|
|
7
7
|
|
|
8
8
|
Gem::Specification.new do |spec|
|
|
9
9
|
spec.name = "network-client"
|
|
10
|
-
spec.version =
|
|
10
|
+
spec.version = Network::VERSION
|
|
11
11
|
spec.date = Date.today.to_s
|
|
12
12
|
spec.authors = ["Abdullah Barrak (abarrak)"]
|
|
13
13
|
spec.email = ["abdullah@abarrak.com"]
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: network-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdullah Barrak (abarrak)
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2017-
|
|
11
|
+
date: 2017-07-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -127,8 +127,8 @@ files:
|
|
|
127
127
|
- README.md
|
|
128
128
|
- Rakefile
|
|
129
129
|
- lib/network-client.rb
|
|
130
|
-
- lib/network
|
|
131
|
-
- lib/network
|
|
130
|
+
- lib/network/client.rb
|
|
131
|
+
- lib/network/version.rb
|
|
132
132
|
- network-client.gemspec
|
|
133
133
|
homepage: https://github.com/abarrak/network-client
|
|
134
134
|
licenses:
|
data/lib/network-client/core.rb
DELETED
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
require 'net/http'
|
|
2
|
-
require 'json'
|
|
3
|
-
require 'logger'
|
|
4
|
-
|
|
5
|
-
module NetworkClient
|
|
6
|
-
class Client
|
|
7
|
-
|
|
8
|
-
HTTP_VERBS = {
|
|
9
|
-
:get => Net::HTTP::Get,
|
|
10
|
-
:post => Net::HTTP::Post,
|
|
11
|
-
:put => Net::HTTP::Put,
|
|
12
|
-
:delete => Net::HTTP::Delete
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
DEFAULT_HEADERS = { 'accept' => 'application/json',
|
|
16
|
-
'Content-Type' => 'application/json' }.freeze
|
|
17
|
-
|
|
18
|
-
# The success response template. Represents the return of rest-like methods holding two values:
|
|
19
|
-
# HTTP response code, and body (parsed as json if request type is json).
|
|
20
|
-
Response = Struct.new(:code, :body)
|
|
21
|
-
|
|
22
|
-
# Stamp in front of each log written by client *@logger*
|
|
23
|
-
LOG_TAG = '[NETWORK CLIENT]:'.freeze
|
|
24
|
-
|
|
25
|
-
attr_reader :username, :password, :default_headers, :logger, :tries
|
|
26
|
-
|
|
27
|
-
# Error list for retrying strategy.
|
|
28
|
-
# Initially contains common errors encountered usually in net calls.
|
|
29
|
-
attr_accessor :errors_to_recover
|
|
30
|
-
|
|
31
|
-
# Error list for stop and propagate strategy.
|
|
32
|
-
# Takes priority over *:errors_to_recover*.
|
|
33
|
-
# Do not assign ancestor error classes here that prevent retry for descendant ones.
|
|
34
|
-
attr_accessor :errors_to_propagate
|
|
35
|
-
|
|
36
|
-
##
|
|
37
|
-
# Construct and prepare client for requests targeting :endpoint.
|
|
38
|
-
#
|
|
39
|
-
# === Parameters:
|
|
40
|
-
#
|
|
41
|
-
# *endpoint*:
|
|
42
|
-
# Uri for the host with schema and port. any other segment like paths will be discarded.
|
|
43
|
-
# *tries*:
|
|
44
|
-
# Number to specify how many is to repeat failed calls. Default is 2.
|
|
45
|
-
# *headers*:
|
|
46
|
-
# Hash to contain any common HTTP headers to be set in client calls.
|
|
47
|
-
# *username*:
|
|
48
|
-
# for HTTP basic authentication. Applies on all requests. Default to nil.
|
|
49
|
-
# *password*:
|
|
50
|
-
# for HTTP basic authentication. Applies on all requests. Default to nil.
|
|
51
|
-
#
|
|
52
|
-
# === Example:
|
|
53
|
-
# require "network-client"
|
|
54
|
-
#
|
|
55
|
-
# github_client = NetworkClient::Client.new(endpoint: 'https://api.github.com')
|
|
56
|
-
# github_client.get '/emojis'
|
|
57
|
-
# #=> { "+1": "https://assets-cdn.github.com/images/icons/emoji/unicode/1f44d.png?v7",
|
|
58
|
-
# "-1": "https://assets-cdn.github.com/images/icons/emoji/unicode/1f44e.png?v7",
|
|
59
|
-
# "100": "https://assets-cdn.github.com/images/icons/emoji/unicode/1f4af.png?v7",
|
|
60
|
-
# ... }
|
|
61
|
-
#
|
|
62
|
-
def initialize(endpoint:, tries: 2, headers: {}, username: nil, password: nil)
|
|
63
|
-
@uri = URI.parse(endpoint)
|
|
64
|
-
@tries = tries
|
|
65
|
-
|
|
66
|
-
set_http_client
|
|
67
|
-
set_default_headers(headers)
|
|
68
|
-
set_basic_auth(username, password)
|
|
69
|
-
set_logger
|
|
70
|
-
define_error_strategies
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def get(path, params = {}, headers = {})
|
|
74
|
-
request_json :get, path, params, headers
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def post(path, params = {}, headers = {})
|
|
78
|
-
request_json :post, path, params, headers
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def put(path, params = {}, headers = {})
|
|
82
|
-
request_json :put, path, params, headers
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def delete(path, params = {}, headers = {})
|
|
86
|
-
request_json :delete, path, params, headers
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def post_form(path, params = {}, headers = {})
|
|
90
|
-
raise NotImplementedError
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def put_form(path, params = {}, headers = {})
|
|
94
|
-
raise NotImplementedError
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def set_logger
|
|
98
|
-
@logger = if block_given?
|
|
99
|
-
yield
|
|
100
|
-
elsif defined?(Rails)
|
|
101
|
-
Rails.logger
|
|
102
|
-
else
|
|
103
|
-
logger = Logger.new(STDOUT)
|
|
104
|
-
logger.level = Logger::DEBUG
|
|
105
|
-
logger
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def set_basic_auth(username, password)
|
|
110
|
-
@username = username.nil? ? '' : username
|
|
111
|
-
@password = password.nil? ? '' : password
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
private
|
|
115
|
-
|
|
116
|
-
def set_http_client
|
|
117
|
-
@http = Net::HTTP.new(@uri.host, @uri.port)
|
|
118
|
-
@http.use_ssl = @uri.scheme == 'https' ? true : false
|
|
119
|
-
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def set_default_headers(headers)
|
|
123
|
-
@default_headers = DEFAULT_HEADERS.merge(headers)
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def define_error_strategies
|
|
127
|
-
@errors_to_recover = [Net::HTTPTooManyRequests,
|
|
128
|
-
Net::HTTPServerError,
|
|
129
|
-
Net::ProtocolError,
|
|
130
|
-
Net::HTTPBadResponse,
|
|
131
|
-
Net::ReadTimeout,
|
|
132
|
-
Net::OpenTimeout,
|
|
133
|
-
Errno::ECONNREFUSED,
|
|
134
|
-
Errno::ETIMEDOUT,
|
|
135
|
-
OpenSSL::SSL::SSLError,
|
|
136
|
-
SocketError]
|
|
137
|
-
@errors_to_propagate = [Net::HTTPRequestURITooLarge,
|
|
138
|
-
Net::HTTPMethodNotAllowed]
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def request_json(http_method, path, params, headers)
|
|
142
|
-
response = request(http_method, path, params, headers)
|
|
143
|
-
body = parse_as_json(response.body)
|
|
144
|
-
Response.new(response.code.to_i, body)
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def request(http_method, path, params, headers)
|
|
148
|
-
headers = @default_headers.merge(headers)
|
|
149
|
-
path = formulate_path(path)
|
|
150
|
-
|
|
151
|
-
case http_method
|
|
152
|
-
when :get
|
|
153
|
-
full_path = encode_path_params(path, params)
|
|
154
|
-
request = HTTP_VERBS[http_method].new(full_path, headers)
|
|
155
|
-
else
|
|
156
|
-
request = HTTP_VERBS[http_method].new(path, headers)
|
|
157
|
-
request.body = params.to_s
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
basic_auth(request)
|
|
161
|
-
response = http_request(request)
|
|
162
|
-
|
|
163
|
-
unless Net::HTTPSuccess === response
|
|
164
|
-
log "endpoint responded with non-success #{response.code} code.\nResponse: #{response.body}"
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
response
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def basic_auth(request)
|
|
171
|
-
request.basic_auth(@username, @password) unless @username.empty? && @password.empty?
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def http_request(request)
|
|
175
|
-
tries_count ||= @tries
|
|
176
|
-
finished = ->() { (tries_count -= 1).zero? }
|
|
177
|
-
|
|
178
|
-
begin
|
|
179
|
-
response = @http.request(request)
|
|
180
|
-
end until !recoverable?(response) || finished.call
|
|
181
|
-
response
|
|
182
|
-
|
|
183
|
-
rescue *@errors_to_propagate => error
|
|
184
|
-
log "Request Failed. \nReason: #{error.message}"
|
|
185
|
-
raise
|
|
186
|
-
|
|
187
|
-
rescue *@errors_to_recover => error
|
|
188
|
-
warn_on_retry "#{error.message}"
|
|
189
|
-
finished.call ? raise : retry
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def recoverable?(response)
|
|
193
|
-
if @errors_to_recover.any? { |error_class| response.is_a?(error_class) }
|
|
194
|
-
warn_on_retry "#{response.class} response type."
|
|
195
|
-
true
|
|
196
|
-
else
|
|
197
|
-
false
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def parse_as_json(response_body)
|
|
202
|
-
body = response_body
|
|
203
|
-
body = body.nil? || body.empty? ? body : JSON.parse(body)
|
|
204
|
-
|
|
205
|
-
rescue JSON::ParserError => error
|
|
206
|
-
log "Parsing response body as JSON failed! Returning raw body. \nDetails: \n#{error.message}"
|
|
207
|
-
body
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def encode_path_params(path, params)
|
|
211
|
-
if params.nil? || params.empty?
|
|
212
|
-
path
|
|
213
|
-
else
|
|
214
|
-
params = stringify_keys(params)
|
|
215
|
-
encoded = URI.encode_www_form(params)
|
|
216
|
-
[path, encoded].join("?")
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def formulate_path(path)
|
|
221
|
-
path = '/' if path.nil? || path.empty?
|
|
222
|
-
path.strip! if path.respond_to?(:strip)
|
|
223
|
-
path.prepend('/') unless path.chars.first == '/'
|
|
224
|
-
path
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
def log(message)
|
|
228
|
-
@logger.error("\n#{LOG_TAG} #{message}.")
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
def warn_on_retry(message)
|
|
232
|
-
@logger.warn("\n#{LOG_TAG} #{message} \nRetrying now ..")
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def stringify_keys(params)
|
|
236
|
-
params.respond_to?(:keys) ? params.collect { |k, v| [k.to_s, v] }.to_h : params
|
|
237
|
-
end
|
|
238
|
-
end
|
|
239
|
-
end
|