lib/artirix_data_models/gateways/data_gateway.rb
class ArtirixDataModels::DataGateway
attr_reader :connection, :post_as_json
def initialize(connection: nil,
post_as_json: true,
ensure_relative: false,
timeout: nil,
authorization_bearer: nil,
authorization_token_hash: nil)
@connection = connection || ConnectionLoader.default_connection
@post_as_json = !!post_as_json
@authorization_bearer = authorization_bearer
@authorization_token_hash = authorization_token_hash
@timeout = timeout
@ensure_relative = !!ensure_relative
end
def ensure_relative?
!!@ensure_relative
end
def get(path, **opts)
call :get, path, **opts
end
def post(path, **opts)
call :post, path, **opts
end
def put(path, **opts)
call :put, path, **opts
end
def patch(path, **opts)
call :patch, path, **opts
end
def delete(path, **opts)
call :delete, path, **opts
end
def call(method,
path,
authorization_bearer: nil,
authorization_token_hash: nil,
body: nil,
cache_adaptor: nil,
fake: false,
fake_response: nil,
headers: nil,
json_body: true,
json_parse_response: true,
response_adaptor: nil,
timeout: nil,
**_ignored_options)
if fake
result = fake_response.respond_to?(:call) ? fake_response.call : fake_response
elsif cache_adaptor.present?
result = cache_adaptor.call do
perform method,
path: path,
body: body,
json_body: json_body,
timeout: timeout,
authorization_bearer: authorization_bearer,
authorization_token_hash: authorization_token_hash,
headers: headers
end
else
result = perform method,
path: path,
body: body,
json_body: json_body,
timeout: timeout,
authorization_bearer: authorization_bearer,
authorization_token_hash: authorization_token_hash,
headers: headers
end
parse_response result: result,
json_parse_response: json_parse_response,
response_adaptor: response_adaptor,
method: method,
path: path
rescue => e
# if anything goes wrong => delete the cache just in case.
if cache_adaptor.present? && cache_adaptor.respond_to?(:delete)
cache_adaptor.delete
end
raise e
end
private
def perform_get(path, **opts)
perform :get, path: path, **opts
end
def perform_post(path, **opts)
perform :post, path: path, **opts
end
def perform_put(path, **opts)
perform :put, path: path, **opts
end
def perform_patch(path, **opts)
perform :patch, path: path, **opts
end
def perform_delete(path, **opts)
perform :delete, path: path, **opts
end
def perform(method,
path:,
body: nil,
json_body: true,
timeout: nil,
authorization_bearer: nil,
authorization_token_hash: nil,
headers: nil)
pars = {
path: path,
body: body,
json_body: json_body,
timeout: timeout,
authorization_bearer: authorization_bearer,
authorization_token_hash: authorization_token_hash,
headers: headers
}
response = connect(method, pars)
treat_response(response, method, path)
end
# for options `timeout`, `authorization_bearer` and `authorization_token_hash`:
# if `nil` is passed (or param is omitted) it will try to use the default passed on the gateway creation
# but if `false` is passed, it will stay as false (can be used to override a default option passed on gateway creation)
def connect(method,
path:,
body: nil,
json_body: true,
timeout: nil,
authorization_bearer: nil,
authorization_token_hash: nil,
headers: nil)
timeout = timeout.nil? ? @timeout : timeout
authorization_bearer = authorization_bearer.nil? ? @authorization_bearer : authorization_bearer
authorization_token_hash = authorization_token_hash.nil? ? @authorization_token_hash : authorization_token_hash
if ensure_relative?
path = path.to_s.start_with?('/') ? path.to_s[1..-1] : path
end
connection.send(method, path) do |req|
req.options.timeout = timeout if timeout.present?
req.headers['Authorization'] = Faraday::Request::Authorization.header(:Bearer, authorization_bearer) if authorization_bearer.present?
req.headers['Authorization'] = Faraday::Request::Authorization.header(:Token, authorization_token_hash) if authorization_token_hash.present?
unless body.nil?
if json_body
req.body = body_to_json body
req.headers['Content-Type'] = 'application/json'
else
req.body = body
end
end
Array(headers).each do |key, value|
req.headers[key.to_s] = value
end
end
rescue Faraday::ConnectionFailed, Faraday::Error::TimeoutError, Errno::ETIMEDOUT => e
raise ConnectionError,
path: path,
method: method,
message: "method: #{method}, path: #{path}, error: #{e}"
end
def body_to_json(body)
case body
when String
body
else
body.to_json
end
end
def parse_response(result:, response_adaptor:, path:, method:, json_parse_response: true)
if result.blank?
parsed_response = nil
elsif json_parse_response
parsed_response = Oj.load result, symbol_keys: true
else
parsed_response = result
end
if response_adaptor.present?
response_adaptor.call parsed_response
else
parsed_response
end
rescue Oj::ParseError => e
raise ParseError,
path: path,
method: method,
response_body: result,
message: e.message
end
#######################
# EXCEPTION TREATMENT #
#######################
def treat_response(response, method, path)
self.class.treat_response(response, method, path)
end
def exception_for_status(response_status)
self.class.exception_for_status(response_status)
end
def self.treat_response(response, method, path)
return response.body if response.success?
klass = exception_for_status(response.status)
raise klass,
path: path,
method: method,
response_body: response.body,
response_status: response.status
end
def self.exception_for_status(response_status)
case response_status.to_i
when 404
NotFound
when 406
NotAcceptable
when 422
UnprocessableEntity
when 409
Conflict
when 400
BadRequest
when 401
Unauthorized
when 403
Forbidden
when 408
RequestTimeout
when 429
TooManyRequests
when 500
ServerError
else
GatewayError
end
end
module ConnectionLoader
class << self
def default_connection(**others)
connection_by_config_key :data_gateway, **others
end
def connection_by_config_key(config_key, **others)
connection config: ArtirixDataModels.configuration.send(config_key), **others
end
def connection(config: {},
url: nil,
login: nil,
password: nil,
bearer_token: nil,
token_hash: nil,
log_body_request: nil,
log_body_response: nil,
faraday_build_proc: nil,
faraday_adapter: nil)
url ||= config.try :url
login ||= config.try :login
password ||= config.try :password
bearer_token ||= config.try :bearer_token
token_hash ||= config.try :token_hash
faraday_adapter ||= config.try(:faraday_adapter) || Faraday.default_adapter
log_body_request ||= log_body_request.nil? ? config.try(:log_body_request) : log_body_request
log_body_response ||= log_body_response.nil? ? config.try(:log_body_response) : log_body_response
raise InvalidConnectionError, 'no url given, nor is it present in `config.url`' unless url.present?
Faraday.new(url: url, request: { params_encoder: Faraday::FlatParamsEncoder }) do |faraday|
if faraday_build_proc.present? && faraday_build_proc.respond_to?(:call)
faraday_build_proc.call faraday
end
faraday.request :url_encoded # form-encode POST params
# faraday.response :logger # log requests to STDOUT
faraday.response :logger, ::Logger.new(STDOUT), bodies: { request: log_body_request, response: log_body_response }
if login.present? || password.present?
faraday.basic_auth(login, password)
elsif bearer_token.present?
faraday.authorization :Bearer, bearer_token
elsif token_hash.present?
faraday.authorization :Token, token_hash
end
faraday.adapter faraday_adapter
end
end
end
class InvalidConnectionError < StandardError
end
end
class Error < StandardError
attr_reader :path, :method, :response_status, :response_body, :message
alias_method :msg, :message
def initialize(*args)
case args.size
when 0
message = nil
options = {}
when 1
if args.first.kind_of? Hash
options = args.first
message = nil
else
message = args.first
options = {}
end
else
message = args[0]
options = args[1]
if message.kind_of? Hash
options, message = message, options
end
end
if message.present?
options[:message] = message
end
build_from_options(options) if options.present?
end
def json_response_body
return nil unless response_body.present?
Oj.load response_body, symbol_keys: true
rescue Oj::Error # in case it's not json
nil
end
def to_s
msg = super
msg = nil if msg == self.class.to_s
parts = {
path: path,
method: method,
response_status: response_status,
response_body: response_body,
message: msg,
}.select { |_, v| v.present? }.map { |k, v| "#{k}: #{v.inspect}" }
"#{self.class}: #{parts.join ', '}"
end
def message
to_s
end
def data_hash
{
class: self.class.to_s,
path: path,
method: method,
response_status: response_status,
response_body: response_body,
message: message,
}
end
# for testing
def matches?(other)
other.kind_of? self.class
end
private
def build_from_options(path: nil, method: nil, response_status: nil, response_body: nil, message: nil, **_other)
@path = path
@method = method
@response_status = response_status
@response_body = response_body
@message = message.presence || self.class.to_s
end
end
class ParseError < Error
end
class GatewayError < Error
end
###########################################
# SPECIAL, not subclasses of GatewayError #
###########################################
# 404
class NotFound < Error
end
# 406
class NotAcceptable < Error
end
# 422
class UnprocessableEntity < Error
end
# 409
class Conflict < Error
end
##############################
# subclasses of GatewayError #
##############################
# 400
class BadRequest < GatewayError
end
# 401
class Unauthorized < GatewayError
end
# 403
class Forbidden < GatewayError
end
# 408
class RequestTimeout < GatewayError
end
# 429
class TooManyRequests < GatewayError
end
# 500
class ServerError < GatewayError
end
# generic error
class ConnectionError < GatewayError
end
end