mirakl 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7d4f9bb25f96fe677dced256c5fa948c1184e075bac25a958738fcba22471203
4
+ data.tar.gz: 05f2fd117d01770311bdede071373752c052a504c686dc20f457aaedce54f310
5
+ SHA512:
6
+ metadata.gz: '02590455d52453f9ac29d2e85cf8929b75b086d6ea56b781d26d78ea6ab34ee63c3e53ef656337850747ac3e17f3a9ee876f716c5e18af968e056c5e76bab785'
7
+ data.tar.gz: ca85c9d65d161c2a3cca4658de4ca77e91a2c086a8c09bf6ce92b16b85787157652d4734457b00a8093bd5b057e7cc03b3c5ae14f2ad552e0f501223f5091cb1
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ /mirakl-*.gem
2
+ /Gemfile.lock
3
+ .ruby-version
4
+ Gemfile.lock
5
+ tags
6
+ /.bundle/
data/Gemfile ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ group :development do
8
+ gem "coveralls", require: false
9
+ gem "mocha", "~> 0.13.2"
10
+ gem "rake"
11
+ gem "shoulda-context"
12
+ gem "test-unit"
13
+ gem "timecop"
14
+ gem "webmock"
15
+
16
+ # Rubocop changes pretty quickly: new cops get added and old cops change
17
+ # names or go into new namespaces. This is a library and we don't have
18
+ # `Gemfile.lock` checked in, so to prevent good builds from suddenly going
19
+ # bad, pin to a specific version number here. Try to keep this relatively
20
+ # up-to-date, but it's not the end of the world if it's not.
21
+ # Note that 0.57.2 is the most recent version we can use until we drop
22
+ # support for Ruby 2.1.
23
+ gem "rubocop", "0.57.2"
24
+
25
+ # Rack 2.0+ requires Ruby >= 2.2.2 which is problematic for the test suite on
26
+ # older Ruby versions. Check Ruby the version here and put a maximum
27
+ # constraint on Rack if necessary.
28
+ if RUBY_VERSION >= "2.2.2"
29
+ gem "rack", ">= 2.0.6"
30
+ else
31
+ gem "rack", ">= 1.6.11", "< 2.0" # rubocop:disable Bundler/DuplicatedGem
32
+ end
33
+
34
+ platforms :mri do
35
+ gem "byebug"
36
+ gem "pry"
37
+ gem "pry-byebug"
38
+ end
39
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2021 - Mirakl SAS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
22
+
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require "irb"
6
+ require "irb/completion"
7
+
8
+ require "#{::File.dirname(__FILE__)}/../lib/mirakl"
9
+
10
+ # Config IRB to enable --simple-prompt and auto indent
11
+ IRB.conf[:PROMPT_MODE] = :SIMPLE
12
+ IRB.conf[:AUTO_INDENT] = true
13
+
14
+ puts "Loaded gem 'mirakl'"
15
+
16
+ IRB.start
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mirakl
4
+ module ApiOperations
5
+ module Request
6
+ module ClassMethods
7
+ def request(method, url, params = {}, opts = {})
8
+ warn_on_opts_in_params(params)
9
+
10
+ opts = Util.normalize_opts(opts)
11
+ opts[:client] ||= MiraklClient.active_client
12
+
13
+ headers = opts.clone
14
+ api_key = headers.delete(:api_key)
15
+ api_base = headers.delete(:api_base)
16
+ client = headers.delete(:client)
17
+ # Assume all remaining opts must be headers
18
+
19
+ resp, opts[:api_key] = client.execute_request(
20
+ method, url,
21
+ api_base: api_base, api_key: api_key,
22
+ headers: headers, params: params
23
+ )
24
+
25
+ # Hash#select returns an array before 1.9
26
+ opts_to_persist = {}
27
+ opts.each do |k, v|
28
+ opts_to_persist[k] = v if Util::OPTS_PERSISTABLE.include?(k)
29
+ end
30
+
31
+ [resp, opts_to_persist]
32
+ end
33
+
34
+ private def warn_on_opts_in_params(params)
35
+ Util::OPTS_USER_SPECIFIED.each do |opt|
36
+ if params.key?(opt)
37
+ warn("WARNING: #{opt} should be in opts instead of params.")
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def self.included(base)
44
+ base.extend(ClassMethods)
45
+ end
46
+
47
+ def request(method, url, params = {}, opts = {})
48
+ # opts = @opts.merge(Util.normalize_opts(opts))
49
+ opts = Util.normalize_opts(opts)
50
+ self.class.request(method, url, params, opts)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,62 @@
1
+ module Mirakl
2
+ class MiraklError < StandardError
3
+ attr_reader :message
4
+
5
+ # Response contains a MiraklError object that has some basic information
6
+ # about the response that conveyed the error.
7
+ attr_accessor :response
8
+
9
+ attr_reader :code
10
+ attr_reader :http_body
11
+ attr_reader :http_headers
12
+ attr_reader :http_status
13
+ attr_reader :json_body # equivalent to #data
14
+
15
+ # Initializes a StripeError.
16
+ def initialize(message = nil, http_status: nil, http_body: nil,
17
+ json_body: nil, http_headers: nil, code: nil)
18
+ @message = message
19
+ @http_status = http_status
20
+ @http_body = http_body
21
+ @http_headers = http_headers || {}
22
+ @json_body = json_body
23
+ @code = code
24
+ end
25
+
26
+ def to_s
27
+ status_string = @http_status.nil? ? "" : "(Status #{@http_status}) "
28
+ "#{status_string}#{@message}"
29
+ end
30
+ end
31
+
32
+ # AuthenticationError is raised when invalid credentials are used to connect
33
+ # to Mirakl's servers.
34
+ class AuthenticationError < MiraklError
35
+ end
36
+
37
+ class APIError < MiraklError
38
+ end
39
+
40
+ # BadRequestError is raised when a request is initiated with invalid
41
+ # parameters.
42
+ class InvalidRequestError < MiraklError
43
+ def initialize(message, param, http_status: nil, http_body: nil,
44
+ json_body: nil, http_headers: nil, code: nil)
45
+ super(message, http_status: http_status, http_body: http_body,
46
+ json_body: json_body, http_headers: http_headers,
47
+ code: code)
48
+ @param = param
49
+ end
50
+ end
51
+
52
+ UnauthorizedError = Class.new(MiraklError)
53
+ ForbiddenError = Class.new(MiraklError)
54
+ ApiRequestsQuotaReachedError = Class.new(MiraklError)
55
+ NotFoundError = Class.new(MiraklError)
56
+ MethodNotAllowedError = Class.new(MiraklError)
57
+ NotAcceptableError = Class.new(MiraklError)
58
+ GoneError = Class.new(MiraklError)
59
+ UnsupportedMediaTypeError = Class.new(MiraklError)
60
+ TooManyRequestsError = Class.new(MiraklError)
61
+
62
+ end
@@ -0,0 +1,476 @@
1
+ module Mirakl
2
+ class MiraklClient
3
+ # MiraklAPIError = Class.new(StandardError)
4
+ #
5
+ # BadRequestError = Class.new(MiraklAPIError)
6
+ # UnauthorizedError = Class.new(MiraklAPIError)
7
+ # ForbiddenError = Class.new(MiraklAPIError)
8
+ # ApiRequestsQuotaReachedError = Class.new(MiraklAPIError)
9
+ # NotFoundError = Class.new(MiraklAPIError)
10
+ # MethodNotAllowedError = Class.new(MiraklAPIError)
11
+ # NotAcceptableError = Class.new(MiraklAPIError)
12
+ # GoneError = Class.new(MiraklAPIError)
13
+ # UnsupportedMediaTypeError = Class.new(MiraklAPIError)
14
+ # TooManyRequestsError = Class.new(MiraklAPIError)
15
+ # ApiError = Class.new(MiraklAPIError)
16
+ #
17
+ #
18
+ # HTTP_OK_CODE = 200
19
+ # HTTP_CREATED_CODE = 201
20
+ # HTTP_NO_CONTENT_CODE = 204
21
+ #
22
+ # HTTP_BAD_REQUEST_CODE = 400
23
+ # HTTP_UNAUTHORIZED_CODE = 401
24
+ # HTTP_FORBIDDEN_CODE = 403
25
+ # HTTP_NOT_FOUND_CODE = 404
26
+ # HTTP_METHOD_NOT_ALLOWED_CODE = 405
27
+ # HTTP_NOT_ACCEPTABLE_CODE = 406
28
+ # HTTP_GONE_CODE = 410
29
+ # HTTP_UNSUPPORTED_MEDIA_TYPE_CODE = 415
30
+ # HTTP_TOO_MANY_REQUESTS_CODE = 429
31
+
32
+
33
+ attr_accessor :conn
34
+
35
+ def initialize(conn = nil)
36
+ self.conn = conn || self.class.default_conn
37
+ end
38
+
39
+
40
+ def self.active_client
41
+ Thread.current[:mirakl_client] || default_client
42
+ end
43
+
44
+ def self.default_client
45
+ Thread.current[:mirakl_default_client] ||=
46
+ Mirakl::MiraklClient.new(default_conn)
47
+ end
48
+
49
+ # A default Faraday connection to be used when one isn't configured. This
50
+ # object should never be mutated, and instead instantiating your own
51
+ # connection and wrapping it in a Mirakl::MiraklClient object should be preferred.
52
+ def self.default_conn
53
+ # We're going to keep connections around so that we can take advantage
54
+ # of connection re-use, so make sure that we have a separate connection
55
+ # object per thread.
56
+ Thread.current[:mirakl_client_default_conn] ||= begin
57
+ conn = Faraday.new do |builder|
58
+ builder.request :multipart, flat_encode: true
59
+ # builder.use Faraday::Request::Multipart,
60
+ builder.use Faraday::Request::UrlEncoded
61
+ builder.use Faraday::Response::RaiseError
62
+
63
+ # Net::HTTP::Persistent doesn't seem to do well on Windows or JRuby,
64
+ # so fall back to default there.
65
+ if Gem.win_platform? || RUBY_PLATFORM == "java"
66
+ builder.adapter :net_http
67
+ else
68
+ builder.adapter :net_http_persistent
69
+ end
70
+ end
71
+
72
+
73
+ # if Mirakl.verify_ssl_certs
74
+ # conn.ssl.verify = true
75
+ # conn.ssl.cert_store = Mirakl.ca_store
76
+ # else
77
+ # conn.ssl.verify = false
78
+ #
79
+ # unless @verify_ssl_warned
80
+ # @verify_ssl_warned = true
81
+ # warn("WARNING: Running without SSL cert verification. " \
82
+ # "You should never do this in production. " \
83
+ # "Execute `Mirakl.verify_ssl_certs = true` to enable " \
84
+ # "verification.")
85
+ # end
86
+ # end
87
+
88
+ conn
89
+ end
90
+ end
91
+
92
+ # Executes the API call within the given block. Usage looks like:
93
+ #
94
+ # client = MiraklClient.new
95
+ # obj, resp = client.request { ... }
96
+ #
97
+ def request
98
+ @last_response = nil
99
+ old_mirakl_client = Thread.current[:mirakl_client]
100
+ Thread.current[:mirakl_client] = self
101
+
102
+ begin
103
+ res = yield
104
+ [res, @last_response]
105
+ ensure
106
+ Thread.current[:mirakl_client] = old_mirakl_client
107
+ end
108
+ end
109
+
110
+
111
+ def execute_request(method, path,
112
+ api_base: nil, api_key: nil, headers: {}, params: {})
113
+
114
+ api_base ||= Mirakl.api_base
115
+ api_key ||= Mirakl.api_key
116
+ # params = Util.objects_to_ids(params)
117
+
118
+ check_api_key!(api_key)
119
+
120
+ body = nil
121
+ query_params = nil
122
+ case method.to_s.downcase.to_sym
123
+ when :get, :head, :delete
124
+ query_params = params
125
+ else
126
+ body = params
127
+ end
128
+
129
+ # This works around an edge case where we end up with both query
130
+ # parameters in `query_params` and query parameters that are appended
131
+ # onto the end of the given path. In this case, Faraday will silently
132
+ # discard the URL's parameters which may break a request.
133
+ #
134
+ # Here we decode any parameters that were added onto the end of a path
135
+ # and add them to `query_params` so that all parameters end up in one
136
+ # place and all of them are correctly included in the final request.
137
+ u = URI.parse(path)
138
+ unless u.query.nil?
139
+ query_params ||= {}
140
+ query_params = Hash[URI.decode_www_form(u.query)].merge(query_params)
141
+
142
+ # Reset the path minus any query parameters that were specified.
143
+ path = u.path
144
+ end
145
+
146
+ headers = request_headers(api_key)
147
+ .update(Util.normalize_headers(headers))
148
+
149
+ Util.log_debug("HEADERS:",
150
+ headers: headers)
151
+
152
+
153
+ params_encoder = FaradayMiraklEncoder.new
154
+ url = api_url(path, api_base)
155
+
156
+ if !body.nil?
157
+ Util.log_debug("BODY:",
158
+ body: body,
159
+ bodyencoded: body.to_json)
160
+
161
+
162
+ body = body.to_json if headers['Content-Type'] == 'application/json'
163
+ end
164
+
165
+ # stores information on the request we're about to make so that we don't
166
+ # have to pass as many parameters around for logging.
167
+ context = RequestLogContext.new
168
+ context.api_key = api_key
169
+ context.body = body ? body : nil # TODO : Refactor this.
170
+ # context.body = body ? body : nil
171
+ context.method = method
172
+ context.path = path
173
+ context.query_params = if query_params
174
+ params_encoder.encode(query_params)
175
+ end
176
+
177
+ # note that both request body and query params will be passed through
178
+ # `FaradayMiraklEncoder`
179
+ http_resp = execute_request_with_rescues(api_base, context) do
180
+ conn.run_request(method, url, body, headers) do |req|
181
+
182
+ Util.log_debug("BODYSOUP:",
183
+ body: body)
184
+
185
+ req.options.open_timeout = Mirakl.open_timeout
186
+ req.options.params_encoder = params_encoder
187
+ req.options.timeout = Mirakl.read_timeout
188
+ req.params = query_params unless query_params.nil?
189
+ end
190
+ end
191
+
192
+ begin
193
+ resp = MiraklResponse.from_faraday_response(http_resp)
194
+ rescue JSON::ParserError
195
+ raise general_api_error(http_resp.status, http_resp.body)
196
+ end
197
+
198
+ # Allows MiraklClient#request to return a response object to a caller.
199
+ @last_response = resp
200
+ [resp, api_key]
201
+ end
202
+
203
+ private def general_api_error(status, body)
204
+ APIError.new("Invalid response object from API: #{body.inspect} " \
205
+ "(HTTP response code was #{status})",
206
+ http_status: status, http_body: body)
207
+ end
208
+
209
+
210
+
211
+ # Used to workaround buggy behavior in Faraday: the library will try to
212
+ # reshape anything that we pass to `req.params` with one of its default
213
+ # encoders. I don't think this process is supposed to be lossy, but it is
214
+ # -- in particular when we send our integer-indexed maps (i.e. arrays),
215
+ # Faraday ends up stripping out the integer indexes.
216
+ #
217
+ # We work around the problem by implementing our own simplified encoder and
218
+ # telling Faraday to use that.
219
+ #
220
+ # The class also performs simple caching so that we don't have to encode
221
+ # parameters twice for every request (once to build the request and once
222
+ # for logging).
223
+ #
224
+ # When initialized with `multipart: true`, the encoder just inspects the
225
+ # hash instead to get a decent representation for logging. In the case of a
226
+ # multipart request, Faraday won't use the result of this encoder.
227
+ class FaradayMiraklEncoder
228
+ def initialize
229
+ @cache = {}
230
+ end
231
+
232
+ # This is quite subtle, but for a `multipart/form-data` request Faraday
233
+ # will throw away the result of this encoder and build its body.
234
+ def encode(hash)
235
+ @cache.fetch(hash) do |k|
236
+ @cache[k] = Util.encode_parameters(hash)
237
+ end
238
+ end
239
+
240
+ # We should never need to do this so it's not implemented.
241
+ def decode(_str)
242
+ raise NotImplementedError,
243
+ "#{self.class.name} does not implement #decode"
244
+ end
245
+ end
246
+
247
+
248
+ private def check_api_key!(api_key)
249
+ unless api_key
250
+ raise AuthenticationError, "No API key provided. " \
251
+ 'Set your API key using "Mirakl.api_key = <API-KEY>". '
252
+ end
253
+
254
+ return unless api_key =~ /^\s/
255
+
256
+ raise AuthenticationError, "Your API key is invalid, as it contains " \
257
+ "whitespace. (HINT: You can double-check your API key from the " \
258
+ "Mirakl web interface"
259
+ end
260
+
261
+ private def execute_request_with_rescues(api_base, context)
262
+ begin
263
+ log_request(context)
264
+ resp = yield
265
+ context = context.dup_from_response(resp)
266
+ log_response(context, resp.status, resp.body)
267
+
268
+ # We rescue all exceptions from a request so that we have an easy spot to
269
+ # implement our retry logic across the board. We'll re-raise if it's a
270
+ # type of exception that we didn't expect to handle.
271
+ rescue StandardError => e
272
+ # If we modify context we copy it into a new variable so as not to
273
+ # taint the original on a retry.
274
+ error_context = context
275
+
276
+ if e.respond_to?(:response) && e.response
277
+ error_context = context.dup_from_response(e.response)
278
+ log_response(error_context,
279
+ e.response[:status], e.response[:body])
280
+ else
281
+ log_response_error(error_context, e)
282
+ end
283
+
284
+ # if self.class.should_retry?(e, num_retries)
285
+ # num_retries += 1
286
+ # sleep self.class.sleep_time(num_retries)
287
+ # retry
288
+ # end
289
+
290
+ case e
291
+ when Faraday::ClientError
292
+ if e.response
293
+ handle_error_response(e.response, error_context)
294
+ else
295
+ handle_network_error(e, error_context, num_retries, api_base)
296
+ end
297
+
298
+ # Only handle errors when we know we can do so, and re-raise otherwise.
299
+ # This should be pretty infrequent.
300
+ else
301
+ raise
302
+ end
303
+ end
304
+
305
+ resp
306
+ end
307
+
308
+
309
+ private def handle_error_response(http_resp, context)
310
+ begin
311
+ resp = MiraklResponse.from_faraday_hash(http_resp)
312
+ Util.log_debug("RESP:",
313
+ resp: resp.data)
314
+
315
+ error_data = resp.data
316
+
317
+ raise MiraklError, "Indeterminate error" unless error_data
318
+ rescue JSON::ParserError
319
+ raise general_api_error(http_resp[:status], http_resp[:body])
320
+ end
321
+
322
+ error = specific_api_error(resp, error_data, context)
323
+
324
+ error.response = resp
325
+ raise(error)
326
+ end
327
+
328
+ private def specific_api_error(resp, error_data, context)
329
+ Util.log_error("Mirakl API error",
330
+ status: resp.http_status,
331
+ error_data: error_data)
332
+
333
+ # The standard set of arguments that can be used to initialize most of
334
+ # the exceptions.
335
+ opts = {
336
+ http_body: resp.http_body,
337
+ http_headers: resp.http_headers,
338
+ http_status: resp.http_status,
339
+ json_body: resp.data,
340
+ }
341
+
342
+ case resp.http_status
343
+ when 400, 404
344
+ if resp.data.key?(:errors)
345
+ InvalidRequestError.new(
346
+ resp.data[:errors][0][:message], resp.data[:errors][0][:field],
347
+ opts
348
+ )
349
+ else
350
+ APIError.new(resp.data[:message], opts)
351
+ end
352
+ when 401
353
+ AuthenticationError.new(resp.data[:message], opts)
354
+ when 403
355
+ PermissionError.new(resp.data[:message], opts)
356
+ when 429
357
+ RateLimitError.new(resp.data[:message], opts)
358
+ else
359
+ APIError.new(resp.data[:message], opts)
360
+ end
361
+ end
362
+
363
+
364
+ private def handle_network_error(error, context, num_retries,
365
+ api_base = nil)
366
+ Util.log_error("Mirakl network error",
367
+ error_message: error.message)
368
+
369
+ case error
370
+ when Faraday::ConnectionFailed
371
+ message = "Unexpected error communicating when trying to connect to " \
372
+ "Mirakl. You may be seeing this message because your DNS is not " \
373
+ "working. To check, try running `host mirakl.com` from the " \
374
+ "command line."
375
+
376
+ when Faraday::SSLError
377
+ message = "Could not establish a secure connection to Mirakl, you " \
378
+ "may need to upgrade your OpenSSL version. To check, try running " \
379
+ "`openssl s_client -connect api.mirakl.com:443` from the command " \
380
+ "line."
381
+
382
+ when Faraday::TimeoutError
383
+ api_base ||= Mirakl.api_base
384
+ message = "Could not connect to Mirakl (#{api_base}). " \
385
+ "Please check your internet connection and try again."
386
+
387
+ else
388
+ message = "Unexpected error communicating with Mirakl."
389
+
390
+ end
391
+
392
+ raise APIConnectionError,
393
+ message + "\n\n(Network error: #{error.message})"
394
+ end
395
+
396
+ private def request_headers(api_key)
397
+ user_agent = "Mirakl/vX RubyBindings/#{Mirakl::VERSION}"
398
+
399
+ headers = {
400
+ "User-Agent" => user_agent,
401
+ "Authorization" => "#{api_key}",
402
+ "Content-Type" => "application/json",
403
+ }
404
+
405
+ headers
406
+ end
407
+
408
+ private def api_url(url = "", api_base = nil)
409
+ (api_base || Mirakl.api_base) + url
410
+ end
411
+
412
+
413
+ private def log_request(context)
414
+ Util.log_info("Request to Mirakl API",
415
+ method: context.method,
416
+ path: context.path)
417
+ Util.log_debug("Request details",
418
+ body: context.body,
419
+ query_params: context.query_params)
420
+ end
421
+
422
+ private def log_response(context, status, body)
423
+ Util.log_info("Response from Mirakl API",
424
+ method: context.method,
425
+ path: context.path,
426
+ status: status)
427
+ Util.log_debug("Response details",
428
+ body: body)
429
+
430
+ return unless context.request_id
431
+ end
432
+
433
+ private def log_response_error(context, error)
434
+ Util.log_error("Request error",
435
+ error_message: error.message,
436
+ method: context.method,
437
+ path: context.path)
438
+ end
439
+
440
+ # RequestLogContext stores information about a request that's begin made so
441
+ # that we can log certain information. It's useful because it means that we
442
+ # don't have to pass around as many parameters.
443
+ class RequestLogContext
444
+ attr_accessor :body
445
+ attr_accessor :api_key
446
+ attr_accessor :method
447
+ attr_accessor :path
448
+ attr_accessor :query_params
449
+ attr_accessor :request_id
450
+
451
+ # The idea with this method is that we might want to update some of
452
+ # context information because a response that we've received from the API
453
+ # contains information that's more authoritative than what we started
454
+ # with for a request. For example, we should trust whatever came back in
455
+ # a `Mirakl-Version` header beyond what configuration information that we
456
+ # might have had available.
457
+ def dup_from_response(resp)
458
+ return self if resp.nil?
459
+
460
+ # Faraday's API is a little unusual. Normally it'll produce a response
461
+ # object with a `headers` method, but on error what it puts into
462
+ # `e.response` is an untyped `Hash`.
463
+ headers = if resp.is_a?(Faraday::Response)
464
+ resp.headers
465
+ else
466
+ resp[:headers]
467
+ end
468
+
469
+ context = dup
470
+ context
471
+ end
472
+ end
473
+
474
+
475
+ end
476
+ end