lib/shippo/api/request.rb
require 'rest_client'
require 'socket'
require 'json'
require 'set'
require 'awesome_print'
require 'shippo/exceptions'
module Shippo
module API
#
# This class is the primary *internal* Interface to the Shippo API.
#
# Public consumers should use Model API, and perform actions on models
# rather than submit requests directly using this class.
#
# +Request+ instance is created with the intention to execute a single
# API call and once executed, it stores +response+ object. Used
# requests can not be re-executed.
#
# == Example
#
# @request = Shippo::API::Request.new(
# method: :get,
# uri: '/address,
# params: { object_id: 1 },
# headers: { 'Last-Modified' => '1213145' }
# begin
# @response = @request.execute
# Shippo::Address.from(@response)
# # =>
#
class Request
attr_accessor :username, :password
attr_accessor :method, :url, :params, :headers
# Result of the execute method is stored in #response and #parsed_response
attr_accessor :response, :parsed_response, :redirection_history
# @param [symbol] method :get or any other method such as :put, :post, etc.
# @param [String] uri URI component appended to the base URL
# @param [Hash] params parameters to append to the URL
# @param [Hash] headers headers hash sent to the server
def initialize(method:, uri:, params: {}, headers: {})
self.method = method
self.params = params
self.headers = headers
self.url = api_url(uri)
self.response = nil
end
def execute
raise ArgumentError.new('Response is already defined, create another Request object.') if self.response
validate!
begin
self.response = shippo_phone_home
self.parsed_response = JSON::parse(response.body, { symbolize_names: true })
rescue ::RestClient::Unauthorized => e
raise Shippo::Exceptions::AuthenticationError.new(e.message)
rescue ::RestClient::BadRequest => e
if e.respond_to?(:response) && e.response.is_a?(RestClient::Response)
awesome_print_response(e) if Shippo::API.debug?
raise Shippo::Exceptions::APIServerError.new('Backend responded with an error',
self, e.response, e.message)
end
rescue ::JSON::JSONError, ::JSON::ParserError => e
raise Shippo::Exceptions::InvalidJsonError.new(e.message)
rescue ::RestClient::BadRequest => e
raise Shippo::Exceptions::InvalidInputError.new(e.inspect)
rescue ::RestClient::Exception => e
raise Shippo::Exceptions::ConnectionError.new(connection_error_message(url, e))
rescue StandardError => e
raise Shippo::Exceptions::ConnectionError.new(connection_error_message(url, e)) if e.message =~ /TCP|connection|getaddrinfo/
STDERR.puts "#{self.class.name}: Internal error occurred while connecting to #{url}: #{e.message}"
STDERR.puts 'Stack Trace'
STDERR.puts e.backtrace.join("\n")
raise Shippo::Exceptions::Error.new(e)
end
self.parsed_response
end
private
def shippo_phone_home
payload = nil
request_url = url
(method == :get) ? request_url = params_to_url(params, url) : payload = params.to_json
setup_headers!(headers)
opts = make_opts!(headers, method, payload, request_url)
if Shippo::API.debug?
puts "\nCLIENT REQUEST:"
ap opts
end
make_request!(opts)
end
def make_request!(opts)
RestClient::Request.execute(opts) { |response, &block|
if [301, 302, 307].include? response.code
response.follow_redirection(&block)
else
response.return!(&block)
end
}
end
def make_opts!(headers, method, payload, url)
{ :headers => headers,
:method => method,
:payload => payload,
:url => url,
:open_timeout => ::Shippo::API.open_timeout,
:timeout => ::Shippo::API.read_timeout,
:user => username,
:password => password,
:user_agent => 'Shippo/v2.0 RubyBindings'
}
end
def setup_headers!(headers)
additional_hash = {
:accept => :json,
:content_type => :json,
:Authorization => "ShippoToken #{token}"
}
additional_hash[:'Shippo-API-Version'] = version if version
headers.merge!(additional_hash)
end
def awesome_print_response(e)
begin
h = JSON.parse(e.response)
if h
puts "\nSERVER RESPONSE:"
ap JSON.parse(e.response)
end
rescue nil
end
end
def base
::Shippo::API.base
end
def token
::Shippo::API.token
end
def version
::Shippo::API.version
end
def connection_error_message(url, error)
%Q[Could not connect to the Shippo API, via URL
#{url}.
Please check your Internet connection, try again, if the problem
persists please contact Shippo Customer Support.
Error Description:
#{error.class.name} ⇨ #{error.message}].gsub(/^\s*/, '')
end
def api_url(uri_component = '')
base + uri_component
end
def params_to_url(params, url)
pairs = []
params.each { |k, v|
pairs.push "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"
}
url += "?#{pairs.join('&')}" unless pairs.empty?
url
end
def validate!
raise Shippo::Exceptions::AuthenticationError.new(
'API credentials seems to be missing, perhaps you forgot to set Shippo::API.token?') \
unless token
end
end
end
end