mirakl 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|