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 +7 -0
- data/.gitignore +6 -0
- data/Gemfile +39 -0
- data/LICENSE +22 -0
- data/bin/mirakl-console.rb +16 -0
- data/lib/mirakl/api_operations/request.rb +54 -0
- data/lib/mirakl/errors.rb +62 -0
- data/lib/mirakl/mirakl_client.rb +476 -0
- data/lib/mirakl/mirakl_object.rb +48 -0
- data/lib/mirakl/mirakl_response.rb +51 -0
- data/lib/mirakl/requests/dr11.rb +18 -0
- data/lib/mirakl/requests/dr74.rb +45 -0
- data/lib/mirakl/requests/s07.rb +18 -0
- data/lib/mirakl/requests/s20.rb +18 -0
- data/lib/mirakl/util.rb +282 -0
- data/lib/mirakl/version.rb +5 -0
- data/lib/mirakl.rb +94 -0
- data/mirakl.gemspec +33 -0
- metadata +91 -0
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
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
|