dc8aa5b6
Joe Weakley
Rubocop corrections
|
1
2
|
# frozen_string_literal: true
|
51fb5579
Isaac Lewis
add client test, ...
|
3
|
module Syspro
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
4
5
|
# This class is the main syspro client
class SysproClient # rubocop:disable Metrics/ClassLength
|
db76748d
Isaac Lewis
cop a bunch of St...
|
6
7
|
attr_accessor :conn, :api_base
|
697a8854
Isaac Lewis
update tests
|
8
9
|
@verify_ssl_warned = false
|
51fb5579
Isaac Lewis
add client test, ...
|
10
11
12
13
14
|
def initialize(conn = nil)
self.conn = conn || self.class.default_conn
@system_profiler = SystemProfiler.new
end
|
49716587
Isaac Lewis
refactor object s...
|
15
|
def logon(username, password, company_id, company_password)
|
697a8854
Isaac Lewis
update tests
|
16
|
Syspro::Logon.logon(username, password, company_id, company_password)
|
49716587
Isaac Lewis
refactor object s...
|
17
18
|
end
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
19
|
def get_syspro_version # rubocop:disable Naming/AccessorMethodName
|
697a8854
Isaac Lewis
update tests
|
20
|
Syspro::GetVersion.get_version
|
49716587
Isaac Lewis
refactor object s...
|
21
22
|
end
|
51fb5579
Isaac Lewis
add client test, ...
|
23
24
25
26
27
|
def self.active_client
Thread.current[:syspro_client] || default_client
end
def self.default_client
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
28
|
Thread.current[:syspro_client_default_client] ||= SysproClient.new(default_conn) # rubocop:disable Metrics/LineLength
|
51fb5579
Isaac Lewis
add client test, ...
|
29
30
|
end
|
db76748d
Isaac Lewis
cop a bunch of St...
|
31
32
33
|
# A default Faraday connection to be used when one isn't configured. This
# object should never be mutated, and instead instantiating your own
# connection and wrapping it in a SysproClient object should be preferred.
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
34
|
def self.default_conn # rubocop:disable Metrics/MethodLength
|
db76748d
Isaac Lewis
cop a bunch of St...
|
35
36
37
38
39
40
41
42
43
44
45
|
# We're going to keep connections around so that we can take advantage
# of connection re-use, so make sure that we have a separate connection
# object per thread.
Thread.current[:syspro_client_default_conn] ||= begin
conn = Faraday.new do |c|
c.use Faraday::Request::Multipart
c.use Faraday::Request::UrlEncoded
c.use Faraday::Response::RaiseError
c.adapter Faraday.default_adapter
end
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
46
47
48
49
50
51
|
# For now, we're not verifying SSL certificates.
# The warning will appear.
# if Syspro.verify_ssl_certs
# conn.ssl.verify = true
# conn.ssl.cert_store = Syspro.ca_store
# else
|
db76748d
Isaac Lewis
cop a bunch of St...
|
52
53
54
55
|
conn.ssl.verify = false
unless @verify_ssl_warned
@verify_ssl_warned = true
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
56
57
|
warn('WARNING: Running without SSL cert verification. ' \
'You should never do this in production. ' \
|
db76748d
Isaac Lewis
cop a bunch of St...
|
58
59
|
"Execute 'Syspro.verify_ssl_certs = true' to enable verification.")
end
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
60
|
# end
|
db76748d
Isaac Lewis
cop a bunch of St...
|
61
62
63
64
65
|
conn
end
end
|
3d0157a5
Isaac Lewis
add logoff
|
66
67
|
# Executes the API call within the given block. Usage looks like:
#
|
4a8bba96
Isaac Lewis
cleanup
|
68
|
# client = SysproClient.new
|
3d0157a5
Isaac Lewis
add logoff
|
69
70
71
72
|
# charge, resp = client.request { Charge.create }
#
def request
@last_response = nil
|
4a8bba96
Isaac Lewis
cleanup
|
73
74
|
old_syspro_client = Thread.current[:syspro_client]
Thread.current[:syspro_client] = self
|
3d0157a5
Isaac Lewis
add logoff
|
75
76
77
78
79
|
begin
res = yield
[res, @last_response]
ensure
|
4a8bba96
Isaac Lewis
cleanup
|
80
|
Thread.current[:syspro_client] = old_syspro_client
|
3d0157a5
Isaac Lewis
add logoff
|
81
82
83
|
end
end
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
84
|
def execute_request(method, path, user_id: nil, api_base: nil, headers: {}, params: {}) # rubocop:disable Metrics/LineLength, Metrics/MethodLength
|
db76748d
Isaac Lewis
cop a bunch of St...
|
85
|
api_base ||= Syspro.api_base
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
86
|
user_id ||= ''
|
db76748d
Isaac Lewis
cop a bunch of St...
|
87
88
89
90
91
92
93
94
95
96
97
|
params = Util.objects_to_ids(params)
url = api_url(path, api_base)
body = nil
query_params = nil
case method.to_s.downcase.to_sym
when :get, :head, :delete
query_params = params
else
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
98
|
body = if headers[:content_type] && headers[:content_type] == 'multipart/form-data' # rubocop:disable Metrics/LineLength
|
db76748d
Isaac Lewis
cop a bunch of St...
|
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
|
params
else
Util.encode_parameters(params)
end
end
headers = request_headers(method)
.update(Util.normalize_headers(headers))
# stores information on the request we're about to make so that we don't
# have to pass as many parameters around for logging.
context = RequestLogContext.new
context.body = body
context.method = method
context.path = path
|
3d0157a5
Isaac Lewis
add logoff
|
114
|
context.user_id = user_id
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
115
|
context.query_params = query_params ? Util.encode_parameters(query_params) : nil # rubocop:disable Metrics/LineLength
|
db76748d
Isaac Lewis
cop a bunch of St...
|
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
|
http_resp = execute_request_with_rescues(api_base, context) do
conn.run_request(method, url, body, headers) do |req|
req.options.open_timeout = Syspro.open_timeout
req.options.timeout = Syspro.read_timeout
req.params = query_params unless query_params.nil?
end
end
begin
resp = SysproResponse.from_faraday_response(http_resp)
rescue JSON::ParserError
raise general_api_error(http_resp.status, http_resp.body)
end
# Allows SysproClient#request to return a response object to a caller.
@last_response = resp
[resp]
end
def general_api_error(status, body)
|
0c0af54a
Isaac Lewis
error handling; c...
|
137
|
ApiError.new("Invalid response object from API: #{body.inspect} " \
|
db76748d
Isaac Lewis
cop a bunch of St...
|
138
139
140
141
|
"(HTTP response code was #{status})",
http_status: status, http_body: body)
end
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
142
|
def api_url(url = '', api_base = nil)
|
db76748d
Isaac Lewis
cop a bunch of St...
|
143
144
145
|
(api_base || Syspro.api_base) + url
end
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
146
|
def request_headers(_method)
|
db76748d
Isaac Lewis
cop a bunch of St...
|
147
148
149
|
user_agent = "Syspro/7 RubyBindings/#{Syspro::VERSION}"
headers = {
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
150
151
|
'User-Agent' => user_agent,
'Content-Type' => 'application/x-www-form-urlencoded'
|
db76748d
Isaac Lewis
cop a bunch of St...
|
152
153
154
155
156
|
}
headers
end
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
157
|
def execute_request_with_rescues(api_base, context) # rubocop:disable Metrics/LineLength, Metrics/MethodLength
|
db76748d
Isaac Lewis
cop a bunch of St...
|
158
159
160
161
162
|
num_retries = 0
begin
request_start = Time.now
log_request(context, num_retries)
resp = yield
|
db76748d
Isaac Lewis
cop a bunch of St...
|
163
164
165
|
log_response(context, request_start, resp.status, resp.body)
# We rescue all exceptions from a request so that we have an easy spot to
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
166
167
|
# implement our retry logic across the board. We'll re-raise if it's a
# type of exception that we didn't expect to handle.
|
db76748d
Isaac Lewis
cop a bunch of St...
|
168
|
rescue StandardError => e
|
db76748d
Isaac Lewis
cop a bunch of St...
|
169
|
if e.respond_to?(:response) && e.response
|
e3484f8f
Isaac Lewis
remove dup check
|
170
|
log_response(context, request_start,
|
db76748d
Isaac Lewis
cop a bunch of St...
|
171
172
|
e.response[:status], e.response[:body])
else
|
e3484f8f
Isaac Lewis
remove dup check
|
173
|
log_response_error(context, request_start, e)
|
db76748d
Isaac Lewis
cop a bunch of St...
|
174
175
176
177
178
179
180
181
182
183
184
|
end
if self.class.should_retry?(e, num_retries)
num_retries += 1
sleep self.class.sleep_time(num_retries)
retry
end
case e
when Faraday::ClientError
if e.response
|
96149efa
Isaac Lewis
working query browse
|
185
|
handle_error_response(e.response, context)
|
db76748d
Isaac Lewis
cop a bunch of St...
|
186
|
else
|
96149efa
Isaac Lewis
working query browse
|
187
|
handle_network_error(e, context, num_retries, api_base)
|
db76748d
Isaac Lewis
cop a bunch of St...
|
188
189
190
191
192
193
194
195
196
197
198
199
|
end
# Only handle errors when we know we can do so, and re-raise otherwise.
# This should be pretty infrequent.
else
raise
end
end
resp
end
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
200
201
|
def handle_network_error(e, context, num_retries, api_base = nil) # rubocop:disable Metrics/LineLength, Metrics/MethodLength, Naming/UncommunicativeMethodParamName
Util.log_error('Syspro network error',
|
96149efa
Isaac Lewis
working query browse
|
202
203
204
205
206
|
error_message: e.message,
request_id: context.request_id)
case e
when Faraday::ConnectionFailed
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
207
|
message = 'Unexpected error communicating when trying to connect to Syspro.' # rubocop:disable Metrics/LineLength
|
96149efa
Isaac Lewis
working query browse
|
208
209
|
when Faraday::SSLError
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
210
|
message = 'Could not establish a secure connection to Syspro.'
|
96149efa
Isaac Lewis
working query browse
|
211
212
213
214
|
when Faraday::TimeoutError
api_base ||= Syspro.api_base
message = "Could not connect to Syspro (#{api_base}). " \
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
215
216
|
'Please check your internet connection and try again. ' \
'If this problem persists, you should check your Syspro service status.' # rubocop:disable Metrics/LineLength
|
96149efa
Isaac Lewis
working query browse
|
217
218
|
else
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
219
220
|
message = 'Unexpected error communicating with Syspro. ' \
'If this problem persists, talk to your Syspro implementation team.'
|
96149efa
Isaac Lewis
working query browse
|
221
222
223
|
end
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
224
|
message += " Request was retried #{num_retries} times." if num_retries.positive? # rubocop:disable Metrics/LineLength
|
96149efa
Isaac Lewis
working query browse
|
225
226
227
228
|
raise ApiConnectionError, message + "\n\n(Network error: #{e.message})"
end
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
229
|
def self.should_retry?(e, num_retries) # rubocop:disable Metrics/LineLength, Naming/UncommunicativeMethodParamName
|
eef01045
Isaac Lewis
re-implement max_...
|
230
231
|
return false if num_retries >= Syspro.max_network_retries
|
db76748d
Isaac Lewis
cop a bunch of St...
|
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
|
# Retry on timeout-related problems (either on open or read).
return true if e.is_a?(Faraday::TimeoutError)
# Destination refused the connection, the connection was reset, or a
# variety of other connection failures. This could occur from a single
# saturated server, so retry in case it's intermittent.
return true if e.is_a?(Faraday::ConnectionFailed)
if e.is_a?(Faraday::ClientError) && e.response
# 409 conflict
return true if e.response[:status] == 409
end
false
end
def log_request(context, num_retries)
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
249
|
Util.log_info('Request to Syspro API',
|
db76748d
Isaac Lewis
cop a bunch of St...
|
250
251
252
253
254
|
account: context.account,
api_version: context.api_version,
method: context.method,
num_retries: num_retries,
path: context.path)
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
255
|
Util.log_debug('Request details',
|
db76748d
Isaac Lewis
cop a bunch of St...
|
256
257
258
259
260
|
body: context.body,
query_params: context.query_params)
end
private :log_request
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
261
262
|
def log_response(context, request_start, status, body) # rubocop:disable Metrics/LineLength, Metrics/MethodLength
Util.log_info('Response from Syspro API',
|
db76748d
Isaac Lewis
cop a bunch of St...
|
263
264
265
266
267
268
269
|
account: context.account,
api_version: context.api_version,
elapsed: Time.now - request_start,
method: context.method,
path: context.path,
request_id: context.request_id,
status: status)
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
270
|
Util.log_debug('Response details',
|
db76748d
Isaac Lewis
cop a bunch of St...
|
271
272
273
274
275
|
body: body,
request_id: context.request_id)
end
private :log_response
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
276
277
|
def log_response_error(context, request_start, e) # rubocop:disable Metrics/LineLength, Naming/UncommunicativeMethodParamName
Util.log_error('Request error',
|
db76748d
Isaac Lewis
cop a bunch of St...
|
278
279
280
281
282
283
284
|
elapsed: Time.now - request_start,
error_message: e.message,
method: context.method,
path: context.path)
end
private :log_response_error
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
285
|
def handle_error_response(http_resp, context) # rubocop:disable Metrics/LineLength, Metrics/MethodLength
|
96149efa
Isaac Lewis
working query browse
|
286
287
288
289
|
begin
resp = SysproResponse.from_faraday_hash(http_resp)
error_data = resp.data[:error]
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
290
|
raise SysproError, 'Indeterminate error' unless error_data
|
96149efa
Isaac Lewis
working query browse
|
291
292
293
294
295
296
297
298
299
300
301
302
303
304
|
rescue Nokogiri::XML::SyntaxError, SysproError
raise general_api_error(http_resp[:status], http_resp[:body])
end
error = if error_data.is_a?(String)
specific_oauth_error(resp, error_data, context)
else
specific_api_error(resp, error_data, context)
end
error.response = resp
raise(error)
end
|
db76748d
Isaac Lewis
cop a bunch of St...
|
305
306
307
308
309
310
311
312
313
314
315
|
# RequestLogContext stores information about a request that's begin made so
# that we can log certain information. It's useful because it means that we
# don't have to pass around as many parameters.
class RequestLogContext
attr_accessor :body
attr_accessor :account
attr_accessor :api_version
attr_accessor :method
attr_accessor :path
attr_accessor :query_params
attr_accessor :request_id
|
3d0157a5
Isaac Lewis
add logoff
|
316
|
attr_accessor :user_id
|
db76748d
Isaac Lewis
cop a bunch of St...
|
317
318
319
320
321
322
|
end
# SystemProfiler extracts information about the system that we're running
# in so that we can generate a rich user agent header to help debug
# integrations.
class SystemProfiler
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
323
324
325
|
def self.uname # rubocop:disable Metrics/MethodLength
if File.exist?('/proc/version')
File.read('/proc/version').strip
|
db76748d
Isaac Lewis
cop a bunch of St...
|
326
|
else
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
327
|
case RbConfig::CONFIG['host_os']
|
db76748d
Isaac Lewis
cop a bunch of St...
|
328
329
330
331
332
|
when /linux|darwin|bsd|sunos|solaris|cygwin/i
uname_from_system
when /mswin|mingw/i
uname_from_system_ver
else
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
333
|
'unknown platform'
|
db76748d
Isaac Lewis
cop a bunch of St...
|
334
335
336
337
338
|
end
end
end
def self.uname_from_system
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
339
|
(`uname -a 2>/dev/null` || '').strip
|
db76748d
Isaac Lewis
cop a bunch of St...
|
340
|
rescue Errno::ENOENT
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
341
|
'uname executable not found'
|
db76748d
Isaac Lewis
cop a bunch of St...
|
342
|
rescue Errno::ENOMEM # couldn't create subprocess
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
343
|
'uname lookup failed'
|
db76748d
Isaac Lewis
cop a bunch of St...
|
344
345
346
|
end
def self.uname_from_system_ver
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
347
|
(`ver` || '').strip
|
db76748d
Isaac Lewis
cop a bunch of St...
|
348
|
rescue Errno::ENOENT
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
349
|
'ver executable not found'
|
db76748d
Isaac Lewis
cop a bunch of St...
|
350
|
rescue Errno::ENOMEM # couldn't create subprocess
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
351
|
'uname lookup failed'
|
db76748d
Isaac Lewis
cop a bunch of St...
|
352
353
354
355
356
357
|
end
def initialize
@uname = self.class.uname
end
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
358
359
|
def user_agent # rubocop:disable Metrics/MethodLength
lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})" # rubocop:disable Metrics/LineLength
|
51fb5579
Isaac Lewis
add client test, ...
|
360
|
|
db76748d
Isaac Lewis
cop a bunch of St...
|
361
362
363
|
{
application: Syspro.app_info,
bindings_version: Syspro::VERSION,
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
364
|
lang: 'ruby',
|
db76748d
Isaac Lewis
cop a bunch of St...
|
365
366
|
lang_version: lang_version,
platform: RUBY_PLATFORM,
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
367
|
engine: defined?(RUBY_ENGINE) ? RUBY_ENGINE : '',
|
db76748d
Isaac Lewis
cop a bunch of St...
|
368
|
uname: @uname,
|
dc8aa5b6
Joe Weakley
Rubocop corrections
|
369
|
hostname: Socket.gethostname
|
db76748d
Isaac Lewis
cop a bunch of St...
|
370
371
|
}.delete_if { |_k, v| v.nil? }
end
|
51fb5579
Isaac Lewis
add client test, ...
|
372
373
374
|
end
end
end
|