mirakl 0.0.1

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 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