feat(api): HTTP API client
This commit is contained in:
parent
a9f431caa5
commit
c6d728d44f
3 changed files with 106 additions and 0 deletions
1
Gemfile
1
Gemfile
|
@ -30,6 +30,7 @@ gem 'devise-i18n'
|
||||||
gem 'devise-two-factor'
|
gem 'devise-two-factor'
|
||||||
gem 'discard'
|
gem 'discard'
|
||||||
gem 'dotenv-rails', require: 'dotenv/rails-now' # dotenv should always be loaded before rails
|
gem 'dotenv-rails', require: 'dotenv/rails-now' # dotenv should always be loaded before rails
|
||||||
|
gem 'dry-monads'
|
||||||
gem 'elastic-apm'
|
gem 'elastic-apm'
|
||||||
gem 'flipper'
|
gem 'flipper'
|
||||||
gem 'flipper-active_record'
|
gem 'flipper-active_record'
|
||||||
|
|
|
@ -218,7 +218,14 @@ GEM
|
||||||
dotenv (= 2.7.6)
|
dotenv (= 2.7.6)
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
dry-cli (1.0.0)
|
dry-cli (1.0.0)
|
||||||
|
dry-core (1.0.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
dry-inflector (0.2.0)
|
dry-inflector (0.2.0)
|
||||||
|
dry-monads (1.6.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
dry-core (~> 1.0, < 2)
|
||||||
|
zeitwerk (~> 2.6)
|
||||||
dumb_delegator (1.0.0)
|
dumb_delegator (1.0.0)
|
||||||
ecma-re-validator (0.3.0)
|
ecma-re-validator (0.3.0)
|
||||||
regexp_parser (~> 2.0)
|
regexp_parser (~> 2.0)
|
||||||
|
@ -828,6 +835,7 @@ DEPENDENCIES
|
||||||
devise-two-factor
|
devise-two-factor
|
||||||
discard
|
discard
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
|
dry-monads
|
||||||
elastic-apm
|
elastic-apm
|
||||||
factory_bot
|
factory_bot
|
||||||
flipper
|
flipper
|
||||||
|
|
97
app/lib/api/client.rb
Normal file
97
app/lib/api/client.rb
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
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,
|
||||||
|
headers: headers_with_authorization(headers, authorization_token),
|
||||||
|
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:)
|
||||||
|
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)
|
||||||
|
if !schema || schema.valid?(body)
|
||||||
|
Success(OK[body.deep_symbolize_keys, response])
|
||||||
|
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)
|
||||||
|
Success(JSON.parse(body))
|
||||||
|
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
|
Loading…
Reference in a new issue