Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance parse_response Method to Improve Error Handling and Logging #153

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 59 additions & 50 deletions lib/checkout_sdk/api_client.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# frozen_string_literal: true

require 'csv'

module CheckoutSdk
class ApiClient
attr_accessor :client, :multipart_client, :log

# @param [CheckoutConfiguration] configuration
# @param [String] uri
def initialize(configuration, uri)
@client = configuration.http_client.clone
@client.url_prefix = uri
Expand All @@ -14,42 +14,27 @@ def initialize(configuration, uri)
@log = configuration.logger
end

# @param [String] path
# @param [SdkAuthorization] authorization
# @param [Object] params
def invoke_get(path,
authorization,
params = nil)
def invoke_get(path, authorization, params = nil)
invoke(:get, path, authorization, params: params)
end

def invoke_post(path,
authorization,
request = nil,
idempotency_key = nil)
def invoke_post(path, authorization, request = nil, idempotency_key = nil)
invoke(:post, path, authorization, request, idempotency_key)
end

def invoke_put(path,
authorization,
request)
def invoke_put(path, authorization, request)
invoke(:put, path, authorization, request)
end

def invoke_patch(path,
authorization,
request = nil)
def invoke_patch(path, authorization, request = nil)
invoke(:patch, path, authorization, request)
end

def invoke_delete(path,
authorization)
def invoke_delete(path, authorization)
invoke(:delete, path, authorization)
end

def submit_file(path,
authorization,
request)
def submit_file(path, authorization, request)
upload(path, authorization, request)
end

Expand All @@ -58,7 +43,7 @@ def submit_file(path,
def invoke(method, path, authorization, body = nil, idempotency_key = nil, params: nil)
path = append_params(path, params) unless params.nil?

headers = get_default_headers authorization
headers = default_headers(authorization)
headers[:'Content-Type'] = 'application/json'
headers[:'Cko-Idempotency-Key'] = idempotency_key unless idempotency_key.nil?

Expand All @@ -71,26 +56,23 @@ def invoke(method, path, authorization, body = nil, idempotency_key = nil, param
raise CheckoutApiException, e.response
end

parse_response response
parse_response(response)
end

def get_default_headers(authorization)
{
'User-Agent': "checkout-sdk-ruby/#{VERSION}",
Accept: 'application/json',
Authorization: authorization.authorization_header
}
def default_headers(authorization)
{ 'User-Agent': "checkout-sdk-ruby/#{VERSION}", Accept: 'application/json',
Authorization: authorization.authorization_header }
end

def append_params(path, input_params)
raise CheckoutArgumentException, 'Query parameters were not provided' if input_params.nil?

if input_params.is_a? String
params = input_params
else
hash = CheckoutSdk::JsonSerializer.to_custom_hash(input_params)
params = URI.encode_www_form(hash)
end
params = if input_params.is_a? String
input_params
else
hash = CheckoutSdk::JsonSerializer.to_custom_hash(input_params)
URI.encode_www_form(hash)
end

"#{path}?#{params}"
end
Expand All @@ -103,16 +85,16 @@ def build_multipart_request(file_request, file)
MIME::Types.type_for(file_request.file).first,
File.basename(file_request.file)
),
:purpose => file_request.purpose
purpose: file_request.purpose
}
end

def upload(path, authorization, file_request)
headers = get_default_headers authorization
headers = default_headers(authorization)

file = File.open(file_request.file)

form = build_multipart_request file_request, file
form = build_multipart_request(file_request, file)

begin
@log.info "post: /#{path}"
Expand All @@ -123,28 +105,55 @@ def upload(path, authorization, file_request)
file.close
end

parse_response response
parse_response(response)
end

def parse_response(response)
raise CheckoutApiException, response if response.status < 200 || response.status >= 400
raise CheckoutApiException, response if response.status < 200 || response.status >= 300

metadata = CheckoutUtils.map_to_http_metadata(response)
body = parse_json_or_contents(response)
body = OpenStruct.new if body.nil?
body = OpenStruct.new(items: body) if body.is_a? Array
body.http_metadata = metadata if body.is_a? OpenStruct
body = parse_body(response)

if body.is_a?(Array)
body = OpenStruct.new(items: body)
elsif !body.is_a?(OpenStruct)
body = OpenStruct.new(contents: body)
end

body.http_metadata = metadata if body.is_a?(OpenStruct)

body
rescue JSON::ParserError => e
raise CheckoutApiException.new(response, "Error parsing JSON: #{e.message}")
rescue StandardError => e
@log&.error("Unexpected error occurred: #{e.message}")
raise
end

def parse_json_or_contents(response)
return if response.body.nil? || response.body == ''
def parse_body(response)
content_type = response.headers['Content-Type']
return OpenStruct.new if response.body.nil? || response.body.empty?

if response.body.start_with?('{', '[')
JSON.parse(response.body, object_class: OpenStruct)
if content_type&.include?('application/json')
parsed_value = JSON.parse(response.body)
deep_convert_to_ostruct(parsed_value)
elsif content_type&.include?('text/csv')
csv_data = CSV.parse(response.body, headers: true)
OpenStruct.new(csv: csv_data)
else
OpenStruct.new(contents: response.body)
end
end

def deep_convert_to_ostruct(obj)
case obj
when Hash
OpenStruct.new(obj.transform_values { |value| deep_convert_to_ostruct(value) })
when Array
obj.map { |item| deep_convert_to_ostruct(item) }
else
obj
end
end
end
end
5 changes: 2 additions & 3 deletions lib/checkout_sdk/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ class Client
attr_reader :api_client,
:authorization_type,
:configuration
protected :api_client, :authorization_type, :configuration

protected

# @param [CheckoutSdk::ApiClient] api_client
# @param [CheckoutConfiguration] configuration
Expand All @@ -22,8 +23,6 @@ def initialize(api_client, configuration, authorization_type)
@configuration = configuration
end

protected

# @param [Array] args
def build_path(*args)
args.join('/')
Expand Down
32 changes: 24 additions & 8 deletions lib/checkout_sdk/error.rb
Original file line number Diff line number Diff line change
@@ -1,30 +1,46 @@
# frozen_string_literal: true

module CheckoutSdk
class CheckoutException < StandardError
end
class CheckoutException < StandardError; end

class CheckoutArgumentException < CheckoutException; end

class CheckoutAuthorizationException < CheckoutException
def self.invalid_authorization(authorization_type)
CheckoutAuthorizationException.new("Operation requires #{authorization_type} authorization type")
new("Operation requires #{authorization_type} authorization type.")
end

def self.invalid_key(key_type)
CheckoutAuthorizationException.new("#{key_type} is required for this operation.")
new("#{key_type} is required for this operation.")
end
end

class CheckoutApiException < CheckoutException
attr_reader :http_metadata, :error_details

def initialize(response)
def initialize(response, message = nil)
@http_metadata = CheckoutUtils.map_to_http_metadata(response)
if !http_metadata.body.nil? && http_metadata.body != ''
@error_details = JSON.parse(http_metadata.body, object_class: OpenStruct)
@error_details = parse_error_details(http_metadata.body)
super(message || build_error_message)
end

private

def parse_error_details(body)
return if body.nil? || body.empty?

JSON.parse(body, object_class: OpenStruct)
rescue JSON::ParserError
nil
end

def build_error_message
message = "The API response status code (#{http_metadata.status_code}) does not indicate success."
if @error_details && !@error_details.to_h.empty?
details = @error_details.to_h.map { |key, value| "#{key}: #{value}" }.join(', ')
message += " Details: #{details}."
end
super("The API response status code (#{http_metadata.status_code}) does not indicate success.")
message
end
end
end
Loading
Loading