2024-04-29 00:17:15 +02:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2023-05-30 11:41:51 +02:00
|
|
|
class API::Client
|
|
|
|
include Dry::Monads[:result]
|
|
|
|
|
|
|
|
TIMEOUT = 10
|
|
|
|
|
|
|
|
def call(url:, params: nil, body: nil, json: nil, headers: nil, method: :get, authorization_token: nil, schema: nil, timeout: TIMEOUT)
|
|
|
|
response = case method
|
|
|
|
when :get
|
|
|
|
Typhoeus.get(url,
|
2023-10-16 13:36:06 +02:00
|
|
|
headers: headers_with_authorization(headers, false, authorization_token),
|
2023-05-30 11:41:51 +02:00
|
|
|
params:,
|
|
|
|
timeout: TIMEOUT)
|
|
|
|
when :post
|
|
|
|
Typhoeus.post(url,
|
|
|
|
headers: headers_with_authorization(headers, json, authorization_token),
|
|
|
|
body: json.nil? ? body : json.to_json,
|
|
|
|
timeout: TIMEOUT)
|
|
|
|
end
|
|
|
|
handle_response(response, schema:)
|
2024-08-01 17:51:46 +02:00
|
|
|
rescue StandardError => reason
|
|
|
|
if reason.is_a?(URI::InvalidURIError)
|
|
|
|
Failure(Error[:uri, 0, false, reason])
|
|
|
|
else
|
|
|
|
Failure(Error[:error, 0, false, reason])
|
|
|
|
end
|
2023-05-30 11:41:51 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def headers_with_authorization(headers, json, authorization_token)
|
|
|
|
headers = headers || {}
|
|
|
|
headers['authorization'] = "Bearer #{authorization_token}" if authorization_token.present?
|
|
|
|
headers['content-type'] = 'application/json' if json.present?
|
|
|
|
headers
|
|
|
|
end
|
|
|
|
|
|
|
|
OK = Data.define(:body, :response)
|
|
|
|
Error = Data.define(:type, :code, :retryable, :reason)
|
|
|
|
|
|
|
|
def handle_response(response, schema:)
|
|
|
|
if response.success?
|
|
|
|
scope = Sentry.get_current_scope
|
|
|
|
if scope.extra.key?(:external_id)
|
|
|
|
scope.set_extras(raw_body: response.body.to_s)
|
|
|
|
end
|
|
|
|
body = parse_body(response.body)
|
|
|
|
case body
|
|
|
|
in Success(body)
|
2023-10-16 13:36:06 +02:00
|
|
|
if !schema || schema.valid?(body.deep_stringify_keys)
|
|
|
|
Success(OK[body, response])
|
2023-05-30 11:41:51 +02:00
|
|
|
else
|
|
|
|
Failure(Error[:schema, response.code, false, SchemaError.new(schema.validate(body))])
|
|
|
|
end
|
|
|
|
in Failure(reason)
|
|
|
|
Failure(Error[:json, response.code, false, reason])
|
|
|
|
end
|
|
|
|
elsif response.timed_out?
|
|
|
|
Failure(Error[:timeout, response.code, true, HTTPError.new(response)])
|
|
|
|
elsif response.code != 0
|
|
|
|
Failure(Error[:http, response.code, true, HTTPError.new(response)])
|
|
|
|
else
|
|
|
|
Failure(Error[:network, response.code, true, HTTPError.new(response)])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def parse_body(body)
|
2023-10-16 13:36:06 +02:00
|
|
|
Success(JSON.parse(body, symbolize_names: true))
|
2023-05-30 11:41:51 +02:00
|
|
|
rescue JSON::ParserError => error
|
|
|
|
Failure(error)
|
|
|
|
end
|
|
|
|
|
|
|
|
class SchemaError < StandardError
|
|
|
|
attr_reader :errors
|
|
|
|
|
|
|
|
def initialize(errors)
|
|
|
|
@errors = errors.to_a
|
|
|
|
|
|
|
|
super(@errors.map(&:to_json).join("\n"))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class HTTPError < StandardError
|
|
|
|
attr_reader :response
|
|
|
|
|
|
|
|
def initialize(response)
|
|
|
|
@response = response
|
|
|
|
|
|
|
|
uri = URI.parse(response.effective_url)
|
|
|
|
|
|
|
|
msg = <<~TEXT
|
|
|
|
url: #{uri.host}#{uri.path}
|
|
|
|
HTTP error code: #{response.code}
|
|
|
|
body: #{CGI.escape(response.body)}
|
|
|
|
curl message: #{response.return_message}
|
|
|
|
total time: #{response.total_time}
|
|
|
|
connect time: #{response.connect_time}
|
|
|
|
response headers: #{response.headers}
|
|
|
|
TEXT
|
|
|
|
|
|
|
|
super(msg)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|